前言
要想记录日志,常用的方式是访问ILogger实例提供的日志记录方法:
private readonly ILogger<WeatherForecastController> _logger;public WeatherForecastController(ILogger<WeatherForecastController> logger)
{_logger = logger;
}[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{ var result = Enumerable.Range(1, 5).Select(index => new WeatherForecast{TemperatureC = Random.Shared.Next(-20, 55),}).ToArray();_logger.LogInformation("LogInformation: {0}", JsonSerializer.Serialize(result));return result;
}
其实,.NET下还有一个高性能日志记录类LoggerMessage[1]。
与ILogger记录器扩展方法(例如LogInformation和LogDebug)相比,LoggerMessage具有以下性能优势:
记录器扩展方法需要将值类型(例如 int)“装箱”(转换)到 object中。LoggerMessage模式使用带强类型参数的静态Action字段和扩展方法来避免装箱。
记录器扩展方法每次写入日志消息时必须分析消息模板(命名的格式字符串)。如果已定义消息,那么LoggerMessage只需分析一次模板即可。
示例代码如下:
private static readonly Action<ILogger, IEnumerable<WeatherForecast>, Exception?> _logWeatherForecast =LoggerMessage.Define<IEnumerable<WeatherForecast>>(logLevel: LogLevel.Information,eventId: 0,formatString: "LoggerMessage: {aa}");//使用
_logWeatherForecast(_logger, result, null);
虽然使用LoggerMessage可以为我们提供更好的性能,但是,需要手工编写大量的LoggerMessage.Define代码;而且formatString消息模板中的参数占位符并没有任何控制(例如{aa}
),很可能导致传递错误参数。
而在.NET 6中,可以使用Source Generator帮助我们自动生成高性能日志记录代码。
Demo
你需要创建一个partial
方法,然后在其头部声明LoggerMessageAttribute
。
示例代码如下:
[LoggerMessage(0, LogLevel.Information, "LoggerMessageAttribute: {weatherForecasts}")]
partial void LogWeatherForecast(IEnumerable<WeatherForecast> weatherForecasts);//使用
LogWeatherForecast(result);
查看自动生成的代码,其实是Source Generator帮我们编写了LoggerMessage.Define代码:
partial class WeatherForecastController
{[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "6.0.5.2210")]private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, global::System.Collections.Generic.IEnumerable<global::WebApplication1.WeatherForecast>, global::System.Exception?> __LogWeatherForecastCallback =global::Microsoft.Extensions.Logging.LoggerMessage.Define<global::System.Collections.Generic.IEnumerable<global::WebApplication1.WeatherForecast>>(global::Microsoft.Extensions.Logging.LogLevel.Information, new global::Microsoft.Extensions.Logging.EventId(0, nameof(LogWeatherForecast)), "LoggerMessageAttribute: {weatherForecasts}", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true }); [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "6.0.5.2210")]partial void LogWeatherForecast(global::System.Collections.Generic.IEnumerable<global::WebApplication1.WeatherForecast> weatherForecasts){if (_logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Information)){__LogWeatherForecastCallback(_logger, weatherForecasts, null);}}
}
LogWeatherForecast
方法直接使用了Controller中声明的_logger对象,并不需要我们传入;而且写入日志前判断了_logger.IsEnabled
避免不必要的日志写入操作,对性能有进一步提高。
更为重要的是,它不会允许传入错误的参数:
结论
使用LoggerMessageAttribute
可以提高日志记录性能,但它也有其缺点:
使用
partial
方法声明必须将类也定义成partial
。日志使用了参数对象的ToString()方法,对于复杂类型,不能在方法中传入序列化对象
LogWeatherForecast(JsonSerializer.Serialize(result))
,因为会始终执行影响性能,可以通过定义成record class
或自定义ToString()方法变通解决:
参考资料
[1]
LoggerMessage: https://docs.microsoft.com/zh-cn/dotnet/core/extensions/high-performance-logging
如果你觉得这篇文章对你有所启发,请关注我的个人公众号”My IO“