日志

分类、可过滤的日志系统,支持按构建级别静音、轮转文件后端与可插拔输出。

面向游戏代码与框架的分类日志 · 第一个启动的服务 · 无操作替代:NullLogger

功能概述

Logger 把 UnityEngine.Debug.Log 包装成一个统一、可过滤的诊断系统。它提供构建级别的静音(Release 构建自动丢弃 Debug 级别消息)、按分类的开/关开关,以及可插拔的输出后端——默认是 Unity Console,也可以是轮转日志文件,或你自己的崩溃上报接收端。你的游戏代码和每个框架服务不再到处散落 Debug.Log 调用,而是通过 Logger.Info(category, message) 输出诊断信息,格式一致,并且有一个集中控制一切的地方。

快速示例

只调试存档时抑制控制台噪音:

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

你可以在任何地方使用静态 Logger 辅助方法而无需解析任何东西——它们会在启动时替你缓存日志后端。如果你偏好显式依赖(例如为了在测试中注入伪实现),可以从服务定位器解析 ILogger 并在 Awake 中缓存。

完整 API 一览

发射方法(全部为静态、线程安全)

  • Logger.Debug(string category, string message) —— Debug 级别;整个调用点会被编译器从 Release 构建中移除
  • Logger.Debug(string message) —— 使用 Categories.Default 的 Debug
  • Logger.Info(string category, string message) —— Info 级别
  • Logger.Info(string message) —— 使用 Categories.Default 的 Info
  • Logger.Warning(string category, string message) —— Warning 级别
  • Logger.Warning(string message) —— 使用 Categories.Default 的 Warning
  • Logger.Error(string category, string message) —— Error 级别
  • Logger.Error(string message) —— 使用 Categories.Default 的 Error
  • Logger.Critical(string category, string message) —— Critical 级别
  • Logger.Critical(string message) —— 使用 Categories.Default 的 Critical
  • Logger.Exception(string category, Exception ex, string message = null) —— 记录异常及其堆栈跟踪和可选的上下文消息;ex 为 null 时抛出 ArgumentNullException

任何发射方法收到 null 分类时都会回落到 Categories.Default

过滤控制(仅限主线程)

  • Logger.MinimumLevel { get; set; } —— 全局级别下限。Editor 和 Development 构建中默认为 Debug,Release 构建中默认为 Warning。设为 LogLevel.Off 可静音一切——立即生效,静态辅助方法也包括在内。
  • Logger.SetCategoryFilter(string category, LogLevel level) —— 覆盖某个分类的下限;分类为 null 时抛出 ArgumentNullException
  • Logger.ClearCategoryFilter(string category) —— 移除某个分类的覆盖,回到全局下限;分类为 null 时抛出 ArgumentNullException

LogLevel 取值

取值含义
Debug逐帧追踪与冗长细节。会从 Release 构建中完全剔除。
Info状态转换与生命周期事件。始终参与编译。
Warning已恢复的故障、可疑的输入、已弃用的路径。始终参与编译。
Error某个操作失败了,但游戏继续运行。始终参与编译。
Critical进程完整性已受损——下一次失败很可能是致命的。始终参与编译。
Off哨兵值——作为下限设置时会过滤掉所有级别。

标准分类(字符串常量)

Logger.Categories.Default.Bootstrap.ServiceLocator.Save.Audio.UI.Input.AI.Net.Perf(性能:池、时间、定时器)、.Configuration.Scene.Localization.Achievements

分类键区分大小写——始终使用这些常量,绝不要用字符串字面量。

ILogger 接口(实现它以提供自定义后端)

  • void Log(LogLevel level, string category, string message) —— 发射一条消息
  • void LogException(string category, Exception ex, string message) —— 发射异常及其堆栈跟踪
  • LogLevel MinimumLevel { get; set; } —— 实例级的级别下限
  • LogLevel GetEffectiveLevel(string category) —— 查询某个分类实际生效的下限;分类为 null 时抛出 ArgumentNullException

内置后端

  • UnityConsoleLogger —— 默认后端。由启动引导注册;把格式化后的行写入 Unity Console。

  • FileLogger —— 可选启用的轮转文件日志器,用于上线后的支持工作。Unity 自带的 Player.log 是一条不加区分的单一流,而且在 Windows 上会在下次启动时被覆盖——等玩家把它发给你时,证据往往已经没了。FileLogger 在每一行保留分类与级别,并轮转文件,让历史得以保存:

    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) —— 日志写入 {persistentDataPath}/logs/cgs.log,文件超过 maxBytes 时轮转,并保留包含当前文件在内的 maxFiles 个文件。请在主线程上构造它(它会读取一次 Application.persistentDataPath)。
    • FileLogger(string path, long maxBytes, int maxFiles) —— 日志写入显式指定的路径;目录不存在时会自动创建。路径为 null 或空白时抛出 ArgumentException
    • FilePath —— 当前正在写入的文件的绝对路径。
    • CaptureUnityLogStream(bool enabled) —— 把 Unity 自身的日志流镜像到文件中:未捕获的异常及其堆栈跟踪,以及任何从未经过 ILoggerDebug.Log。当 UnityConsoleLogger 同时处于激活状态时不要启用它——那个日志器通过 Debug.Log 写出,因此每条框架日志都会被捕获两次。
    • 线程安全(每次写入都通过锁串行化)并且从不抛出:I/O 失败会被吞掉,连续 8 次写入失败后实例会自我禁用,而不是每次调用都重试一次注定失败的写入。通过 LogException 记录的异常会绕过 MinimumLevel——把唯一能解释崩溃原因的那条记录过滤掉,绝不是该设置的本意。
    • 实现 IDisposable——在关闭时释放它以刷写并退订。
  • NullLogger —— 把每个日志调用都变成静默的无操作:

    // 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());
    

    用它从无头或自动化构建中剥离日志开销、在没有控制台刷屏的情况下测试游戏代码,或在没有日志噪音的情况下做基准测试。日志被静音时,其他所有服务照常工作——框架中没有任何东西依赖日志输出。

编写自己的后端

任何实现 ILogger 的类都可以充当后端。例如,把错误转发到你的崩溃上报 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;
}

ServiceLocator.Replace<ILogger>(new CrashReportLogger()) 注册它。在这次调用之后从服务定位器解析 ILogger 的代码会使用你的后端。一个注意点:静态 Logger 辅助方法在第一次运行时缓存后端,而这发生在框架启动期间——在 Play 会话中较晚做出的替换不会重定向静态辅助方法。它们会在下一次进入 Play、缓存重置时拾取你的后端。

行为与边界情况

  • Release 构建完全丢弃 Debug 调用。 Logger.Debug($"msg = {Expensive()}") 的整个调用点会被编译器从 Release 构建中移除,因此 Expensive() 根本不会执行。免费的优化。

  • 与 Unity 类型的命名冲突。 当文件同时有 using CommonGameSystem.Core;using UnityEngine; 时,LoggerILogger 会与 UnityEngine.LoggerUnityEngine.ILogger 冲突。在文件顶部添加别名:

    using Logger = CommonGameSystem.Core.Logger;
    using ILogger = CommonGameSystem.Core.ILogger;
    
  • 工作线程需要启动时的预热。 Bootstrap 在启动期间于主线程上进行一次 Logger 调用,这使得后续的工作线程调用(存档 I/O、网络 I/O)是安全的。如果某个工作线程在预热之前记录日志,该消息会回落到普通的 Debug.Log

  • 分类键区分大小写。 "Net""net" 是两个不同的过滤器。始终使用 Logger.Categories.* 常量,绝不要用字符串字面量。

  • 过滤 API 仅限主线程。 请在主线程上调用 SetCategoryFilter / ClearCategoryFilter。发射方法(InfoWarningError 等)在任何线程上都是安全的。

  • GetEffectiveLevel 要求非 null 分类。 与发射路径不同(后者把 null 当作 Categories.Default),查询 API 在分类为 null 时抛出 ArgumentNullException。这能尽早暴露配置错误。

  • 会话中途替换后端不会重定向静态辅助方法。 参见上文"编写自己的后端"下的注意点。要立即在运行时静音,请使用 Logger.MinimumLevel = LogLevel.Off

相关页面