sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

C# NLog の基本的な使い方3(カスタマイズ)

この記事は、NLog(ログを管理する機能)の使い方(基本)について調べたことを記事にしています。

2022 年 12 月現在、NLog バージョン v5.1.0 について、以下は記述しています。

記事一覧

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で例示したプログラムの実行コードです)

参考