微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

.NET Core的日志[2]:将日志输出到控制台

对于一个控制台应用,比如采用控制台应用作为宿主的ASP.NET Core应用,我们可以将记录的日志直接输出到控制台上。针对控制台的Logger是一个类型为ConsoleLogger的对象,ConsoleLogger对应的LoggerProvider类型为ConsoleLoggerProvider,这两个类型都定义在 NuGet包“Microsoft.Extensions.Logging.Console”之中。 本文已经同步到《ASP.NET Core框架揭秘》之中]

目录
一、ConsoleLogger
二、ConsoleLogScope
三、ConsoleLoggerProvider
四、扩展方法AddConsole

一、ConsoleLogger

如下所示的代码片段展示了ConsoleLogger类型的定义。它具有四个属性,代表Logger名称的Name属性最初由ConsoleLoggerProvider提供,实际上就是LoggerFactory在创建Logger时指定的日志类型(Category)。ConsoleLogger的Console属性代表当前控制台,它的类型为IConsole接口。之所以没有直接采用System.Console向控制台输出格式化的日志消息,是因为需要提供跨平台的支持,IConsole接口表示的就是这么一个与具体平台无关的抽象化的控制台。

   1: public @H_404_24@class ConsoleLogger : ILogger
   2: {
   3:     @H_404_24@string                           Name { get; }
   4:     @H_404_24@public IConsole                         Console { get; set; }
   5:     @H_404_24@public Func<@H_404_24@string,LogLevel,@H_404_24@bool>     Filter { get; set; }
   6:     @H_404_24@bool                             IncludeScopes { get; set; }    
   7:  
   8:     @H_404_24@public ConsoleLogger(@H_404_24@string name,Func<@H_404_24@bool> filter,1)">bool includeScopes);
   9:     @H_404_24@public Idisposable BeginScope<TState>(TState state);
  10:   
  11:     @H_404_24@bool IsEnabled(LogLevel logLevel);
  12:     @H_404_24@void Log<TState>(LogLevel logLevel,EventId eventId,TState state,Exception exception,Func<TState,Exception,1)">string> formatter);  
  13:     @H_404_24@virtual @H_404_24@void WriteMessage(LogLevel logLevel,1)">string logName,1)">int eventId,1)">string message);
  14: }

ConsoleLogger的Filter属性通过一个委托对象来过滤真正需要写到控制台的日志消息,该属性的返回类型为Func<string,bool>,两个输入参数分别表示分发给它的日志消息的类型和等级,如果执行该委托对象返回False,日志消息将会被直接忽略。ConsoleLogger的IsEnabled方法会直接将指定日志等级作为参数(ConsoleLogger的Name属性作为另一个参数)调用这个委托对象得到最终的返回结果。ConsoleLogger的IncludeScopes与上面介绍的日志上下文范围有关,我们会在后续的部分对它进行单独介绍。

对于ConsoleLogger的这四个属性,除了表示当前控制台的Console属性,其余三个均可以在创建它的时候通过构造函数的相应参数来指定。接下来我们来简单了解一下表示抽象化控制台的IConsole接口,该接口具有如下三个方法。在调用Write和WriteLine方法向控制台输出内容的时候,我们除了指定写入的消息文本之外,还可以控制消息在控制台上的背景色和前景色。Flush方法与数据输出缓冲机制有关,如果采用缓冲机制,通过Write或者WriteLine方法写入的消息并不会立即输出到控制台,而是先被保存到缓冲区,Flush方法被执行的时候会将缓冲区的所有日志消息批量输出到控制台上。

void Write(@H_404_24@string message,ConsoleColor? background,ConsoleColor? foreground);
void Flush();
   1: {LogLevel} : {Category}[{EventId}]
class ConsoleLogScope
internal ConsoleLogScope(@H_404_24@object state);
   6:  
public ConsoleLogScope            Parent { get; set; }
object state)
   5:         ConsoleLogScope current = Current;
   7:         Current.Parent = current;
   9:     }
private @H_404_24@class disposableScope : Idisposable
  13:         @H_404_24@void dispose()
  15:             ConsoleLogScope.Current = ConsoleLogScope.Current.Parent;
  16:         }
  17:     }
@H_314_404@  18: }

简单地说,我们调用ConsoleLogScope的静态Push方法会创建当前日志上下文范围并返回一个disposableScope对象,一旦我们调用这个disposableScope对象的dispose方法,这就意味着这个上下文范围的终结。与此同时,原来的ConsoleLogScope从新成为当前的日志上下文。下面的代码片段体现了ConsoleLogScope针对作用域控制方式,这段代码来体现另一个细节,那就是ConsoleLogScope类型的ToString方法被重写,它返回的是ConsoleLogScope对象被创建时指定的State对象(state参数)的字符串形式(调用ToString方法的返回值)。

3: Debug.Assert(@H_404_24@"Scope1" == ConsoleLogScope.Current.ToString());
   6:         Debug.Assert(@H_404_24@"Scope2" == ConsoleLogScope.Current.ToString());
   8:     Debug.Assert(@H_404_24@   9: }

再次将我们目光从ConsoleLogScope转移到ConsoleLogger上面,当ConsoleLogger的BeginScope方法调用的时候,它会将自己的名称(Name属性)和指定的State对象作为参数调用ConsoleLogScope的静态方法Push并返回一个disposableScope对象。只要我们没有调用disposableScope的dispose方法,就可以通过调用ConsoleLogScope的静态属性Current得到当前日志上下文,它的ToString方法和指定的State对象的ToString方法返回相同的字符串。

4: {
   7: }

认情况下,ConsoleLogger针对日志上下文范围的支持关闭的,我们需要利用它的IncludeScopes属性开启这个特性。如果ConsoleLogger的Log方法是在某个日志上下文范围中被调用,它会采用如下的格式输出日志消息,其中{State}表示调用BeginScope方法传入的State对象。

3: {Message}

比如在一个处理订购订单的应用场景中,如果需要将针对同一笔订单的多条日志消息关联在一起,我们就可以针对订单的ID创建一个日志上下文范围,并在此上下文范围内调用Logger对象的Log方法进行日志记录,那么订单ID将会包含在每条写入的日志消息中。

2: .AddLogging()
   4:     .GetService<ILoggerFactory>()
   6:     .CreateLogger(@H_404_24@"Ordering");
   9: {
  11:     logger.LogError(@H_404_24@"商品ID录入错误(商品ID: {0})",1)">"9787121235368");
   1: warn: Ordering[0]
   3:       商品库存不足(商品ID: 9787121237812,当前库存:20,订购数量:50)
   5:       => Order ID: 20160520001
class ConsoleLoggerProvider : ILoggerProvider,Idisposable
public ConsoleLoggerProvider(Func<@H_404_24@public ConsoleLoggerProvider(IConsoleLoggerSettings settings);
public ILogger CreateLogger(@H_404_24@string name);
   8: }

ConsoleLoggerProvider还具有另一个构造函数重载,它接受一个IConsoleLoggerSettings接口的参数,该接口表示为创建的ConsoleLogger而指定的配置。配置的目的是为了指导ConsoleLoggerProvider创建正确的ConsoleLogger,所以它最终还是为了提供日志过滤条件和是否将日志写入操作纳入当前上下文范围的布尔值,前者体现为TryGetSwitch方法,后者对应其IncludeScopes属性。由于配置数据具有不同的载体,或者具有不同来源,比如文件数据库和环境变量等,所以需要考虑应用于配置源的同步问题。IConsoleLoggerSettings接口的Changetoken属性提供了一个向应用通知配置源发生改变的令牌,Reload则在配置源发生改变时从新加载配置。

bool IncludeScopes { get; }
   6:     IConsoleLoggerSettings Reload();
   8: }

在NuGet包“Microsoft.Extensions.Logging.Console”中提供了两个实现了IConsoleLoggerSettings接口的类型,其中一个是具有如下定义的ConsoleLoggerSettings。ConsoleLoggerSettings的实现方式非常简单,它通过一个字典对象来保存日志类型与最低等级之间的映射,并利用它来实现TryGetSwitch方法。由于配置原数据体现为一个内存变量,所以无需考虑配置的同步问题,所以ConsoleLoggerSettings的Reload方法的返回值是它自己,Changetoken被定义成简单的可读写的属性

bool IncludeScopes { get; set; }
public IDictionary<404_24@new Dictionary<public IConsoleLoggerSettings Reload() => @H_404_24@this;
   9: }

IConsoleLoggerSettings接口的另一个实现者ConfigurationConsoleLoggerSettings则直接采用真正的配置。如下面的代码片段所示,一个ConfigurationConsoleLoggerSettings对象实际上是对一个Configuration对象的封装。它的IncludeScopes属性和TryGetSwitch方法的返回值都来源于Configuration对象承载的配置。ConfigurationConsoleLoggerSettings的同步直接利用配置模型的同步机制来实现,具体来说它的Changetoken属性也是直接由这个Configuration对象提供(GetChangetoken方法返回的Changetoken)。

public IChangetoken Changetoken { get; }
public ConfigurationConsoleLoggerSettings(IConfiguration configuration);
out LogLevel level);
   1: {
"LogLevel":{
   6:      …
   1: ConsoleLoggerProvider loggerProvider = @H_404_24@new ConsoleLoggerProvider(@H_404_24@new ConsoleLoggerSettings());
static @H_404_24@class ConsoleLoggerExtensions
this ILoggerFactory factory,IConfiguration configuration);
  10:     @H_404_24@  11: }

接下来通过一个实例来演示通过指定一个Configuration对象来调用扩展方法AddConsole来创建并注册ConsoleLoggerProvider。我们将ConsoleLogger的相关配置按照如下的形式定义在一个JSON文件中,并将其命名为logging.json。通过这个配置,我们要求创建的ConsoleLogger忽略当前的日志上下文范围,并为日志类型“App”设置的最低的等级“Warning”。

"App": @H_404_24@"Warning"
   6: }

我们在project.json文件添加了针对如下几个NuGet包的依赖。为了在项目编译时自动配置文件logging.json拷贝到输出目录下,我们将这个配置文件名设置为配置项“buildOptions/copyToOutput”的值。

"buildOptions": {
"copyToOutput": @H_404_24@"logging.json"
"Microsoft.Extensions.DependencyInjection"    : @H_404_24@"1.0.0",1)">"Microsoft.Extensions.Logging"                : @H_404_24@"Microsoft.Extensions.Logging.Console"        : @H_404_24@"Microsoft.Extensions.Configuration.Json"     : @H_404_24@"System.Text.Encoding.CodePages"              : @H_404_24@"4.0.1",1)">  14:     ...
  16: }

我们在作为入口的Main方法中编写了下面一段程序。如下面的代码片段所示,我们通过加载这个logging.json文件创建了一个Configuration对象。在成功创建LoggerFactory后,我们将Configuration对象作为参数调用扩展方法AddConsole创建一个ConsoleLoggerProvider并注册到LoggerFactory之上。我们最终利用LoggerFactory创建了一个Logger对象,并利用后者记录三条日志。Logger采用的类型为“App”,这与配置文件设置的类型一致。

2: 
   4:     .AddJsonFile(@H_404_24@"logging.json")
   7: ILogger logger = @H_404_24@   8:     .AddLogging()
  10:     .GetService<ILoggerFactory>()
  12:     .CreateLogger(@H_404_24@"App");
  14: @H_404_24@int eventId = 3721;
  16: logger.LogWarning(eventId,1)">"并发量接近上限({maximum}) ",200);