はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】Debug.Logはもう古い!? Unity公式のLoggingパッケージ「Unity Logging」の使い方まとめ

はじめに

これはUnity Advent Calendar 2023の15日目の記事です。
qiita.com


この記事ではUnity公式のLoggingパッケージであるUnity Loggingを紹介したいと思います。

概要

Unity LoggingはUnity公式が出しているLoggingパッケージです。Log.〇〇を実行したログをコンソールに出力するだけでなく、テキストファイルとして出力したりJSONとして出力したりすることができます。

Unity Loggingの一連の流れのイメージ

The Logging package is a versatile and highly configurable structured asynchronous logging solution. In addition to what you would expect from a traditional logging package (level, timestamp, stacktrace), it contains various ways to stream and record the logs such as StdOut, text or JSON files, DebugLog, and your own custom implementation. You can also individually or collectively configure these logs.

It depends only on Collections package and Burst package; can be used without ECS.

The file-based logs are rolling per default, which prevents your application from taking over the memory space in the case of a long running process such as a server application.

You can also customize the package to create your own serializer. What’s more, you can call the logger from Burst-compatible code.

// DeepL翻訳
ロギングパッケージは、多用途で高度に設定可能な構造化された非同期ロギングソリューションです。伝統的なロギングパッケージ(レベル、タイムスタンプ、スタックトレース)に期待されるものに加えて、 StdOut、テキストファイル、JSON ファイル、DebugLog、独自のカスタム実装など、ログをストリーミングして記録するさまざまな方法が含まれています。また、これらのログを個別に、またはまとめて設定することもできます。

CollectionsパッケージとBurstパッケージのみに依存し、ECSなしでも使用できます。

ファイルベースのログはデフォルトでローリングされ、サーバーアプリケーションのような長時間実行されるプロセスの場合に、アプリケーションがメモリ領域を占有するのを防ぎます。

Logging package | Unity Logging | 1.2.0-exp.3

UnityEngine.Debugと異なる点は多々ありますが、特に構造化ロギングに対応していることが大きな特徴だと思います。

ログ管理のクラウドサービス、例えばDatadog LogsやStackdriver Loggingなどは、柔軟なフィルタリングや検索、式を使ったクエリを行えますが、その機能をフルに活かすためにはログが適切にパースされている必要があります。構造化ロギング(Structured Logging, Semantic Logging)は、ログをJSONで送ることにより、特別な後処理をせずログサービスに読み込ませることができます。

ZLogger – .NET CoreとUnityのためのゼロアロケーション構造化ロガー | Cygames Engineers' Blog

// 出力されたJSONの例
{"Timestamp":"2023/12/11 13:12:33.835","Level":"DEBUG","Message":"{name}'s HP : {hp}","Stacktrace":"Sample:Start() (at Assets/Scripts/Sample.cs:80)\n","Properties":{"name":"John", "hp":10}}
{"Timestamp":"2023/12/11 13:12:33.835","Level":"INFO","Message":"{name}'s HP : {hp}","Stacktrace":"Sample:Start() (at Assets/Scripts/Sample.cs:81)\n","Properties":{"name":"John", "hp":10}}
{"Timestamp":"2023/12/11 13:12:33.836","Level":"WARNING","Message":"{name}'s HP : {hp}","Stacktrace":"Sample:Start() (at Assets/Scripts/Sample.cs:82)\n","Properties":{"name":"John", "hp":10}}
{"Timestamp":"2023/12/11 13:12:33.837","Level":"ERROR","Message":"{name}'s HP : {hp}","Stacktrace":"Sample:Start() (at Assets/Scripts/Sample.cs:83)\n","Properties":{"name":"John", "hp":10}}
{"Timestamp":"2023/12/11 13:12:33.838","Level":"FATAL","Message":"{name}'s HP : {hp}","Stacktrace":"Sample:Start() (at Assets/Scripts/Sample.cs:84)\n","Properties":{"name":"John", "hp":10}}



似たようなライブラリとしてZLoggerが挙げられますが、このUnity LoggingBurstを利用したりとUnityだけでの利用を想定してゴリゴリに最適化したようなイメージですね。

github.com

導入

PackageManagerからインストールします。公式パッケージなので、Unity RegistryからUnity Loggingを検索してインストールできます。

Unity Loggingをインストール

依存先としてBurstCollectionsがあります。

パッケージ名はcom.unity.loggingですね。

環境

Unity Logging v1.0.16
Burst v1.8.9
Collections v2.1.4

基本的な使い方

LogLevel

Unity Loggingには複数のLogLevelが設定されています。Verbose < Debug < Info < Warning < Error < Fatalの順番を覚えておいてください。

例えば〇〇以上のログは出力するといった使われ方がするので、順番が重要です。

namespace Unity.Logging
{
  public enum LogLevel : byte
  {
    Verbose,
    Debug,
    Info,
    Warning,
    Error,
    Fatal,
  }
}

ログ出力

まずはComposite formattingを利用しない、シンプルなログ出力の仕方を紹介します。

// 例. "LogLevel.Verbose : 0"というログ
Log.Verbose("LogLevel.Verbose : 0");

// 例. "LogLevel.Debug : 1"というログ
Log.Debug("LogLevel.Debug : 1");

// 例. "LogLevel.Info : 2"というログ
Log.Info("LogLevel.Info : 2");

// 例. "LogLevel.Warning : 3"というログ
Log.Warning("LogLevel.Warning : 3");

// 例. "LogLevel.Error : 4"というログ
Log.Error("LogLevel.Error : 4");

// 例. "LogLevel.Fatal : 5"というログ
Log.Fatal("LogLevel.Fatal : 5");
Consoleに出力されている様子

LogLevelに対応したLog.〇〇が存在します。注意点としてConfigureというLoggerの設定があるのですが、これがデフォルトだとDebug以上しか出力されません。(つまり自分で設定しない限りLog.Verboseは出力されない)

Default Configure

自分でConfigure(Loggerの設定)を設定できるのですが、デフォルトでは以下のように設定されています。

  • LogLevel.Fatal以外は非同期にログをキャプチャする
  • MinimumLevel(キャプチャする最低のLogLevel)をDebugに設定
  • OutputTemplate(どのようにログを出力するかのテンプレート)を以下のように設定
  • Projectフォルダ/logs/Output.jsonProjectフォルダ/logs/Output.logにログの情報を記載
  • Unityのコンソールにログを出力
// デフォルトのOutputTemplate
[{Timestamp}] {Level} | {Message}{NewLine}{Stacktrace}

Default configuration | Unity Logging | 1.2.0-pre.4

それぞれの用語・詳細はこの後紹介します。

Custom Configure

前述の通り、Configureを自身で設定することができます。設定するにはLog.Loggerに対して以下のように代入を行います。

// Custom Configureを設定している例
Log.Logger = new Logger(new LoggerConfig()
    .MinimumLevel.Debug()                                                       // Debug以上のログをキャプチャ
    .OutputTemplate("[{Timestamp}] {Level} | {Message}{NewLine}{Stacktrace}")   // ログ出力のテンプレート 
    .WriteTo.File("Logs/Output.log")                                            // テキストファイル(.log)を出力
    .WriteTo.JsonFile("Logs/Output.json")                                       // 構造化ログ(.json)を出力
    .WriteTo.UnityEditorConsole());                                             // コンソールに出力

Custom configuration using LoggerConfig | Unity Logging | 1.2.0-exp.3

MinimumLevel

Loggerのキャプチャするログの最小レベルを設定します。

// Verbose以上のログをキャプチャ
MinimumLevel.Verbose()

// Debug以上のログをキャプチャ
MinimumLevel.Debug()

// Info以上のログをキャプチャ
MinimumLevel.Info()

// Warning以上のログをキャプチャ
MinimumLevel.Warning()

// Error以上のログをキャプチャ
MinimumLevel.Error()

// Fatalのみのログをキャプチャ
MinimumLevel.Fatal()

また出力先でMinimumLevelを変更することもできます。

// 出力先でMinimumLevelを変更できる
Log.Logger = new Logger(new LoggerConfig()
    .MinimumLevel.Debug() 
    .OutputTemplate("[{Timestamp}] {Level} | {Message}{NewLine}{Stacktrace}")
    .WriteTo.File("Logs/Output.log", minLevel: LogLevel.Verbose)
    .WriteTo.JsonFile("Logs/Output.json", minLevel: LogLevel.Warning)
    .WriteTo.UnityEditorConsole());

OutputTemplate

キャプチャしたログを.logやコンソールにどのように表示するのかを設定します。

使えるキーワードは以下の通り。

キーワード 意味
Timestamp UTC時刻
Level ログレベル
Message キャプチャしたい実際の値
Stacktrace Messageをキャプチャしたスタックトレース
NewLine 改行(Environment.NewLine)を挿入する
Properties Messageに埋め込む変数のデータ
// 例
OutputTemplate("{Timestamp} - {Level} - {Message}")
// .log, Consoleでの表示
2023/12/11 15:00:05.651 - VERBOSE - Hello Verbose
2023/12/11 15:00:05.652 - DEBUG - Hello Debug
2023/12/11 15:00:05.653 - INFO - Hello Info
2023/12/11 15:00:05.654 - WARNING - Hello Warning
2023/12/11 15:00:05.654 - ERROR - Hello Error
2023/12/11 15:00:05.655 - FATAL - Hello Fatal. That was FATAL

こちらも出力先によって設定できます。

// 出力先によってOutputTemplateを変更する
Log.Logger = new Logger(new LoggerConfig()
    .MinimumLevel.Debug() 
    .OutputTemplate("{Timestamp} - {Level} - {Message}") 
    .WriteTo.File("Logs/Output.log", outputTemplate: "[{Timestamp}] {Level} | {Message}{NewLine}{Stacktrace}")
    .WriteTo.JsonFile("Logs/Output.json") 
    .WriteTo.UnityEditorConsole()); 

CaptureStacktrace

スタックトレースをキャプチャするかどうかを設定します。デフォルトはtrueです。

// CaptureStacktraceをfalseに設定
Log.Logger = new Logger(new LoggerConfig()
    .MinimumLevel.Debug()
    .OutputTemplate("{Timestamp} - {Level} - {Message}") 
    .WriteTo.File("Logs/Output.log")
    .WriteTo.JsonFile("Logs/Output.json")
    .WriteTo.UnityEditorConsole()
    .CaptureStacktrace(false)
);

RedirectUnityLogs

UnityEngine.Debug.LogUnityEngine.Debug.LogWarning, UnityEngine.Debug.LogErrorが実行されたときにその情報をUnity Logging側でも受け取るように設定できます。

// RedirectUnityLogsを設定
Log.Logger = new Logger(new LoggerConfig()
    .MinimumLevel.Debug()
    .OutputTemplate("{Timestamp} - {Level} - {Message}") 
    .WriteTo.File("Logs/Output.log")
    .WriteTo.JsonFile("Logs/Output.json")
    .WriteTo.UnityEditorConsole()
    .RedirectUnityLogs()
);
// 以下のメソッドを読んだ時にLoggerでも受け取る
Debug.Log("Debug.Log");
Debug.LogWarning("Debug.Log");
Debug.LogError("Debug.LogError");

SyncMode

ログをキャプチャする際に同期・非同期・それらの混合から選択することができます。

  • 同期 : FullSync
  • 非同期 : FullAsync
  • 混合(Fatalのみ同期でそれ以外が非同期) : FatalIsSynch

デフォルトではFatalIsSynchです。

// SyncModeをFatalIsSyncに設定
Log.Logger = new Logger(new LoggerConfig()
    .MinimumLevel.Debug()
    .OutputTemplate("{Timestamp} - {Level} - {Message}") 
    .WriteTo.File("Logs/Output.log")
    .WriteTo.JsonFile("Logs/Output.json")
    .WriteTo.UnityEditorConsole()
    .SyncMode.FatalIsSync()
); 

ログの出力先(Sinks)の設定

ファイルシステム

テキストファイル(.log)への出力とJson形式のテキストファイルへの出力ができます。

// テキストファイル(.log)への出力
Log.Logger = new Logger(new LoggerConfig()
    .WriteTo.File("Logs/Output.log")
);

// Json形式のテキストファイル(.json)への出力
Log.Logger = new Logger(new LoggerConfig()
    .WriteTo.JsonFile("Logs/Output.json")
);

ただし以下のプラットフォームだと対応していないようです。

  • Switch
  • WebGL

それぞれの引数の詳細は内部実装をみてみると良いと思います。

/// <summary>
/// Write logs to the file in a text form
/// </summary>
/// <param name="writeTo">Logger config</param>
/// <param name="absFileName">Absolute file path to the log file</param>
/// <param name="formatter">Formatter that should be used by this sink. Text is default</param>
/// <param name="maxFileSizeBytes">Threshold of file size in bytes after which new file should be created (rolling). Default of 5 MB. Set to 0 MB if no rolling by file size is needed</param>
/// <param name="maxRoll">Max amount of rolls after which old files will be rewritten</param>
/// <param name="maxTimeSpan">Threshold of time after which new file should be created (rolling). 'default' if no rolling by time is needed</param>
/// <param name="captureStackTrace">True if stack traces should be captured</param>
/// <param name="minLevel">Minimal level of logs for this particular sink. Null if common level should be used</param>
/// <param name="outputTemplate">Output message template for this particular sink. Null if common template should be used</param>
/// <returns>Logger config</returns>
public static LoggerConfig File(this LoggerWriterConfig writeTo, string absFileName,
    FormatterStruct formatter = default,
    long maxFileSizeBytes = 5 * 1024 * 1024, int maxRoll = 15, TimeSpan maxTimeSpan = default,
    bool? captureStackTrace = null,
    LogLevel? minLevel = null,
    FixedString512Bytes? outputTemplate = null)

/// <summary>
/// Write structured logs to the json file
/// </summary>
/// <param name="writeTo">Logger config</param>
/// <param name="absFileName">Absolute file path to the log file</param>
/// <param name="maxFileSizeBytes">Threshold of file size in bytes after which new file should be created (rolling). Default of 5 MB. Set to 0 MB if no rolling by file size is needed</param>
/// <param name="maxRoll">Max amount of rolls after which old files will be rewritten</param>
/// <param name="maxTimeSpan">Threshold of time after which new file should be created (rolling). 'default' if no rolling by time is needed</param>
/// <param name="captureStackTrace">True if stack traces should be captured</param>
/// <param name="minLevel">Minimal level of logs for this particular sink. Null if common level should be used</param>
/// <param name="outputTemplate">Output message template for this particular sink. Null if common template should be used</param>
/// <returns>Logger config</returns>
public static LoggerConfig JsonFile(this LoggerWriterConfig writeTo,
    string absFileName,
    long maxFileSizeBytes = 5 * 1024 * 1024, int maxRoll = 15, TimeSpan maxTimeSpan = default,
    bool? captureStackTrace = null,
    LogLevel? minLevel = null,
    FixedString512Bytes? outputTemplate = null)
コンソール

UnityEditorのConsoleで出力するかOSの標準出力をすることができます。

// UnityEditorのConsoleに出力
Log.Logger = new Logger(new LoggerConfig()
    .WriteTo.UnityEditorConsole()
);

// OSの標準出力
Log.Logger = new Logger(new LoggerConfig()
    .WriteTo.StdOut()
);

こちらも詳細は内部実装をみてみましょう。

/// <summary>
/// Write logs to the UnityEditor's Console window. Does nothing in a standalone build or in Unity prior to 2022.2
/// </summary>
/// <param name="writeTo">Logger config</param>
/// <param name="captureStackTrace">True if stack traces should be captured</param>
/// <param name="minLevel">Minimal level of logs for this particular sink. Null if common level should be used</param>
/// <param name="outputTemplate">Output message template for this particular sink. Null if common template should be used</param>
/// <returns>Logger config</returns>
public static LoggerConfig UnityEditorConsole(this LoggerWriterConfig writeTo,
    bool? captureStackTrace = null,
    LogLevel? minLevel = null,
    FixedString512Bytes? outputTemplate = null)

/// <summary>
/// Write logs to the standard output
/// </summary>
/// <param name="writeTo">Logger config</param>
/// <param name="formatter">Formatter that should be used by this sink. Text is default</param>
/// <param name="captureStackTrace">True if stack traces should be captured</param>
/// <param name="minLevel">Minimal level of logs for this particular sink. Null if common level should be used</param>
/// <param name="outputTemplate">Output message template for this particular sink. Null if common template should be used</param>
/// <returns>Logger config</returns>
public static LoggerConfig StdOut(this LoggerWriterConfig writeTo,
    FormatterStruct formatter = default,
    bool? captureStackTrace = null,
    LogLevel? minLevel = null,
    FixedString512Bytes? outputTemplate = null)
それ以外

UnityEngine.Debug.Logにメッセージをルーティングしたり、メモリ内の文字列にルーティングしたりすることができます。

// UnityEngine.Debug.Logにメッセージを出力
Log.Logger = new Logger(new LoggerConfig()
    .WriteTo.UnityDebugLog()
);

// メモリ内の文字列に出力
Log.Logger = new Logger(new LoggerConfig()
    .WriteTo.StringLogger()
);

サンプルコード

細々としたものを紹介してきましたが、最後にこれらを組み合わせて使ってみたサンプルコードを載せておきます。

private void Start()
{
    // Loggerの設定をする
    Log.Logger = new Logger(new LoggerConfig()
        .MinimumLevel.Verbose()
        .OutputTemplate("{Timestamp} - {Level} - {Message}")
        .WriteTo.File("Logs/Output.log")
        .WriteTo.JsonFile("Logs/Output.json")
        .WriteTo.UnityEditorConsole()
    );

    // Decorateを用いることでPropertiesに追加することができる("Source":"Awake")
    using (var scope = Log.Decorate("Source", "Awake"))
    {
        // 2023/12/13 14:36:43.237 - VERBOSE - Hello Verbose 42
        // {"Timestamp":"2023/12/13 14:36:43.237","Level":"VERBOSE","Message":"Hello Verbose {0}","Stacktrace":"Sample:Start() (at Assets/Scripts/Sample.cs:24)\n","Properties":{"arg0":42, "Source":"Awake"}}
        Log.Verbose("Hello Verbose {0}", 42);

        // 2023/12/13 14:36:43.238 - DEBUG - Hello Debug
        // {"Timestamp":"2023/12/13 14:32:55.612","Level":"DEBUG","Message":"Hello Debug","Stacktrace":"Sample:Start() (at Assets/Scripts/Sample.cs:27)\n","Properties":{"Source":"Awake"}}
        Log.Debug("Hello Debug");

        // 2023/12/13 14:36:43.239 - INFO - Hello Info
        // {"Timestamp":"2023/12/13 14:32:55.613","Level":"INFO","Message":"Hello Info","Stacktrace":"Sample:Start() (at Assets/Scripts/Sample.cs:30)\n","Properties":{"Source":"Awake"}}
        Log.Info("Hello Info");

        // 2023/12/13 14:36:43.240 - WARNING - Hello Warning
        // {"Timestamp":"2023/12/13 14:32:55.614","Level":"WARNING","Message":"Hello Warning","Stacktrace":"Sample:Start() (at Assets/Scripts/Sample.cs:33)\n","Properties":{"Source":"Awake"}}
        Log.Warning("Hello Warning");

        // 2023/12/13 14:36:43.240 - ERROR - Hello Error
        // {"Timestamp":"2023/12/13 14:32:55.615","Level":"ERROR","Message":"Hello Error","Stacktrace":"Sample:Start() (at Assets/Scripts/Sample.cs:36)\n","Properties":{"Source":"Awake"}}
        Log.Error("Hello Error");

        // 2023/12/13 14:36:43.241 - FATAL - Hello Fatal. That was FATAL
        // {"Timestamp":"2023/12/13 14:32:55.616","Level":"FATAL","Message":"Hello Fatal. That was {Level}","Stacktrace":"Sample:Start() (at Assets/Scripts/Sample.cs:39)\n","Properties":{"Source":"Awake"}}
        Log.Fatal("Hello Fatal. That was {Level}");
    }

    // 変数の埋め込み
    // 「$""」ではないことに注意, 「{}」と「arg〇」はそれぞれ順番によって対応が決まる
    // 2023/12/13 14:36:43.254 - INFO - LogLevel.Info: Hanachiru's HP is 10
    // {"Timestamp":"2023/12/13 14:32:55.629","Level":"INFO","Message":"LogLevel.Info: {name}'s HP is {hp}","Stacktrace":"Sample:Start() (at Assets/Scripts/Sample.cs:47)\n","Properties":{"name":"Hanachiru", "hp":10}}
    string name = "Hanachiru";
    int hp = 10;
    Log.Info("LogLevel.Info: {name}'s HP is {hp}", name, hp);

    // 変数を埋め込む際に順番を指定することができる arg⚪︎ の番号が対応している
    // 2023/12/13 14:36:43.255 - INFO - HP : 20, MP : 10, ATK : 30
    // {"Timestamp":"2023/12/13 14:32:55.631","Level":"INFO","Message":"HP : {1}, MP : {0}, ATK : {2}","Stacktrace":"Sample:Start() (at Assets/Scripts/Sample.cs:50)\n","Properties":{"arg1":20, "arg0":10, "arg2":30}}
    Log.Info("HP : {1}, MP : {0}, ATK : {2}", 10, 20, 30);
}