针对控制台的ILogger实现类型为ConsoleLogger,对应的ILoggerProvider实现类型为ConsoleLoggerProvider,这两个类型都定义在 NuGet包“Microsoft.Extensions.Logging.Console”中。ConsoleLogger要将一条日志输出到控制台上,首选要解决的是格式化的问题,具体来说是如何将日志消息的内容荷载和元数据(类别、等级和事件ID等)格式化成呈现在控制台上的文本。针对日志的格式化由ConsoleFormatter对象来完成。(本篇提供的实例已经汇总到《ASP.NET Core 6框架揭秘-实例演示版》)
[S901]SimpleConsoleFormatter格式化器(源代码)
[S902]SystemdConsoleFormatter格式化器(源代码)
[S903]JsonConsoleFormatter格式化器(源代码)
[S904]改变ConsoleLogger的标准输出和错误输出(源代码)
[S905]自定义控制台日志的格式化器(源代码)
下面了代码演示了如何使用SimpleConsoleFormatter来格式化控制台输出的日志。我们利用命令行参数控制是否采用单行文本输出和着色方案。在调用ILoggingBuiler接口的AddConsole扩展方法对ConsoleLoggerProvider对象进行注册之后,我们接着调用它的AddSimpleConsole扩展方法将SimpleConsoleFormatter作为格式化器,并利用作为参数的Action<SimpleConsoleFormatterOptions>委托根据命令行参数的对SimpleConsoleFormatterOptions配置选项进行了相应设置(S901)。
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Console; var configuration = new ConfigurationBuilder() .AddCommandLine(args) .Build(); var singleLine = configuration.GetSection("singleLine").Get<bool>(); var colorBebavior = configuration.GetSection("color").Get<LoggerColorBehavior>(); var logger = LoggerFactory.Create(builder => builder .AddConsole() .AddSimpleConsole(options => { options.SingleLine = singleLine; options.ColorBehavior = colorBebavior; })) .CreateLogger<Program>(); var levels = (LogLevel[])Enum.GetValues(typeof(LogLevel)); levels = levels.Where(it => it != LogLevel.None).ToArray(); var eventId = 1; Array.ForEach(levels,level => logger.Log(level, eventId++, "This is a/an {0} log message.", level)); Console.Read();
我们已命名行的方式三次启动演示程序,并利用参数(singleLine=true, color=Disabled)控制对应的格式化行为。从图1所示的结果可以看出日志输出的格式是与我们指定的命名行参数是匹配的。
图1 基于SimpleConsoleFormatter的格式化
我们使用如下这个实例来演示针对SystemdConsoleFormatter的格式化。如代码片段所示,我们利用命令行参数“includeScopes”来决定是否支持日志范围。在调用ILoggingBuilder接口的AddConsole扩展方法ConsoleLoggerProvider对象进行注册之后,我们接着调用了该接口的AddSystemdConsole扩展方法对SystemdConsoleFormatter及其配置选项进行注册。为了输出所有等级的日志,我们将最低日志等级设置为Trace。为了体现针对异常信息的输出,我们在调用Log方法是传入了一个Exception对象。
using Microsoft.Extensions.Logging; var includeScopes = args.Contains("includeScopes"); var logger = LoggerFactory.Create(builder => builder .SetMinimumLevel(LogLevel.Trace) .AddConsole() .AddSystemdConsole(options => options.IncludeScopes = includeScopes)) .CreateLogger<Program>(); var levels = (LogLevel[])Enum.GetValues(typeof(LogLevel)); levels = levels.Where(it => it != LogLevel.None).ToArray(); var eventId = 1; Array.ForEach(levels, Log); Console.Read(); void Log(LogLevel logLevel) { using (logger.BeginScope("Foo")) { using (logger.BeginScope("Bar")) { using (logger.BeginScope("Baz")) { logger.Log(logLevel, eventId++, new Exception("Error..."), "This is a/an {0} log message.", logLevel); } } } }
我们采用命令行的方式两次启动演示程序,第一次采用默认配置,第二次利用命令行参数“includeScopes”开启针对日志范围的支持。从图2所示的输出结果可以看出六条日志均以单条文本的形式输出到控制台上,对应的日志等级(Trace、Debug、Information、Warning、Error和Critical)均被转换成Syslog日志等级(7、7、6、4、3和2)。
图2 基于SystemdConsoleFormatter的格式化
我们对上面的演示程序略加改动,将针对ILoggingBuilder接口的AddSystemdConsole扩展方法的调用替换成调用AddJsonConsole扩展方法,后者帮助我们完成针对JsonConsoleFormatter及其配置选项的注册。为了减少控制台上输出的内容,我们移除了针对最低日志等级的设置。
using Microsoft.Extensions.Logging; var includeScopes = args.Contains("includeScopes"); var logger = LoggerFactory .Create(builder => builder .AddConsole() .AddJsonConsole(options => options.IncludeScopes = includeScopes)) .CreateLogger<Program>(); var levels = (LogLevel[])Enum.GetValues(typeof(LogLevel)); levels = levels.Where(it => it != LogLevel.None).ToArray(); var eventId = 1; Array.ForEach(levels, Log); Console.Read(); void Log(LogLevel logLevel) { using (logger.BeginScope("Foo")) { using (logger.BeginScope("Bar")) { using (logger.BeginScope("Baz")) { logger.Log(logLevel, eventId++, new Exception("Error..."), "This is a/an {0} log message.", logLevel); } } } }
我们依然采用上面的方式两次执行改动后的程序。在默认以及开启日志范围的情况下,控制台分别具有图3所示的输出。可以看出输出的内容不仅包含参数填充生成完整内容,还包含原始的模板。日志范围的路径是以数组的方式输出的。
图3 基于JsonConsoleFormatter的格式化
ConsoleLogger具有“标准输出”和“错误输出”两个输出渠道,分别对应着Console类型的静态属性Out和Error返回的TextWriter对象。对于不高于LogToStandardErrorThreshold设定等级的日志会采用标准输出,高于或者等于该等级的日志则采用错误输出。由于LogToStandardErrorThreshold属性的默认值为None,所以任何等级的日志都被写入标准输出。如下的代码片段演示了如何通过设置这个属性改变不同等级日志的输出渠道。
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Console; using (var @out = new StreamWriter("out.log") { AutoFlush = true }) using (var error = new StreamWriter("error.log") { AutoFlush = true }) { Console.SetOut(@out); Console.SetError(error); var logger = LoggerFactory.Create(builder => builder .SetMinimumLevel(LogLevel.Trace) .AddConsole(options =>options.LogToStandardErrorThreshold = LogLevel.Error) .AddSimpleConsole(options =>options.ColorBehavior = LoggerColorBehavior.Disabled)) .CreateLogger<Program>(); var levels = (LogLevel[])Enum.GetValues(typeof(LogLevel)); levels = levels.Where(it => it != LogLevel.None).ToArray(); var eventId = 1; Array.ForEach(levels, Log); Console.Read(); void Log(LogLevel logLevel) => logger.Log(logLevel, eventId++, "This is a/an {0} log message.", logLevel); }
如上面的代码片段所示,我们创建了两个针对本地文件(out.log和error.log)的StreamWriter,并通过调用Console类型的静态方法SetOut和SetError将其设置为控制台的标准输出和错误输出。在针对ILogingBuilder接口的AddConsole扩展方法调用中,我们将配置选项ConsoleLoggerOptions的LogToStandardErrorThreshold属性设置为Error,并调用SetMinimumLevel方法将最低日志等级设置为Trace。我们还调用AddSimpleConsole扩展方法禁用作色方案。当程序运行之后,针对具有不同等级的六条日志,四条不高于Error的日志被输出到如图4所示的out.log中,另外两条则作为错误日志被输出到error.log中,控制台上将不会有任何输出内容。
图4 标准输入和错误输出
为了能够更加灵活地控制日志在控制台上的输出格式,我们自定义了如下这个格式化器类型。如代码片段所示,这个名为TemplatedConsoleFormatter会按照指定的模板来格式化输出的日志内容,它使用的配置选项类型为TemplatedConsoleFormatterOptions,日志格式模板就体现在它的Template属性上。
public class TemplatedConsoleFormatter : ConsoleFormatter { private readonly bool _includeScopes; private readonly string _tempalte; public TemplatedConsoleFormatter(IOptions<TemplatedConsoleFormatterOptions> options) : base("templated") { _includeScopes = options.Value.IncludeScopes; _tempalte = options.Value?.Template?? "[{LogLevel}]{Category}/{EventId}:{Message}\n{Scopes}\n"; } public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider scopeProvider, TextWriter textWriter) { var builder = new StringBuilder(_tempalte); builder.Replace("{Category}", logEntry.Category); builder.Replace("{EventId}", logEntry.EventId.ToString()); builder.Replace("{LogLevel}", logEntry.LogLevel.ToString()); builder.Replace("{Message}", logEntry.Formatter?.Invoke(logEntry.State, logEntry.Exception)); if (_includeScopes && scopeProvider != null) { var buidler2 = new StringBuilder(); var writer = new StringWriter(buidler2); scopeProvider.ForEachScope(WriteScope, writer); void WriteScope(object? scope, StringWriter state) { writer.Write("=>" + scope); } builder.Replace("{Scopes}", buidler2.ToString()); } textWriter.Write(builder); } } public class TemplatedConsoleFormatterOptions: ConsoleFormatterOptions { public string? Template { get; set; } }
我们将TemplatedConsoleFormatter的别名指定为“templated”,格式模板利用占位符“{Category}”、“{EventId}”、“{LogLevel}”、“{Message}”和“{Scopes}”表示日志类别、事件ID、等级、消息和范围信息。 “[{LogLevel}]{Category}/{EventId}:{Message}\n{Scopes}\n”是我们提供的默认模板。现在我们采用如下这个名为appsettings.json的JSON文件来提供所有的配置。
{ "Logging": { "Console": { "FormatterName": "templated", "LogToStandardErrorThreshold": "Error", "FormatterOptions": { "IncludeScopes": true, "UseUtcTimestamp": true, "Template": "[{LogLevel}]{Category}:{Message}\n" } } } }
如上面的代码片段所示,我们将控制台日志输出的相关设置定义在“Logging:Console”配置节中,并定义了格式化器名称(“templated”)、错误日志最低等级(“Error”)。“FormatterOptions”配置节的内容将最终绑定到TemplatedConsoleFormatterOptions配置选项上。在如下所示的演示程序中,我们加载这个配置文件并提取代表“Logging”配置节的IConfigguration对象,我们将这个对象作为参数调用ILoggingBuilder接口的AddConfiguration扩展方法进行了注册。
using App; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; var logger = LoggerFactory.Create(Configure).CreateLogger<Program>(); var levels = (LogLevel[])Enum.GetValues(typeof(LogLevel)); levels = levels.Where(it => it != LogLevel.None).ToArray(); var eventId = 1; Array.ForEach(levels, Log); Console.Read(); void Configure(ILoggingBuilder builder) { var configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .Build() .GetSection("Logging"); builder .AddConfiguration(configuration) .AddConsole() .AddConsoleFormatter<TemplatedConsoleFormatter,TemplatedConsoleFormatterOptions>(); } void Log(LogLevel logLevel) => logger.Log(logLevel, eventId++, "This is a/an {0} log message.", logLevel);
在调用ILoggingBuilder接口的AddConsole扩展方法对ConsoleLoggerProvider进行注册之后,我们调用了它的AddConsoleFormatter<TemplatedConsoleFormatter,TemplatedConsoleFormatterOptions>方法,该方法将利用配置来绑定注册格式化器对应的TemplatedConsoleFormatterOptions配置选项。演示程序启动之后,每一条日志将按照配置中提供的模板进行格式化,并以图5所示的形式输出到控制台上。
图5 基于TemplatedConsoleFormatter的格式化