Logger
Categorized, filterable logging with build-level muting, a rotating file backend, and pluggable outputs.
Categorized logging for game code and framework · The first service to start · No-op replacement:
NullLogger
What it does
The Logger wraps UnityEngine.Debug.Log in a single, filterable diagnostic system. It gives you build-level muting (Release builds drop Debug-level messages automatically), per-category on/off switches, and a pluggable output backend — the Unity Console by default, a rotating log file, or your own crash-reporting sink. Instead of scattered Debug.Log calls, your game code and every framework service route diagnostics through Logger.Info(category, message) with consistent formatting and one central place to control it all.
Quick example
Suppress console noise while debugging saves only:
using CommonGameSystem.Core;
using UnityEngine;
using Logger = CommonGameSystem.Core.Logger; // Unity has its own Logger type
[DefaultExecutionOrder(100)] // Run after the framework has started
public class DebugConfig : MonoBehaviour
{
private void Awake()
{
// Silence everything except the Save category
Logger.MinimumLevel = LogLevel.Off;
Logger.SetCategoryFilter(Logger.Categories.Save, LogLevel.Debug);
}
}
// Elsewhere — only Save messages appear in the Console
Logger.Info(Logger.Categories.Net, "heartbeat ok"); // Silent
Logger.Info(Logger.Categories.Save, "slot 0 corrupted"); // Shown
You can use the static Logger helpers anywhere without resolving anything — they cache the logging backend for you at startup. If you prefer explicit dependencies (for example, to inject a fake in tests), resolve ILogger from the Service Locator and cache it in Awake.
Full API surface
Emit methods (all static, thread-safe)
Logger.Debug(string category, string message)— Debug level; the whole call site is removed from Release builds by the compilerLogger.Debug(string message)— Debug withCategories.DefaultLogger.Info(string category, string message)— Info levelLogger.Info(string message)— Info withCategories.DefaultLogger.Warning(string category, string message)— Warning levelLogger.Warning(string message)— Warning withCategories.DefaultLogger.Error(string category, string message)— Error levelLogger.Error(string message)— Error withCategories.DefaultLogger.Critical(string category, string message)— Critical levelLogger.Critical(string message)— Critical withCategories.DefaultLogger.Exception(string category, Exception ex, string message = null)— logs an exception with its stack trace and optional context message; throwsArgumentNullExceptionifexis null
A null category on any emit method falls through to Categories.Default.
Filter control (main thread only)
Logger.MinimumLevel { get; set; }— Global level floor. Defaults toDebugin the Editor and Development builds,Warningin Release builds. Set toLogLevel.Offto mute everything — this takes effect immediately, static helpers included.Logger.SetCategoryFilter(string category, LogLevel level)— Override one category's floor; throwsArgumentNullExceptionif the category is null.Logger.ClearCategoryFilter(string category)— Remove a category override, reverting to the global floor; throwsArgumentNullExceptionif the category is null.
LogLevel values
| Value | Meaning |
|---|---|
Debug | Per-frame tracing and verbose detail. Stripped from Release builds entirely. |
Info | State transitions and lifecycle events. Always compiled. |
Warning | Recovered fault, suspect input, deprecated path. Always compiled. |
Error | An operation failed but the game continues. Always compiled. |
Critical | Process integrity is compromised — the next failure is likely fatal. Always compiled. |
Off | Sentinel — set as a floor to filter every level out. |
Standard categories (string constants)
Logger.Categories.Default, .Bootstrap, .ServiceLocator, .Save, .Audio, .UI, .Input, .AI, .Net, .Perf (performance: pools, time, timers), .Configuration, .Scene, .Localization, .Achievements.
Category keys are case-sensitive — always use the constants, never string literals.
ILogger interface (implement this for a custom backend)
void Log(LogLevel level, string category, string message)— Emit a messagevoid LogException(string category, Exception ex, string message)— Emit an exception with its stack traceLogLevel MinimumLevel { get; set; }— Per-instance level floorLogLevel GetEffectiveLevel(string category)— Query the actual floor for a category; throwsArgumentNullExceptionon a null category
Built-in backends
-
UnityConsoleLogger— the default. Registered by Bootstrap; writes formatted lines to the Unity Console. -
FileLogger— an opt-in rotating file logger for post-launch support. Unity's ownPlayer.logis a single undifferentiated stream, and on Windows it is overwritten on the next launch — by the time a player sends it, the evidence is often gone.FileLoggerkeeps categories and levels on every line and rotates files so history survives:using CommonGameSystem.Core; using UnityEngine; [DefaultExecutionOrder(100)] public class FileLoggerInstaller : MonoBehaviour { private void Awake() { var fileLog = new FileLogger(); // {persistentDataPath}/logs/cgs.log fileLog.CaptureUnityLogStream(true); // also record uncaught exceptions ServiceLocator.Replace<ILogger>(fileLog); } }FileLogger(long maxBytes = 4 MB, int maxFiles = 3)— logs to{persistentDataPath}/logs/cgs.log, rotating once the file exceedsmaxBytesand keepingmaxFilesfiles including the live one. Construct it on the main thread (it readsApplication.persistentDataPathonce).FileLogger(string path, long maxBytes, int maxFiles)— logs to an explicit path; the directory is created if missing. ThrowsArgumentExceptionon a null or blank path.FilePath— the absolute path of the file currently being written.CaptureUnityLogStream(bool enabled)— mirrors Unity's own log stream into the file: uncaught exceptions with stack traces, plus anyDebug.Logthat never went throughILogger. Do not enable this while aUnityConsoleLoggeris also active — that logger writes throughDebug.Log, so every framework line would be captured twice.- Thread-safe (every write is serialized through a lock) and never throws: I/O failures are swallowed, and after 8 consecutive write failures the instance disables itself instead of retrying a doomed write on every call. Exceptions logged through
LogExceptionbypassMinimumLevel— filtering out the one record that explains a crash is never what the setting meant. - Implements
IDisposable— dispose it on shutdown to flush and unsubscribe.
-
NullLogger— turns every logging call into a silent no-op:// Mute everything at runtime — takes effect immediately, static helpers included: Logger.MinimumLevel = LogLevel.Off; // Or replace the backend with a silent no-op (picked up by new resolves): ServiceLocator.Replace<ILogger>(new NullLogger());Use it to strip logging overhead from headless or automated builds, test game code without console spam, or benchmark without logging noise. Every other service keeps working normally when logging is silenced — nothing in the framework depends on log output.
Writing your own backend
Any class implementing ILogger can be the backend. For example, forwarding errors to your crash-reporting SDK:
using System;
using CommonGameSystem.Core;
public class CrashReportLogger : ILogger
{
public LogLevel MinimumLevel { get; set; } = LogLevel.Warning;
public void Log(LogLevel level, string category, string message)
{
if (level < MinimumLevel) return;
// Forward to your crash-reporting or analytics SDK here.
Console.WriteLine($"[{level}] [{category}] {message}");
}
public void LogException(string category, Exception ex, string message)
{
Console.WriteLine($"[Exception] [{category}] {message}\n{ex}");
}
public LogLevel GetEffectiveLevel(string category) => MinimumLevel;
}
Register it with ServiceLocator.Replace<ILogger>(new CrashReportLogger()). Code that resolves ILogger from the Service Locator after this call uses your backend. One caveat: the static Logger helpers cache their backend the first time they run, which happens during framework startup — a replacement made later in a Play session does not retarget the static helpers. They pick up your backend at the next Play entry, when their cache resets.
Behavior & edge cases
-
Release builds drop Debug calls entirely.
Logger.Debug($"msg = {Expensive()}")has its whole call site removed by the compiler in Release builds, soExpensive()never runs. Free optimization. -
Name clash with Unity types.
LoggerandILoggercollide withUnityEngine.LoggerandUnityEngine.ILoggerwhen your file has bothusing CommonGameSystem.Core;andusing UnityEngine;. Add aliases at the top of the file:using Logger = CommonGameSystem.Core.Logger; using ILogger = CommonGameSystem.Core.ILogger; -
Worker threads need the startup warm-up. Bootstrap makes one Logger call on the main thread during startup, which makes later worker-thread calls (save I/O, network I/O) safe. If a worker thread logs before that warm-up, the message falls back to plain
Debug.Log. -
Category keys are case-sensitive.
"Net"and"net"are different filters. Always use theLogger.Categories.*constants, never string literals. -
Filter APIs are main-thread only. Call
SetCategoryFilter/ClearCategoryFilterfrom the main thread. The emit methods (Info,Warning,Error, and the rest) are thread-safe everywhere. -
GetEffectiveLevelrequires a non-null category. Unlike the emit path (which treatsnullasCategories.Default), the query APIs throwArgumentNullExceptionon a null category. This surfaces configuration bugs early. -
Mid-session backend swaps don't retarget the static helpers. See the caveat under "Writing your own backend" above. Use
Logger.MinimumLevel = LogLevel.Offfor an immediate runtime mute.
Related pages
- Bootstrap — startup order and the Logger warm-up
- Service Locator — resolving and replacing
ILogger - Save/Load — a service that logs through
Categories.Save - Manual: Troubleshooting — reading framework log output when something goes wrong