はじめに
これはUnity Advent Calendar 2023の15日目の記事です。
qiita.com
この記事ではUnity公式のLogging
パッケージであるUnity Logging
を紹介したいと思います。
概要
Unity Logging
はUnity公式が出しているLogging
パッケージです。Log.〇〇
を実行したログをコンソールに出力するだけでなく、テキストファイルとして出力したりJSONとして出力したりすることができます。
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 Logging
はBurst
を利用したりとUnityだけでの利用を想定してゴリゴリに最適化したようなイメージですね。
導入
PackageManager
からインストールします。公式パッケージなので、Unity Registry
からUnity Logging
を検索してインストールできます。
依存先としてBurst
やCollections
があります。
パッケージ名は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");
LogLevel
に対応したLog.〇〇
が存在します。注意点としてConfigure
というLogger
の設定があるのですが、これがデフォルトだとDebug
以上しか出力されません。(つまり自分で設定しない限りLog.Verbose
は出力されない)
Default Configure
自分でConfigure
(Logger
の設定)を設定できるのですが、デフォルトでは以下のように設定されています。
LogLevel.Fatal
以外は非同期にログをキャプチャするMinimumLevel
(キャプチャする最低のLogLevel
)をDebug
に設定OutputTemplate
(どのようにログを出力するかのテンプレート)を以下のように設定Projectフォルダ/logs/Output.json
とProjectフォルダ/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.Log
やUnityEngine.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); }