优化 .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 { get; set; }
}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