基于 Newtonsoft Json 实现 console log formatter

Intro

.NET 从 5.0 开始支持了 console formatter,首次支持了 json console, 自定义 console 日志也变得可能,可以参考之前的介绍 .net 5.0 中的 JsonConsole

然而 .NET 里的 json console 里的 json 编码要求是比较严格的,有一些特殊符号和特殊编码会有点问题,我们需要使用 JavaScriptEncoder.UnsafeRelaxedJsonEscaping 才能正常显式,于是想着直接基于 newtonsoft json 来实现一个 console log formatter.

Implement

参考 JsonConsoleFormatter 可以实现基于 NewtonsoftJson 的 NewtonJsonFormatter ,实现如下:

public sealed class NewtonJsonFormatterOptions: ConsoleFormatterOptions
{
}

public sealed class NewtonJsonFormatter: ConsoleFormatter
{
    public const string FormatterName = "NewtonJson";
    
    private readonly NewtonJsonFormatterOptions _options;

    public NewtonJsonFormatter(IOptions<NewtonJsonFormatterOptions> options) : base(FormatterName)
    {
        _options = options.Value;
    }

    public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter)
    {
        var message = logEntry.Formatter(logEntry.State, logEntry.Exception);
        if (logEntry.Exception == null && message == null)
        {
            return;
        }

        JsonWriter writer = new JsonTextWriter(textWriter);
        writer.WriteStartObject();
        if (_options.TimestampFormat != null)
        {
            writer.WritePropertyName("Timestamp");
            var timestamp = _options.UseUtcTimestamp ? DateTimeOffset.UtcNow : DateTimeOffset.Now;
            var timestampText = timestamp.ToString(_options.TimestampFormat);
            writer.WriteValue(timestampText);
        }
        writer.WritePropertyName("Level");
        writer.WriteValue(logEntry.LogLevel.ToString());
        writer.WritePropertyName("EventId");
        writer.WriteValue(logEntry.EventId.ToString());
        writer.WritePropertyName(nameof(logEntry.Category));
        writer.WriteValue(logEntry.Category);
        writer.WritePropertyName("Message");
        writer.WriteValue(message);
        if (logEntry.Exception != null)
        {
            writer.WritePropertyName("Exception");
            writer.WriteValue(logEntry.Exception);
        }

        if (logEntry.State != null)
        {
            writer.WritePropertyName(nameof(logEntry.State));
            writer.WriteStartObject();
            writer.WritePropertyName("Message");
            writer.WriteValue(logEntry.State.ToString());
            if (logEntry.State is System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<string, object>> stateProperties)
            {
                foreach (var item in stateProperties)
                {
                    writer.WritePropertyName(item.Key);
                    writer.WriteValue(item.Value);
                }
            }
            writer.WriteEndObject();
        }
        
        writer.WriteEndObject();
        writer.Flush();
        textWriter.WriteLine();
    }
}

formatter 的设计需要有一个 options 所以我们需要新加一个 NewtonJsonFormatterOptions

为了使用起来方便,我们定义一个扩展方法 AddNewtonJsonConsole

public static partial class LoggingBuilderExtensions
{
    public static ILoggingBuilder AddNewtonJsonConsole(this ILoggingBuilder loggingBuilder, 
        Action<NewtonJsonFormatterOptions>? optionsConfigure = null)
    {
        loggingBuilder.AddConsoleFormatter<NewtonJsonFormatter, NewtonJsonFormatterOptions>();
        loggingBuilder.AddConsole(options => options.FormatterName = NewtonJsonFormatter.FormatterName);
        if (optionsConfigure != null)
        {
            loggingBuilder.Services.Configure(optionsConfigure);
        }
        return loggingBuilder;
    }
}

首先我们需要通过 AddConsoleFormatter 来注册我们自定义的 NewtonJsonFormatter

然后使用 AddConsole 注册 console 服务并指定 FormatterName ,另外可以通过一个可选的委托来自定义一些配置

Sample

来看一个简单的使用示例:

使用默认的 JsonConsole 时

var builder = Host.CreateEmptyApplicationBuilder(null);
builder.Logging.AddJsonConsole();
using var host = builder.Build();
host.Run();

输出结果如下:

Default JsonConsole

使用我们自定义的 NewtonJsonConsole 时:

var builder = Host.CreateEmptyApplicationBuilder(null);
builder.Logging.AddNewtonJsonConsole();
using var host = builder.Build();
host.Run();
NewtonJsonConsole

可以看到使用我们基于 NewtonsoftJson 自定义的 NewtonJsonConsole 时,Ctrl+C 不会再被 encode 了,可以看到原始的日志,就不用再看到一脸懵了

More

前面的实现仅供参考,仔细看的话会发现我们前面定义的 NewtonJsonConsole 并没有输出 OriginalFormat,并且没有处理 scope 的信息,实际使用的话还需要根据需要进行修改,实现仅供参考

源码链接:https://github.com/WeihanLi/WeihanLi.Common/blob/dev/samples/DotNetCoreSample/NewtonJsonFormatter.cs