- target の layout renderer を使いこなす
- target の layout renderer を実装する
- target を実装する
- どうなってるのか気になったらソースコードを読む
- サンプル
- 参考
この記事は、NLog(ログを管理する機能)の使い方(基本)について調べたことを記事にしています。
2022 年 12 月現在、NLog バージョン
v5.1.0
について、以下は記述しています。
記事一覧
- NLog とは?
- Getting Start
- カスタマイズ(この記事)
target の layout renderer を使いこなす
NLog の基本的な使い方は、Getting Start の内容で思った以上に把握できたと思います。従来の基本的なログを取るだけなら、デフォルトの機能だけで満足できてしまいます。
なので、カスタマイズの第一歩として挙がることは、target の layout renderer のカスタマイズです。
日付のカスタマイズ
出力するログのテキストフォーマットですね。様々な情報を出力に追加することができるので、この機能を正しく把握しておくとよいと私は思います。手始めにしやすいのは Date and time
を自分の必要する粒度で記録すること。
Getting Start のデフォルトでは {$longdate}
が利用されており、そのフォーマットは yyyy-MM-dd HH:mm:ss.ffff
で必要条件は満たすと思うのですが、ちょっと冗長になりがちです。
- ログの細かい秒数だけ必要なら
{$time}
HH:mm:ss.mmm
- 日付だけでいいなら
{$shortdate}
yyyy-MM-dd
- 指定書式の日付なら
{$date:format=}
${date:format=yyyy-MM-dd HH\:mm\:ss}
レベルのカスタマイズ
日付の ${date}
の例のように、layout renderer の中にはオプション パラメーター(format 引数)のようなものを持つものがあります。
たとえば、${level}
のパラメーターはわかりやすいです。
${level:uppercase=true}
これは、logger からログを書いたときに使ったレベル(Info, Warn, Error など)を表すテキストを出力します。
デフォルトだと Info
のような形になるけど uppercase=true
を指定してやると INFO
のように大文字になります。このほかにも uppercase=true:truncate=3
のように指定することで INF
のように3文字に整形できます。(DB に記録する際の文字数上限などのため)
このパラメーターの仕組みがどうなっているのかは、後述の layout renderer や target のカスタマイズを見ると補強できます。
例外のカスタマイズ
例外のログを残すには howto が用意されています。これは nlog を利用するうえで、参考にする価値が高いと思います。
簡単に言っても、nlog の中でも例外 exception は特別な扱いをすることをおススメしています。
具体的には、次の点が挙げられる:
- logger の引数に exception を渡すこと
- 記録するログのレベルに注意すること
- 例外は try-catch 構文の catch の中でログの記録をすること
コードで示す例は、次のとおり:
static void Test4() { var logger = LogManager.GetCurrentClassLogger(); try { throw new Exception("Test exception"); } catch (Exception ex) { logger.Error(ex, "Got exception."); // NLog 4.0 and newer } }
この次に、例外の詳細をログに記述する方法として、やはり target の layout renderer をカスタマイズする。基本的には ${exception}
を使用する。このパラメーターのオプション パラメーターの指定について理解しておく必要がある。
Getting Start で利用されていたオプションは {message:withexception=true}
です。これは、${exception}
を利用せずに例外を記録する簡易的な手法で、上のコードを実行すると以下の出力になります。
Test exception at Nlog.Practice1.Program.Test4() in C:\Users\User**\source\repos\Samples\NLog.Practice1\NLog.Practice1\Program.cs:line 83
オプションを {message:withexception=false}
にするとこうなる。
Test exception
さらに {message:withexception=false:exceptionSeparator=@}
のように exceptionSeparator
で例外の詳細とメッセージを分ける文字を指定することもできるけど、${exception}
を使えばいいと思います。
Got exception.@System.Exception: Test exception at Nlog.Practice1.Program.Test4() in C:\Users\User**\source\repos\Samples\NLog.Practice1\NLog.Practice1\Program.cs:line 83 (2023-01-05 16:56:55)
withexception
は便利そうに見えますが、 throw new Exception("Test exception") をしたときに記入したテキストがどこにも出力されていません。とりあえず、例外を列挙したいときにおススメなのは ${exception:format=@}
です。json 形式で不足なく例外を記録できます。
{ "Type":"System.Exception", "TargetSite":"Void Test4()", "Message":"Test exception", "Data":{}, "Source":"NLog.Practice1", "HResult":-2146233088, "StackTrace":" at Nlog.Practice1.Program.Test4() in C:\\Users\\User**\\source\\repos\\Samples\\NLog.Practice1\\NLog.Practice1\\Program.cs:line 83" }
${exception}
は例外が発生していないときは、何もログを残さないです。個人的には、内部的な動作で記録を切り替えずに、次のように logger を分けるテクニックもあると思います。
<rules> <logger name="*" minlevel="Trace" maxlevel="Info" writeTo="defaultTarget" /> <logger name="*" minlevel="Error" maxlevel="Fatal" writeTo="errorTarget" /> </rules>
これだと Fatal
も例外を含むように利用することになりますが、target の記述をひとつの target にまとめすぎる問題を軽減できます。また、重要なログを分離するといった方向にも使えます。
target の layout renderer を実装する
layout renderer は target の layout 変数に設定する ${level}
や ${message}
みたいな予約語のやつです。(この記事の最初に書いた内容ですね)
たとえば、${sample}
というのを追加してみます。
<target xsi:type="Console" name="logconsole2" layout="${sample:abc:option1=aaa}" />
[LayoutRenderer("sample")] public class SampleLayoutRenderer : LayoutRenderer { [RequiredParameter] public string? Option1 { get; set; } = null; public string? Option2 { get; set; } = null; [DefaultParameter] public string Default { get; set; } = ""; protected override void Append(StringBuilder builder, LogEventInfo logEvent) { builder.Append($"sample={Default} and Op1={Option1 ?? "null"} OP2={Option2 ?? "null"} "); } }
面倒ですが、登録の作業が必要です。コンソールなら、最初に登録をします。(登録の詳細はこの記事を「参考」に)
static void Main(string[] args) { ConfigurationItemFactory.Default.LayoutRenderers.RegisterDefinition("sample", typeof(SampleLayoutRenderer)); ... }
logger の実行があったときに、(上のサンプルだと、コンソールに)出力されるのは以下のとおり。
実行結果:
sample=abc and Op1=aaa OP2=null
これだけだと、nlog.config に記述したことをそのまま abc だったり aaa と拾ってきただけなんで、あんまり嬉しくない。実際は拾ってきたオプションの値を利用して、logEvent の中身をいい感じに加工したデータを builder に Append する、といったことになります。
補足として DefaultParameter 属性は
sample:abc
のようにsample:default=abc
としなくてもよいようにオプションを未指定の場合に、デフォルトで設定されるパラメーターを指定できます。RequiredParameter 属性をつけておくと、${sample:abc:option1=xxx}
のように必ず option1 に値を設定しないと、実行時エラーになるようにできます。
target を実装する
今度は、target を作るサンプルです。(TargetWithLayout の継承なので、Layout を含みます)
<target xsi:type="sample2" name="logconsole3" data="test data" layout="${sample:abc:option1=aaa}${message} ${shortdate}" />
まず、data というプロパティを追加実装しています。よくある例だと Host
で ip や url を指定したりする、output
のような名前で txt や ini 等を指定したりする、といったことが考えられます。
実装は layout renderer とよく似ています。以下は Console に出力する例です。
[Target("sample2")] public class SampleTargetWithLayout : TargetWithLayout { [RequiredParameter] [DefaultParameter] public string Data { get; set; } = ""; protected override void Write(LogEventInfo logEvent) { string logMessage = this.Layout.Render(logEvent); Console.WriteLine(logMessage); } }
target の場合も layout renderer と同じでプログラムの最初に登録する必要があります。
static void Main(string[] args) { ConfigurationItemFactory.Default.LayoutRenderers.RegisterDefinition("sample", typeof(SampleLayoutRenderer)); ConfigurationItemFactory.Default.Targets.RegisterDefinition("sample2", typeof(SampleTargetWithLayout)); ... }
実行結果:
sample=abc and Op1=aaa OP2=null Got exception. 2023-01-07
layoutrenderer のカスタマイズと target のカスタマイズのやり方を理解できれば、ほとんどのケースで nlog は 80 点以上の使い方ができるように(私は)思いました。
基本的な機能はデフォルトでそろっているけど、微妙に記録する内容が好みと違うときは layoutrenderer の実装を検討して、独自形式のファイルを保存したくなったときなんかに target を実装する、くらいでどうでしょうか。
どうなってるのか気になったらソースコードを読む
NLog は github にソースコードが公開されていて、テストコード付きなのもあってコードを読み解きやすいです。
Logger は ILogger インターフェースを継承していて次みたいな定義がされてる。
void Info([Localizable(false)] string message);
で、Info メソッド系は基本的に WriteToTargets メソッドにすすんで、レベルに合う target のコレクションを取得して、書き込むテキストは LogEventInfo クラスのインスタンスにして各ターゲットにわたる。
ここで LogEventInfo クラスの作られ方がわかるので、LayoutRenderer の拡張を実装(カスタマイズ)すると、この LogEventInfo を突然渡されて「何者だこいつ」ってならずに済む。
サンプル
github にサンプルコードを公開しています。(基本的な使い方3で例示したプログラムの実行コードです)