优化 .NET Core logging 中的泛型 logger

DotNetCore实战

共 7462字,需浏览 15分钟

 ·

2021-04-29 22:01

优化 .NET Core logging 中的泛型 logger

Intro

在微软的 logging 组件中,我们可以比较方便的使用泛型 Logger,如:ILogger<Generic> 这样的,

但是如果泛型 Logger 的类型是一个泛型类型就会有些问题,具体的泛型参数不会作为 categoryName 的一部分,我们可以实现一个自己的 ILogger<T> 来改变这个行为,详细可以参考下面的介绍

Reproduce

这个问题非常好重现,只需要一个测试的泛型类就可以了,我写了一个简单的测试类,定义如下:

private class GenericTest<T>
{
    private readonly ILogger<GenericTest<T>> _logger;

    public GenericTest(ILogger<GenericTest<T>> logger)
    {
        _logger = logger;
    }

    public void Test() => _logger.LogInformation("test");
}

测试代码如下:

using var services = new ServiceCollection()
    .AddLogging(builder => builder.AddConsole())
    .AddSingleton(typeof(GenericTest<>))
    .BuildServiceProvider();
services.GetRequiredService<GenericTest<int>>()
    .Test();
services.GetRequiredService<GenericTest<string>>()
    .Test();

这里使用了两个泛型类型,一个泛型参数是 int,一个是 string,来看上面代码的输出结果吧,输出结果如下:

可以看到,默认的日志行为我们没有办法区分泛型类的泛型参数具体是什么,这对于我们来说有时候是很不方便的

What's inside

我们可以在 Github 上找到 logging 组件的源代码,可以参考:https://github.com/dotnet/runtime/blob/fa06656c41947e22fc6efd909cce0a6a180f1078/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerT.cs

通过源码我们可以看到默认的行为,并不会记录泛型参数,经过测试如果我们需要包含泛型参数信息只需要把 includeGenericParameters 参数设置为 true 即可,既然明确了如何实现我们期望的效果改起来就会很简单

Cutom Generic Logger

微软的 Logging 非常的依赖注入,泛型的 Logger 也是依赖注入的,我们只需要注入自己的泛型 Logger 实现就可以代替默认的行为了,可以参考:https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Logging/src/LoggingServiceCollectionExtensions.cs#L42


为了不造成 breaking change,我们可以加一个配置,默认还是与微软现在的行为保持一致,针对想要区分的类型使用带泛型参数的行为,实现代码如下:

// 泛型 logger 配置
public sealed class GenericLoggerOptions
{
    // 返回 true 则使用带泛型参数的 typeName,否则使用默认的行为
    public Func<Type, bool>? FullNamePredict { getset; }
}

internal sealed class GenericLogger<T> : ILogger<T>
{
    private readonly ILogger _logger;

    /// <summary>
    /// Creates a new <see cref="GenericLogger{T}"/>.
    /// </summary>
    /// <param name="factory">The factory.</param>
    /// <param name="options">GenericLoggerOptions</param>
    public GenericLogger(ILoggerFactory factory, IOptions<GenericLoggerOptions> options)
    {
        if (factory == null)
        {
            throw new ArgumentNullException(nameof(factory));
        }

        // 通过配置的委托来判断是否要包含泛型参数
        var includeGenericParameters = options.Value.FullNamePredict?.Invoke(typeof(T)) == true;
        
        _logger = factory.CreateLogger(TypeHelper.GetTypeDisplayName(typeof(T), includeGenericParameters: includeGenericParameters, nestedTypeDelimiter: '.'));
    }

    /// <inheritdoc />
    IDisposable ILogger.BeginScope<TState>(TState state)
    {
        return _logger.BeginScope(state);
    }

    /// <inheritdoc />
    bool ILogger.IsEnabled(LogLevel logLevel)
    {
        return _logger.IsEnabled(logLevel);
    }

    /// <inheritdoc />
    void ILogger.Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
    {
        _logger.Log(logLevel, eventId, state, exception, formatter);
    }

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) => throw new NotImplementedException();

    public bool IsEnabled(LogLevel logLevel) => throw new NotImplementedException();

    public IDisposable BeginScope<TState>(TState state) => throw new NotImplementedException();
}

TypeHelper 中的方法就是微软 Logging 中引用的 TypeNameHelper,因为是 internal,所以单独拷出来一份,

上面的 Logger 与微软默认的 logger 唯一的不同之处就在于多了一个配置。。

为了使用起来方便,定义了一个 ILoggingBuilder 的扩展方法,定义如下:

public static ILoggingBuilder UseCustomGenericLogger(this ILoggingBuilder loggingBuilder, Action<GenericLoggerOptions> genericLoggerConfig)
{
    Guard.NotNull(loggingBuilder, nameof(loggingBuilder));
    Guard.NotNull(genericLoggerConfig, nameof(genericLoggerConfig));
    loggingBuilder.Services.Configure(genericLoggerConfig);
    loggingBuilder.Services.AddSingleton(typeof(ILogger<>), typeof(GenericLogger<>));
    return loggingBuilder;
}

好了,现在我们来测试一下我们自己的泛型 logger 吧,测试代码如下:

using var services = new ServiceCollection()
    .AddLogging(builder => builder.AddConsole().UseCustomGenericLogger(options => options.FullNamePredict = _ => true))
    .AddSingleton(typeof(GenericTest<>))
    .BuildServiceProvider();
services.GetRequiredService<GenericTest<int>>()
    .Test();
services.GetRequiredService<GenericTest<string>>()
    .Test();

输出结果如下:

可以看到现在的输出日志中已经包含了泛型类型的泛型参数,如果你对自己名称还不够满意,也可以自定义 GetTypeDisplayName 的行为

More

上面的测试代码有需要的可以从 Github 上获取:https://github.com/WeihanLi/WeihanLi.Common/blob/dev/samples/DotNetCoreSample/LoggerTest.cs#L37

感觉泛型参数还是记录一下的比较好,这样我们才能知道具体是哪一个类型打印出来的日志,像第一种方式打印出来的日志,完全就是一脸懵逼,真正出现了问题,完全不知道是哪一个类型的日志,只能靠猜了,这体验就太不好了,不过还好我们可以比较方便的进行定制。

不知道你是否也有这样的想法呢,在 Github 上提了一个 issue https://github.com/dotnet/runtime/issues/51368,如果感兴趣,可以关注一下 ,留下你的看法

References

  • https://github.com/dotnet/runtime/issues/51368
  • https://github.com/WeihanLi/WeihanLi.Common/blob/dev/samples/DotNetCoreSample/LoggerTest.cs#L37


浏览 28
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报