ログを構造化するとなにがいいのか

#はじめに
[先日書いた新卒研修のアウトプット記事](https://qiita.com/YutoTakakura/items/92dcdab50102bb0b0795)が、思いのほか多くの方に見ていただいております。
こんなにLGTMやストックをいただいたのは初めてなので、大変嬉しいです。

今回は、ログの構造化について学んだことをまとめてみました。
#現在のアプリケーションログの背景
今までログは、ファイルに出力するのが一般的でした。
しかしクラウドやコンテナなどの普及により、そのままファイルに残しておくことは不都合が生じるようになりました。
主に、次の二点の理由からです。

## データ保持の安全性
近年では、CI/CDなどにより本番環境のコンテナの破棄が頻繁に行われるようになりました。
そのため、アプリケーションログなどの永続的に保存するデータはコンテナの外に出す必要が出てきます。


## 検索利便性
また、業務用のアプリケーションでは立ち上がるコンテナの数はかなり多くなるため、それぞれのコンテナにあるログファイルを一つ一つ検索するのもあまり現実的ではありません。

# ログを実行環境でストリームとして管理する
ストリームとは、ログを様々な形で管理する方針のことです。
ログをストリームとして管理してログ基盤に連携すると、上記二つの問題点を払拭することができます。

具体例として、以下のようなことができるようになります。

* コンテナが破棄されても基盤に連携されたログから検索対象を確認できる
* 一つのログを複数のアプリケーションで一貫して確認する
* 特定のエラーが出た際にメールで通知する
...etc

プログラム上でログを出力する際にも、ストリームで連携しやすいようにすることが求められます。
ここで登場するのが、ログの構造化です。

#文字列型のログ
構造化されていないシンプルな、文字を出力するだけのログです。

```C#
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
					
var serviceProvider = new ServiceCollection()
		.AddLogging(configure => configure.AddConsole())
		.BuildServiceProvider();
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("mylogger");
logger.LogError ("Hello {なにか}", "world");
```
結果

```
fail: mylogger[0]
      Hello world
```
このような構造化されていないログの場合、ストリーム連携に必要な情報が欠落してしまっていたり、その場合の後からの構造化がしにくかったりという問題が発生します。

#構造化ログ
そこで、ログを出す段階で文字列ではなく一般的なJSONで構造化することを考えてみます。

```C#
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

var serviceProvider = new ServiceCollection()
//　　　　.AddLogging(configure => configure.AddConsole())
//　　　　JSONとして出力する（構造化）
	.AddLogging(configure => configure.AddJsonConsole())
	.BuildServiceProvider();
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("mylogger");
logger.LogError ("Hello {なにか}", "world");
```
結果

```JSON
{
    "EventId":0,
    "LogLevel":"Error",
    "Category":"mylogger",
    "Message":"Hello world\u3000sample",
    "State":{
        "Message":"Hello world\u3000sample",
        "\u306A\u306B\u304B":"world",
        "\u306A\u306B\u304B\uFF12":"sample",
        "{OriginalFormat}":"Hello {\u306A\u306B\u304B}\u3000{\u306A\u306B\u304B\uFF12}"
    }
}

```
このログですと、最初から機械的に処理しやすくなっているので後からストリーム連携がしずらくて困ることが少なくなります。

#（おまけ）Serilogで構造化ログを出力
C#の構造化ログ出力ライブラリであるSerilogを使うと、以下のように書くことができます。
LogTemplateに@を付けると、プロパティーの構造をそのままログに出してくれるのでわかりやすいですね。

```C#
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;

Log.Logger = new LoggerConfiguration()
      .Enrich.FromLogContext()
      .WriteTo.Console(formatter: new Serilog.Formatting.Json.JsonFormatter())
      .CreateLogger();

var serviceProvider = new ServiceCollection()
	.AddLogging(configure => configure.AddSerilog())
//	.AddLogging(configure => configure.AddJsonConsole())
	.BuildServiceProvider();
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("mylogger");
// LogTemplateに@を付けると、プロパティーの構造をそのままログに出してくれる
logger.LogError ("Hello {@aaa}", new Hoge { Id = 0, Name = "あいうえお"});

public class Hoge 
{
	public int Id {get;set;}
	public string Name {get;set;}
}
```

結果

```JSON
{
    "Timestamp":"2021-08-16T02:56:28.7384641+00:00",
    "Level":"Error",
    "MessageTemplate":"Hello {@aaa}",
    "Properties":{
        "aaa":{
            "_typeTag":"Hoge",
            "Id":0,
            "Name":"あいうえお"
            },
        "SourceContext":"mylogger"
    }
}
```
# 最後に
この記事が少しでも参考になりましたら、幸いです。

# 参照
* https://docs.microsoft.com/ja-jp/dotnet/core/extensions/logging?tabs=command-line
* https://serilog.net/
* https://12factor.net/ja/logs