.NET Core 汎用ホスト（GenericHost）ILoggerのログ出力メソッド

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

汎用ホスト（GenericHost）におけるログ出力で使用する ILogger のログ出力メソッドの拡張について考えてみます。

このドキュメントの内容は .NET Core 3.1 で確認しています。

## 標準のログ出力メソッド

Microsoft.Extensions.Logging.ILogger インターフェースに定義されているログ出力メソッドは Log&lt;TState&gt; のみです。最終的にはこのメソッドに集約されることになります。

```c#:Microsoft.Extensions.Logging.ILogger
public interface ILogger
{
    IDisposable BeginScope<TState>(TState state);
    bool IsEnabled(LogLevel logLevel);
    void Log<TState>(
        LogLevel logLevel
        , EventId eventId
        , TState state
        , Exception exception
        , Func<TState, Exception, string> formatter
    );
}
```

Microsoft.Extensions.Logging.LoggerExtensions クラスに ILogger インターフェースに対する拡張メソッドが定義されています。通常はこれらのログ出力メソッドを使用することが多いと思います。

```c#:Microsoft.Extensions.Logging.LoggerExtensions
public static class LoggerExtensions
{
    public static IDisposable BeginScope(this ILogger logger, string messageFormat, params object[] args);

    public static void Log(this ILogger logger
        , LogLevel logLevel, Exception exception, string message, params object[] args
    );
    public static void Log(this ILogger logger
        , LogLevel logLevel, EventId eventId, string message, params object[] args
    );
    public static void Log(this ILogger logger
        , LogLevel logLevel, string message, params object[] args
    );
    public static void Log(this ILogger logger
        , LogLevel logLevel, EventId eventId, Exception exception, string message, params object[] args
    );

    public static void LogCritical(this ILogger logger
        , string message, params object[] args
    );
    public static void LogCritical(this ILogger logger
        , Exception exception, string message, params object[] args
    );
    public static void LogCritical(this ILogger logger
        , EventId eventId, string message, params object[] args
    );
    public static void LogCritical(this ILogger logger
        , EventId eventId, Exception exception, string message, params object[] args
    );

    public static void LogDebug(this ILogger logger
        , EventId eventId, Exception exception, string message, params object[] args
    );
    public static void LogDebug(this ILogger logger
        , EventId eventId, string message, params object[] args
    );
    public static void LogDebug(this ILogger logger
        , Exception exception, string message, params object[] args
    );
    public static void LogDebug(this ILogger logger
        , string message, params object[] args
    );

    public static void LogError(this ILogger logger
        , string message, params object[] args
    );
    public static void LogError(this ILogger logger
        , Exception exception, string message, params object[] args
    );
    public static void LogError(this ILogger logger
        , EventId eventId, Exception exception, string message, params object[] args
    );
    public static void LogError(this ILogger logger
        , EventId eventId, string message, params object[] args
    );

    public static void LogInformation(this ILogger logger
        , EventId eventId, string message, params object[] args
    );
    public static void LogInformation(this ILogger logger
        , Exception exception, string message, params object[] args
    );
    public static void LogInformation(this ILogger logger
        , EventId eventId, Exception exception, string message, params object[] args
    );
    public static void LogInformation(this ILogger logger
        , string message, params object[] args
    );

    public static void LogTrace(this ILogger logger
        , string message, params object[] args
    );
    public static void LogTrace(this ILogger logger
        , Exception exception, string message, params object[] args
    );
    public static void LogTrace(this ILogger logger
        , EventId eventId, string message, params object[] args
    );
    public static void LogTrace(this ILogger logger
        , EventId eventId, Exception exception, string message, params object[] args
    );

    public static void LogWarning(this ILogger logger
        , EventId eventId, string message, params object[] args
    );
    public static void LogWarning(this ILogger logger
        , EventId eventId, Exception exception, string message, params object[] args
    );
    public static void LogWarning(this ILogger logger
        , string message, params object[] args
    );
    public static void LogWarning(this ILogger logger
        , Exception exception, string message, params object[] args
    );
}
```

## Func&lt;string&gt; を引数とするオーバーロードを追加する

ログメッセージを生成する Func&lt;string&gt; 型のメソッドを引数として受け取るログ出力メソッドを拡張メソッドとして実装してみます。最終的に ILogger.Log&lt;TState&gt; メソッドを呼び出すこととします。LogMessageBuilderState 型のインスタンスに受け取ったメソッドを格納し、ILogger.Log&lt;TState&gt; メソッドに渡します。

```c#
public static void LogDebug(this ILogger logger, EventId eventId, Exception exception, Func<string> messageBuilder)
{
    if (!logger.IsEnabled(LogLevel.Debug)) { return; }
    var state = new LogMessageBuilderState(messageBuilder);
    logger.Log(LogLevel.Debug, eventId, state, exception, LoggerEnvironment.LogMessageBuilderFormatter);
}
```

```c#:LoggerEnvironment
public static class LoggerEnvironment
{
    public static Func<LogMessageBuilderState, Exception, string> LogMessageBuilderFormatter
    {
        get { return s_LogMessageBuilderFormatter; }
        set { s_LogMessageBuilderFormatter = value; }
    }
    private static Func<LogMessageBuilderState, Exception, string> s_LogMessageBuilderFormatter = LogMessageBuilderState.Format;
}
```

```c#:LogMessageBuilderState
public readonly struct LogMessageBuilderState
{
    public LogMessageBuilderState(Func<string> messageBuilder)
    {
        MessageBuilder = messageBuilder;
    }

    public Func<string> MessageBuilder { get; }

    public override string ToString()
    {
        return MessageBuilder?.Invoke();
    }

    public static string Format(LogMessageBuilderState info, Exception exception)
    {
        return info.ToString();
    }
}
```






## CallerInfo 属性を使って呼び出し元情報を付加する

CallerInfo 属性を使ったログ出力メソッドを実装してみます。

### CallerInfo 属性の仕組み

CallerInfo 属性はメソッドの呼び出し元情報を引数として取得することができる属性です。コンパイラによってメソッドの呼び出しコードに値が埋め込まれるため、実行時の負荷はほとんどありません。

|属性|説明|
|:--|:--|
|CallerMemberName|対象のメソッドを呼び出したコードが実装されているメソッドまたはプロパティ。|
|CallerFilePath|対象のメソッドを呼び出したコードが実装されているソースファイルのパス。|
|CallerLineNumber|対象のメソッドを呼び出したコードが実装されている行番号。|

次のようなログ出力メソッドがあるとします。

```c#
// using System.Runtime.CompilerServices;
public static class LogUtility
{
    public static void Log(string message
        , [CallerMemberName] string callerMember = ""
        , [CallerFilePath] string callerFilePath = ""
        , [CallerLineNumber] int callerLineNumber = -1
    )
    {
        Console.WriteLine($"[{callerMember}] {message} ({callerFilePath}:{callerLineNumber})")
    }
}
```

このログ出力メソッドを次のように呼び出します。CallerInfo 属性にあたる3つの引数は optional ですので省略可能です。

```c#:Program.cs
namespace GenericHostSample
{
    class Program
    {
        static async Task Main(string[] args)
        {
            // LogUtility.Log("Start!", "Main", "d:\source\GenericHostSample\Program.cs", 7)
            // のようなコードとしてコンパイルされます。
            LogUtility.Log("Start!");
            await CreateHostBuilder(args).RunConsoleAsync();
        }
    }
}
```

コンソールには次のようなログが出力されます。

```console
[Main] Start! (d:\source\GenericHostSample\Program.cs:7)
```


### ILogger への組み込み

CallerInfo 属性を使ったログ出力メソッドを拡張メソッドとして実装してみます。最終的に ILogger.Log&lt;TState&gt; メソッドを呼び出すこととします。LogWithCallerState 型のインスタンスにログメッセージと呼び出し元情報を格納し、ILogger.Log&lt;TState&gt; メソッドに渡します。

フォーマットメソッドは静的フィールドに保持させておく他、ILogger が ILogFormatter&lt;LogWithCallerState&gt; インターフェースを実装する場合はそのフォーマットメソッドを使用するようにしています。

```c#
// using System.Runtime.CompilerServices;

public static void LogDebugWithCaller(this ILogger logger, string message, object[] args = null
    , [CallerMemberName] string callerMember = ""
    , [CallerFilePath] string callerFilePath = ""
    , [CallerLineNumber] int callerLineNumber = -1)
{
    if (!logger.IsEnabled(LogLevel.Debug)) { return; }

    var state = new LogWithCallerState(message, args, callerMember, callerFilePath, callerLineNumber);

    if (logger is ILogFormatter<LogWithCallerState> formatter)
    {
        logger.Log(LogLevel.Debug, default, state, null, formatter.Format);
    }
    else
    {
        logger.Log(LogLevel.Debug, default, state, null, LoggerEnvironment.LogWithCallerFormatter);
    }
}
```

```c#:ILogFormatter&lt;TState&gt;
public interface ILogFormatter<TState>
{
    string Format(TState state, Exception exception);
}
```

```c#:LoggerEnvironment
public static class LoggerEnvironment
{
    public static Func<LogWithCallerState, Exception, string> LogWithCallerFormatter
    {
        get { return s_LogWithCallerFormatter; }
        set { s_LogWithCallerFormatter = value; }
    }
    private static Func<LogWithCallerState, Exception, string> s_LogWithCallerFormatter = LogWithCallerState.Format;
}
```

```c#:LogWithCallerState
public readonly struct LogWithCallerState
{
    #region ctor

    public LogWithCallerState(string logmessage, object[] logMessageArgs, string callerMember, string callerFilePath, int callerLineNumber)
    {
        if (logmessage != null && logMessageArgs?.Length > 0)
        {
            LogMessage = () => string.Format(logmessage, logMessageArgs);
        }
        else
        {
            LogMessage = () => logmessage;
        }

        CallerMember = callerMember;
        CallerFilePath = callerFilePath;
        CallerLineNumber = callerLineNumber;
    }

    public LogWithCallerState(Func<string> logMessage, string callerMember, string callerFilePath, int callerLineNumber)
    {
        LogMessage = logMessage;
        CallerMember = callerMember;
        CallerFilePath = callerFilePath;
        CallerLineNumber = callerLineNumber;
    }

    #endregion

    public string CallerMember { get; }
    public string CallerFilePath { get; }
    public int CallerLineNumber { get; }
    public Func<string> LogMessage { get; }

    public override string ToString()
    {
        return $"{LogMessage?.Invoke()}|{CallerMember}|{Path.GetFileName(CallerFilePath)}|{CallerLineNumber}";
    }

    public static string Format(LogWithCallerInfo info, Exception exception)
    {
        return info.ToString();
    }
}
```