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 compiler
  • Logger.Debug(string message) — Debug with Categories.Default
  • Logger.Info(string category, string message) — Info level
  • Logger.Info(string message) — Info with Categories.Default
  • Logger.Warning(string category, string message) — Warning level
  • Logger.Warning(string message) — Warning with Categories.Default
  • Logger.Error(string category, string message) — Error level
  • Logger.Error(string message) — Error with Categories.Default
  • Logger.Critical(string category, string message) — Critical level
  • Logger.Critical(string message) — Critical with Categories.Default
  • Logger.Exception(string category, Exception ex, string message = null) — logs an exception with its stack trace and optional context message; throws ArgumentNullException if ex is 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 to Debug in the Editor and Development builds, Warning in Release builds. Set to LogLevel.Off to mute everything — this takes effect immediately, static helpers included.
  • Logger.SetCategoryFilter(string category, LogLevel level) — Override one category's floor; throws ArgumentNullException if the category is null.
  • Logger.ClearCategoryFilter(string category) — Remove a category override, reverting to the global floor; throws ArgumentNullException if the category is null.

LogLevel values

ValueMeaning
DebugPer-frame tracing and verbose detail. Stripped from Release builds entirely.
InfoState transitions and lifecycle events. Always compiled.
WarningRecovered fault, suspect input, deprecated path. Always compiled.
ErrorAn operation failed but the game continues. Always compiled.
CriticalProcess integrity is compromised — the next failure is likely fatal. Always compiled.
OffSentinel — 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 message
  • void LogException(string category, Exception ex, string message) — Emit an exception with its stack trace
  • LogLevel MinimumLevel { get; set; } — Per-instance level floor
  • LogLevel GetEffectiveLevel(string category) — Query the actual floor for a category; throws ArgumentNullException on 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 own Player.log is 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. FileLogger keeps 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 exceeds maxBytes and keeping maxFiles files including the live one. Construct it on the main thread (it reads Application.persistentDataPath once).
    • FileLogger(string path, long maxBytes, int maxFiles) — logs to an explicit path; the directory is created if missing. Throws ArgumentException on 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 any Debug.Log that never went through ILogger. Do not enable this while a UnityConsoleLogger is also active — that logger writes through Debug.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 LogException bypass MinimumLevel — 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, so Expensive() never runs. Free optimization.

  • Name clash with Unity types. Logger and ILogger collide with UnityEngine.Logger and UnityEngine.ILogger when your file has both using CommonGameSystem.Core; and using 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 the Logger.Categories.* constants, never string literals.

  • Filter APIs are main-thread only. Call SetCategoryFilter / ClearCategoryFilter from the main thread. The emit methods (Info, Warning, Error, and the rest) are thread-safe everywhere.

  • GetEffectiveLevel requires a non-null category. Unlike the emit path (which treats null as Categories.Default), the query APIs throw ArgumentNullException on 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.Off for an immediate runtime mute.