C# 汎用ホストのDIにカスタムロガーを組み込む

## このドキュメントの内容

任意のロガーを汎用ホストのDIに組み込む例です。

## 要件

今回は次の要件を満たすものを考えます。

* 汎用ホストの既定のログプロバイダに組み込む。
* 構成ファイルで独自のオプション値を設定できること。

## 実装方針

* ILogger インターフェースを実装したロガークラスと ILoggerProvider インターフェースを実装したプロバイダークラスを実装します。
* プロバイダークラスを汎用ホストが管理するログプロバイダーに登録します。
* 独自のプロパティを定義したオプションクラスを実装し、構成ファイルから読み込むようにします。

## 実装したコード

### SampleLogger クラス

ログをコンソールに表示するだけのロガー。

```c#:SampleLogger.cs
/// <summary>
/// サンプルロガー。
/// </summary>
internal class SampleLogger : ILogger
{
    /// <summary>
    /// インスタンスを生成します。
    /// </summary>
    /// <param name="categoryName">カテゴリー</param>
    /// <param name="options">オプション</param>
    public SampleLogger(string categoryName, IOptionsMonitor<SampleLoggerOptions> options)
    {
        m_CategoryName = categoryName;
        m_Options = options;
    }

    private readonly string m_CategoryName;
    private readonly IOptionsMonitor<SampleLoggerOptions> m_Options;

    /// <summary>
    /// スコープを開始します。
    /// </summary>
    /// <typeparam name="TState">状態の型</typeparam>
    /// <param name="state">状態</param>
    /// <returns>スコープ</returns>
    public IDisposable? BeginScope<TState>(TState state) where TState : notnull
    {
        return new Scope<TState>(state);
    }

    /// <summary>
    /// 指定されたログレベルに対するログ出力が有効かどうかを取得します。
    /// </summary>
    /// <param name="logLevel">ログレベル</param>
    /// <returns></returns>
    public bool IsEnabled(LogLevel logLevel)
    {
        return true;
    }

    /// <summary>
    /// ログを出力します。
    /// </summary>
    /// <typeparam name="TState">状態の型</typeparam>
    /// <param name="logLevel">ログレベル</param>
    /// <param name="eventId">イベントID</param>
    /// <param name="state">状態</param>
    /// <param name="exception">例外</param>
    /// <param name="formatter">ログメッセージをフォーマットするメソッド</param>
    public void Log<TState>(
        LogLevel logLevel,
        EventId eventId,
        TState state,
        Exception? exception,
        Func<TState, Exception?, string> formatter
    )
    {
        try
        {
            Console.ForegroundColor = m_Options.CurrentValue.Color.GetValueOrDefault();
            Console.WriteLine($"[{m_CategoryName}] {formatter(state, exception)}");
        }
        finally
        {
            Console.ForegroundColor = ConsoleColor.White;
        }
    }

    /// <summary>
    /// スコープ
    /// </summary>
    /// <typeparam name="TState"></typeparam>
    private class Scope<TState> : IDisposable
    {
        internal Scope(TState state)
        {
            State = state;
        }

        public void Dispose()
        {
            GC.SuppressFinalize(this);
        }

        public TState State { get; }
    }
}
```

### SampleLoggerProvider クラス

SampleLogger を生成するプロバイダー。

```c#
/// <summary>
/// ロガープロバイダーのサンプル。<see cref="SampleLogger"/> を生成します。
/// </summary>
[ProviderAlias("SampleLogger")]
public class SampleLoggerProvider : ILoggerProvider
{
    /// <summary>
    /// インスタンスを生成します。
    /// </summary>
    /// <param name="options">オプション</param>
    public SampleLoggerProvider(IOptionsMonitor<SampleLoggerOptions> options)
    {
        m_Options = options;
    }

    private readonly IOptionsMonitor<SampleLoggerOptions> m_Options;

    /// <summary>
    /// ロガーを生成します。
    /// </summary>
    /// <param name="categoryName">カテゴリー</param>
    /// <returns></returns>
    public ILogger CreateLogger(string categoryName)
    {
        return new SampleLogger(categoryName, m_Options);
    }

    public void Dispose()
    {
        GC.SuppressFinalize(this);
    }
}
```

### SampleLoggerOptions クラス

サンプルロガーのオプション。

```c#:SampleLoggerOptions.cs
/// <summary>
/// サンプルロガーのオプション。
/// </summary>
public class SampleLoggerOptions
{
    public ConsoleColor? Color { get; set; }
}
```

### appsettings.json

汎用ホストの既定の Logger セクション内にサンプルロガーのオプションを記述します。

* "SampleLogger" は SampleLoggerProvider クラスに付与した ProviderAlias 属性で指定した名前に対応します。
* "Color" は SampleLoggerOptions クラスの Color プロパティにバインドされます。
 
```json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.Hosting.Lifetime": "Information"
    },
    "SampleLogger": {
      "LogLevel": {
        "Default": "Information"
      },
      "Color": "Red"
    }
  }
}
```




### アプリケーションコード

「ワーカーサービス」プロジェクトテンプレートを使用しました。SampleLoggerProvider をDIに登録しています。

* ポイントは LoggerProviderOptions.RegisterProviderOptions メソッドです。このメソッドでプロバイダとオプションを関連付けておくことにより、Logger::SampleLogger セクションに定義したオプションが SampleLoggerOptions に読み込まれるようになります。

```c#:Program.cs
public class Program
{
    public static void Main(string[] args)
    {
        var builder = Host.CreateDefaultBuilder(args);

        builder.ConfigureMyProject();

        builder.ConfigureServices(services =>        {
            services.AddHostedService<Worker>();
        });

        builder.ConfigureLogging((context, builder) =>
        {
            // プロバイダーを登録します
            var provider = ServiceDescriptor.Singleton<ILoggerProvider, SampleLoggerProvider>();
            builder.Services.Add(provider);

            // プロバイダーとオプションを関連付けます
            LoggerProviderOptions.RegisterProviderOptions<SampleLoggerOptions, SampleLoggerProvider>(builder.Services);

            // オプションの値を実行時に構成する必要がある場合、構成するメソッドを登録します
            // builder.Services.AddOptions<SampleLoggerOptions>().Configure(options =>
            // {
            // });
        });

        var host = builder.Build();

        host.Run();
    }
}
```

### 生成されるロガー

DI によって生成されたロガーの中身です。この例では汎用ホストであらかじめ組み込まれた4つのロガー（ConsoleLogger, DebugLogger, EventSourceLogger, EventLogLogger）に SampleLogger を加えた5つのロガーが内包されていることがわかります。
![logger.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280969/500dcef4-49e8-487d-6c5b-2c6fa8ac014f.png)


### 拡張メソッドを実装する

前述のDI関連の処理を ILoggingBuilder に対する拡張メソッドとして実装しておくと、アプリケーションコードの実装量を減らすことができます。

```c#
/// <summary>
/// <see cref="ILoggingBuilder"/> に対する拡張メソッド。
/// </summary>
public static class LoggingBuilderExtensions
{
    /// <summary>
    /// <see cref="SampleLogger"/> を追加します。
    /// </summary>
    /// <param name="loggingBuilder"></param>
    /// <param name="configureOptions">オプションを構成するメソッド</param>
    /// <returns><paramref name="loggingBuilder"/> に指定されたインスタンス自身</returns>
    public static ILoggingBuilder AddSampleLogger(
        this ILoggingBuilder loggingBuilder,
        Action<SampleLoggerOptions>? configureOptions = null
    )
    {
        var provider = ServiceDescriptor.Singleton<ILoggerProvider, SampleLoggerProvider>();

        loggingBuilder.Services.Add(provider);

        LoggerProviderOptions.RegisterProviderOptions<SampleLoggerOptions, SampleLoggerProvider>(loggingBuilder.Services);

        if (configureOptions != null)
        {
            loggingBuilder.Services.AddOptions<SampleLoggerOptions>()
                .Configure(configureOptions);
        }

        return loggingBuilder;
    }
}
```

```c#:program.cs
public class Program
{
    public static void Main(string[] args)
    {
        var builder = Host.CreateDefaultBuilder(args);

        builder.ConfigureMyProject();

        builder.ConfigureServices(services =>
        {
            services.AddHostedService<Worker>();
        });

        builder.ConfigureLogging((context, builder) =>
        {
            // サンプルロガーを追加します
            builder.AddSampleLogger();
        });

        var host = builder.Build();

        host.Run();
    }
}
```

## 参考：OSSのロギングライブラリ

### ZLogger

https://github.com/Cysharp/ZLogger

ILoggingBuilder に対する拡張メソッドが提供されています。
汎用ホストの既定のログプロバイダに組み込まれ、Logging.{プロバイダ名}.LogLevel プロパティによるログレベルの設定・変更もサポートされています。

```c#
builder.ConfigureLogging(context =>
{
    builder.AddZLoggerConsole();
});
```
```json:appsettings.json
{
  "Logging": {
    "ZLoggerConsole": {
      "LogLevel": {
        "Default": "Information"
      }
    }
  }
}
```

### NLog

https://github.com/NLog/NLog.Extensions.Logging

ILoggingBuilder に対する拡張メソッドが提供されています。
汎用ホストの既定のログプロバイダに組み込まれないため、Logging.{プロバイダ名}.LogLevel プロパティによるログレベルの設定・変更はサポートされていないようです。NLog 独自のオプション構成で制御することになります。

```c#
builder.ConfigureLogging((context, builder) =>
{
    builder.AddNLog(
        new NLogLoggingConfiguration(context.Configuration.GetSection("Logging:NLog"))
    );
});
```
```json:appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.Hosting.Lifetime": "Information"
    },
    "NLog": {
      "targets": {
        "logconsole": {
          "type": "Console"
        }
      },
      "rules": [
        {
          "logger": "*",
          "minLevel": "Info",
          "writeTo": "logconsole"
        }
      ]
    }
  }
}
```