C#でSerilogとFody.Anotarを使って、全部盛りのログをとる

# 概要
SerilogとFody.Anotarを使用して、ゴテゴテにした全部盛りのログを取ります。

最終的には

```csharp
LogTo.Information("Hello World!");
```

と書くと、以下のように出力されます。

```console
 | 22:47:59.596 | INFO | 01:MN | 21164:SerilogConsoleTest | Hello World! | SerilogConsoleTest.Program | Void Main(String[]) | 27 L | SerilogConsoleTest | 1.0.0.0 | <マシン名> | <ユーザー名> | 834036 B|
```

中身は記録時刻、ログレベル、スレッドID、スレッド名、プロセスID、プロセス名、メッセージ本文、呼び出し元名前空間＋クラス名、呼び出し元メソッドシグネチャ、行番号、アセンブリ名、アセンブリバージョン、マシン名、ユーザー名、使用メモリ量、(あれば例外)、と**盛るぜぇ～超盛るぜぇ～**になっています。

さらに、メッセージ本文と例外は構造化ログの機能があるため、単純な文字列以上の表現ができます。

# 方法

## コンソールアプリケーション
まず、VisualStuidoのコンソールアプリケーションテンプレートを選んで作成します。
記事では.NET Coreを使用しますが、.NET Frameworkでも問題ありません。

```Program.cs
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");        
    }
}
```

これを変更していきます。

## Serilogの導入

まずは最小限のSerilogを使ったログからです。
nugetで"Serilog"と検索すると、似たロゴのパッケージがズラリと並びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120077/aad2cea0-4740-af84-1b34-955e0db0c0ef.png)

腰が引けるきもしますが、1つ1つのパッケージはそれほど重くありません。
まず、ログの出力先に合わせたパッケージを指定します。

- Serilog.Sinks.File: ファイル 
- Serilog.Sinks.Console: コンソール
- Serilog.Sinks.Debug: デバッグ出力ウインドウ

そして、先ほどのコンソールアプリケーションを変更します。

```Program.cs
class Program
{
    static void Main(string[] args)
    {
        SetupLoggerConfig();
        Log.Information("Hello World!");       
    }
    private static void SetupLoggerConfig()
    {        
        string template = "| {Timestamp:HH:mm:ss.fff} | {Level:u4} | {Message:j} | {NewLine}{Exception}";
        string logFilePathHead = $"logs\\{nameof(SerilogConsoleTest)}";

        Log.Logger = new LoggerConfiguration()                     
                        .MinimumLevel.Verbose()
                        .WriteTo.Console(outputTemplate: template)
                        .WriteTo.Debug(outputTemplate: template)
                        .WriteTo.File($"{logFilePathHead}.txt", LogEventLevel.Information, outputTemplate: template, rollingInterval: RollingInterval.Day)                                            
                        .CreateLogger();
    }
}
```

`SetupLoggerConfig()`でロガーのセットアップを行っています。
ここではC#を用いますが、appsettings.jsonやApp.config(XML)ファイルで設定することもできます。

各項目の解説です。

- `.MinimumLevel.Verbose()`: 記録するロギングレベルを決定します。Verboseは最も低いレベルです。製品にする場合はInfoにすることが多いです。また`LoggingLevelSwitch`を使用して、動的に変更することも可能です。
- `.WriteTo.~()`: ログの出力先を指定します。ここではコンソール・デバッグウインドウ・テキストファイルに出力します。出力先ごとにロギングレベルをさらにしぼることもできます。
- `.WriteTo.~()`内の`outputTemplate`引数: 出力メッセージのフォーマットを指定します。ログの各情報の区切りは`|`（パイプ）にしました。このへんは好みですが、`,`（カンマ）や`:`（コロン）は後で出てくるJSON型式と被るので避けたほうがよいです。
- `outputTemplate`内の`{Level:u4}`: ロギングレベルの4文字表現です。1～11文字まで選べます。
- `outputTemplate`内の`{Message:j}`: ログメッセージ本体です。`j`は構造化データをJSON型式で表示します。
- `.WriteTo.File()`内の`rollingInterval`引数: ファイルを区切るタイミングを指定します。

出力はこのようになります。
まだシンプルですね。

```console
| 21:45:47.367 | INFO | Hello World! |
```

## Serilogパッケージの追加

ここからログ内容にいろいろ**盛るぜぇ～超盛るぜぇ～。**

出力内容を盛るためのパッケージを選択します。

- Serilog.Enrichers.Thread: スレッド
- Serilog.Enrichers.Process: プロセス
- Serilog.Enrichers.Environment: マシン名とユーザー情報
- Serilog.Enrichers.AssemblyName: アセンブリ情報
- Serilog.Enrichers.Memory: メモリ使用量

構造化ログ用のパッケージも選択します

- Serilog.Exceptions: 例外詳細表示
- Serilog.Formatting.Compact: Compact-JSONフォーマット

ロガーのセットアップ部分に、いろいろ追加していきます。

```csharp
private static void SetupLoggerConfig()
{
    string template = "| {Timestamp:HH:mm:ss.fff} | {Level:u4} | {ThreadId:00}:{ThreadName} | {ProcessId:00}:{ProcessName} | {Message:j} | {SourceContext} | {MethodName} | {LineNumber} L | {AssemblyName} | {AssemblyVersion} | {MachineName} | {EnvironmentUserName} | {MemoryUsage} B|{NewLine}{Exception}";

    string logFilePathHead = $"logs\\{nameof(SerilogConsoleTest)}";

    Log.Logger = new LoggerConfiguration()
                    .Enrich.WithThreadId()
                    .Enrich.WithThreadName().Enrich.WithProperty("ThreadName", "__")
                    .Enrich.WithProcessId().Enrich.WithProcessName()
                    .Enrich.WithMachineName()
                    .Enrich.WithEnvironmentUserName()
                    .Enrich.WithAssemblyName()
                    .Enrich.WithAssemblyVersion()
                    .Enrich.WithMemoryUsage()
                    .Enrich.WithExceptionDetails()
                    .MinimumLevel.Verbose()
                    .WriteTo.Console(outputTemplate: template)
                    .WriteTo.Debug(outputTemplate: template)
                    .WriteTo.File($"{logFilePathHead}.txt", LogEventLevel.Information, outputTemplate: template, rollingInterval: RollingInterval.Day)
                    .WriteTo.File(new CompactJsonFormatter(), $"{logFilePathHead}_comapct.json", LogEventLevel.Information, rollingInterval: RollingInterval.Day)
                    .CreateLogger();

    Thread.CurrentThread.Name = "MN";
}
```

各項目の解説です。対応するものが`outputTemplate`にも追加されています。

- `.Enrich.WithThreadId()`: スレッドIDを追加します。
- `.Enrich.WithThreadName().Enrich.WithProperty("ThreadName", "__")`: スレッド名を追加して、未設定なら`__`を変わりに表示します。
- `.Enrich.WithProcessId().Enrich.WithProcessName()`: プロセスIDとプロセス名を追加します
- `.Enrich.WithMachineName()`: マシン名（PCの名称）を追加します。
- `.Enrich.WithEnvironmentUserName()`: ユーザー名を追加します。
- `.Enrich.WithAssemblyName()`: アセンブリ名を追加します。
- `.Enrich.WithAssemblyVersion()`: アセンブリバージョンを追加します。
- `.Enrich.WithMemoryUsage()`: メモリ使用量を追加します。
- `.Enrich.WithExceptionDetails()`: 例外を`ToString()`したよりも詳細な情報を追加します。
- `.WriteTo.File(new CompactJsonFormatter(), ~)`: Compact-JSON型式の構造化ログを保存します。構造化ログについてはあとで説明します。
- `Thread.CurrentThread.Name = "MN"`: 開始スレッド名に"MN"(Main)とつけて、他のスレッドと区別しやすくしています。デスクトップアプリケーションなど、特定のスレッドからしかアクセスできないオブジェクトがあったときに便利です。
- `outputTemplate`内の`{SourceContext}`は名前空間+クラス名、`{MethodName}`はメソッドシグネチャ、`{LineNumber} L`はソースコード行番号ですが、今は表示されません。

ログ書き込み部分は変わらないです。
この段階での出力が以下です。一部抜けがありますが、かなり盛れています。

```console
| 22:12:17.894 | INFO | 01:MN | 2132:SerilogConsoleTest | Hello World! |  |  |  L | SerilogConsoleTest | 1.0.0.0 | <マシン名> | <ユーザー名> | 800236 B|
```

## Fody.Anotarの導入

さらに、メソッド名やクラス名を取得するために、Serilog連携用のFodyパッケージを使用します。
Fodyは中間言語（IL）に対してコンパイル時に加工を行うパッケージです。ログ内容を盛るためだけに使うのは大げさな気もしますが、超盛るためだからショウガナイ。

- Anotar.Serilog.Fody

プロジェクトの直下に
FodyWeavers.xmlファイルを追加して編集します。

```FodyWeavers.xml
<Weavers>
  <Anotar.Serilog />
</Weavers>
```

Log書き込み部分を`Log.~`から`LogTo.~`に変更します。

```csharp
LogTo.Information("Hello World!");
```

ログセットアップ部分は変更無いです。
そして最終結果が以下です。先ほど抜けていたクラス名などがFody.Anotarによって埋められました。

```console
| 22:47:59.596 | INFO | 01:MN | 21164:SerilogConsoleTest | Hello World! | SerilogConsoleTest.Program | Void Main(String[]) | 27 L | SerilogConsoleTest | 1.0.0.0 | <マシン名> | <ユーザー名> | 834036 B|
```

12文字のメッセージが215文字にまで膨らみました。
めでたしめでたし

# 他のログ機能

## ログレベル

ログ出力は全部で6段階あります。重要度が低い順に出力すると以下のようになります。

```csharp
private static void CallAllLogLevel()
{
    LogTo.Verbose("Log Level 1");
    LogTo.Debug("Log Level 2");
    LogTo.Information("Log Level 3");
    LogTo.Warning("Log Level 4");
    LogTo.Error("Log Level 5");
    LogTo.Fatal("Log Level 6");
}
```

これをコンソールで出力すると少し色付けがされています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120077/24729933-77ed-6fd3-c86d-0e31b132c011.png)

出力ウインドウに表示する場合[VSColorOutput](https://marketplace.visualstudio.com/items?itemName=MikeWard-AnnArbor.VSColorOutput)を使用すると見やすくなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120077/13e2fa26-b64a-b431-b554-0a1e242b6b03.png)

## メモリ使用量と例外出力

メモリ使用量のわかり易い例と例外出力です。
以下のように、倍々で配列の長さを増やしていくと、そのうち`OutOfMemoryException`が発生します。

```csharp
private static void CauseOutOfMemoryEx()
{
    var bigArray = new int[0];
    try
    {
        for (int bit = 0; bit < 64; bit++)
        {
            bigArray = Enumerable.Range(0, (int)Math.Pow(2, bit)).ToArray();
            LogTo.Debug("Created Array {Size}", bigArray.Length);
        }

    }
    catch (Exception ex)
    {
        LogTo.Error(ex, "Fail to Create big Array");
    }
}
```

出力結果の一部です。
行の最後の ~Bがメモリ使用量です。
例外は例外種類・`Message`・`StackTrace`が表示されています。

```console
| 00:20:22.613 | DBUG | ~ | Created Array 1 | SerilogConsoleTest.Program | Void CauseOutOfMemoryEx() | 59 L | ~ | 941556 B|
~
| 00:14:07.392 | DBUG | ~ | Created Array 134217728 | SerilogConsoleTest.Program | Void CauseOutOfMemoryEx() | 59 L | ~ | 806009196 B|
| 00:14:07.465 | EROR | ~ | Fail to Create big Array | SerilogConsoleTest.Program | Void CauseOutOfMemoryEx() | 65 L | ~ | 537582008 B|
System.OutOfMemoryException: Exception of type 'System.OutOfMemoryException' was thrown.
   at System.Linq.Enumerable.RangeIterator.ToArray()
   at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)
   at SerilogConsoleTest.Program.CauseOutOfMemoryEx() in D:\Documents\VSProjects\SerilogConsoleTest\SerilogConsoleTest\Program.cs:line 58
```

## メッセージ内へのJSON型式埋め込み

メッセージ内にオブジェクトをJSON型式にシリアライズして埋め込みできます。

こんな変哲もないPersonクラスがあったとして、

```csharp
public class Person
{
    public string Name { get; set; }
    public double Height { get; set; }
}
```

ログ出力します。
ポイントは`@`をつけること、メッセージ文字列の最初に`$`をつけないことです。

```csharp
private static void LogStructual()
{
    var person = new Person { Name = "Tiger", Height = 143.6 };
    LogTo.Information("Show {@tiger}", person);
}
```

出力結果は以下のようになります。
`person`変数の中身がJSON型式で表示されています。

```console
| 00:08:58.942 | INFO | ~ | Show {"Name":"Tiger","Height":143.6,"$type":"Person"} | SerilogConsoleTest.Program | Void LogStructual() | ~
```

## 構造化ログ
この記事ではあまり構造化ログについて触れていませんが、本来Serilogはそのためのライブラリです。
ログのセットアップ部分で以下のようにComact-JSON型式のファイル出力を追加します。

```csharp
.WriteTo.File(new CompactJsonFormatter(), $"{logFilePathHead}_comapct.json", LogEventLevel.Information, rollingInterval: RollingInterval.Day)
```

上記、`person`変数の出力は以下のようになっています。
通常のJSON型式よりは一部が短くなっています。

```json:SerilogConsoleTest_comapct20200501.json（一部）
{
    "@t": "2020-04-30T15:30:49.3180731Z",
    "@mt": "Show {@tiger}",
    "tiger": {
        "Name": "Tiger",
        "Height": 143.6,
        "$type": "Person"
    },
    "LineNumber": 44,
    "MethodName": "Void LogStructual()",
    "SourceContext": "SerilogConsoleTest.Program",
    "ThreadId": 1,
    "ThreadName": "MN",
    "ProcessId": 20272,
    "ProcessName": "SerilogConsoleTest",
    "MachineName": "<マシン名>",
    "EnvironmentUserName": "<ユーザー名>",
    "AssemblyName": "SerilogConsoleTest",
    "AssemblyVersion": "1.0.0.0",
    "MemoryUsage": 912476
}
```

構造化ログの詳細については参考URLを参照ください。

構造化ログを簡単に見たい場合は、[Compact Log Format Viewer](https://github.com/warrenbuckley/Compact-Log-Format-Viewer)が便利です。
作成したログファイル(JSON)を読込するだけで、このように表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120077/101e5747-e0cf-2208-9331-3e60b4b48e3b.png)

## 試していない機能

Serilogは多様なプラットフォームに対応したパッケージです。
試していませんが、ASP.NETでもできますし、最近話題のAzure App SeriviceでもAWSでも使えるようです。
webの場合`RequestId`なども取得できるようです。

# 全体コード
以下に置いておきます。
https://github.com/soi013/SerilogConsoleTest

# 参考

https://github.com/serilog/serilog/wiki
https://github.com/Fody/Anotar

https://www.buildinsider.net/column/kawai-yoshifumi/003
https://www.youtube.com/watch?v=_iryZxv8Rxw

# 環境

VisualStudio2019
.NET Core 3.1
C#8

```xml:SerilogConsoleTest.csproj（一部)
<PackageReference Include="Anotar.Serilog.Fody" Version="5.1.0" />
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.Enrichers.AssemblyName" Version="1.0.9" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.1.3" />
<PackageReference Include="Serilog.Enrichers.Memory" Version="1.0.4" />
<PackageReference Include="Serilog.Enrichers.Process" Version="2.0.1" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Exceptions" Version="5.4.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="1.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.Debug" Version="1.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
```