.NET Core的日志[5]:利用TraceSource写日志

时间:2022-08-26 02:32:25

从微软推出第一个版本的.NET Framework的时候,就在“System.Diagnostics”命名空间中提供了Debug和Trace两个类帮助我们完成针对调试和跟踪信息的日志记录。在.NET Framework 2.0中,微软引入了TraceSource并对跟踪日志系统进行了优化,优化后的跟踪日志系统在.NET Core中又经过了相应的简化。.NET Core的日志模型借助TraceSourceLoggerProvider实现对TraceSource的整合,在正式介绍这个Logger之前,我们先来认识一下TraceSource跟踪日志系统中的三个核心对象。[ 本文已经同步到《ASP.NET Core框架揭秘》之中]

目录
一、基于TraceSource的追踪日志系统
二、TraceSourceLogger
三、TraceSourceLoggerProvider

一、基于TraceSource的追踪日志系统

对于这个基于TraceSource的跟踪日志系统来说,除了TraceSource之外,它还具有额外连个核心的对象,它们分别是TraceListener和SourceSwitch,三者之间的关系如下图所示。日志消息的写入实现在TraceListener上,我们可以将一组TraceListener注册到某个TraceSource之上。当我们利用TraceSource记录某条跟踪日志时,日志消息会分发给注册的每一个TraceListener并由它们将日志消息写到对应的目的地。每个TraceSource都具有一个SourceSwitch,后者起到了日志过滤的作用。具体来说,SourceSwitch定义了相应的过滤条件来帮助TraceSource决定是否应该将跟踪日志分发给TraceListener,如果指定的日志消息不满足过滤条件,TraceSource将不会进行任何实质性的日志记录工作。

.NET Core的日志[5]:利用TraceSource写日志

如下所示的是TraceSource的定义。每一个TraceSource都具有一个名称,它一般代表写入跟踪日志的应用程序、服务或者组件的名称。我们可以调用它的三组Trace方法(TraceData、TraceEvent和TraceInformation)来记录跟踪日志。由于这些方法都标注了一个ConditionaleAttribute特性并将条件编译符“TRACE”,所以针对这些方法的调用只有在针对Trace模式编译的应用中才是有效的。

   1: public class TraceSource

   2: {

   3:     public TraceListenerCollection Listeners { get; }

   4:     public string             Name { get; }

   5:     public SourceSwitch         Switch { get; set; }

   6:  

   7:     public TraceSource(string name);

   8:     public TraceSource(string name, SourceLevels defaultLevel);

   9:    

  10:     [Conditional("TRACE")]

  11:     public void TraceData(TraceEventType eventType, int id, object data);

  12:     [Conditional("TRACE")]

  13:     public void TraceData(TraceEventType eventType, int id, params object[] data);

  14:  

  15:     [Conditional("TRACE")]

  16:     public void TraceEvent(TraceEventType eventType, int id);

  17:     [Conditional("TRACE")]

  18:     public void TraceEvent(TraceEventType eventType, int id, string message);

  19:     [Conditional("TRACE")]

  20:      public void TraceEvent(TraceEventType eventType, int id, string format, params object[] args);

  21:  

  22:     [Conditional("TRACE")]

  23:     public void TraceInformation(string message);

  24:     [Conditional("TRACE")]

  25:     public void TraceInformation(string format, params object[] args);  

  26: }

通过TraceData、TraceEvent和TraceInformation这三个方法记录的跟踪日志都具有一个通过枚举类型TraceEventType表示的事件类型,它相当于前面提到的日志等级。TraceEventType的这些枚举项的值越小意味着等级越高,定义日志等级的LogLevel则于此相反。在调用TraceData和TraceEvent方法时,我们需要显式地为写入的跟踪日志指定事件类型,而TraceInformation方法则默认使用Information类型。

   1: public enum TraceEventType

   2: {

   3:     Critical         = 1,

   4:     Error            = 2,

   5:     Warning          = 4,

   6:     Information      = 8,

   7:     Verbose          = 16,

   8: }

与TraceEventType枚举对应的还具有另一个名为SourceLevels的枚举,除了包含五种具体事件类型之外,还具有额外两个选项All和Off,该枚举对象被SourceSwitch用来过滤日志。在调用构造函数创建TraceSource的时候,我们可以指定一个SourceLevels枚举值作为默认的等级。如果这个等级未作显式设置,创建的TraceSource采用的等级为Off,这意味着默认情况下针对追踪日志的记录是禁止的。

   1: [Flags]

   2: public enum SourceLevels

   3: {

   4:     All             = -1,

   5:     Off             = 0,

   6:     Critical        = 1,

   7:     Error           = 3,

   8:     Warning         = 7

   9:     Information     = 15,

  10:     Verbose         = 31

  11: }

我们创建的TraceSource是指定(或者默认设置)的表示日志等级的SourceLevels枚举会用来创建一个具有如下定义的SourceSwitch对象,TraceSource的Switch属性返回的就是这么一个对象。顾名思义,SourceSwitch是一个开关,它利用ShouldTrace方法决定了针对某种类型的跟踪日志的写入操作是应该开启还是关闭。如下面的代码片段所示,ShouldTrace方法返回的结果是根据通过Level属性返回的跟踪日志等级计算出来的,表示跟踪日志等级的SourceLevels枚举正是最初正是由TraceSource在初始化时提供的。

   1: public class SourceSwitch : Switch

   2: {

   3:     public SourceLevels Level {get;set;}

   4:  

   5:     public SourceSwitch(string name);

   6:     public SourceSwitch(string displayName, string defaultSwitchValue);

   7:  

   8:     public bool ShouldTrace(TraceEventType eventType)

   9:     {

  10:         return ((base.SwitchSetting & eventType) > 0);

  11:     }    

  12: }

TraceSource对象自身并不负责针对跟踪日志的写入,它仅仅将日志的写入请求分发给注册的TraceListener并委托它们来完成写日志的功能。这些注册到TraceSource上的TraceListenter被保存到由它的Listeners属性返回的集合对象中。所有的TraceListener都拍生于如下这个抽象的TraceListener类型,它定义了如下两组TraceData和TraceEvent方法。当我们调用TraceSource的TraceData、TraceEvent和TraceInformation方法时,如果通过SourceSwitch判断应该开启针对当前跟踪日志的写入功能,那么注册的TraceListener的TraceData或者TraceEvent方法将会被调用。

   1: public abstract class TraceListener : IDisposable

   2: {

   3:     ...

   4:     public virtual void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, object data);

   5:     public virtual void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, params object[] data);

   6:  

   7:     public virtual void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id);

   8:     public virtual void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message);

   9:     public virtual void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string format, params object[] args);

  10: }

接下来我们通过一个简单的控制台应用来演示如何创建一个TraceSource并使用它来记录追踪日志。由于TraceSource定义在“System.Diagnostics.TraceSource”这个NuGet包中,我们需要在project.json文件中需要按照如下的方式添加针对这个NuGet包的依赖。和前面演示的实例一样,为了提供针对中文编码的支持,我们不得不添加针对“System.Text.Encoding.CodePages”这个NuGet包的依赖。

   1: {

   2:   ...

   3:   "dependencies": {

   4:     "System.Diagnostics.TraceSource": "4.0.0",    

   5:     "System.Text.Encoding.CodePages": "4.0.1"

   6:   }

   7: }

由于TraceSource总是利用注册在它上面的TraceListener来完成写日志的工作,所以我们按照如下的方式自定义了ConsoleTraceListener。顾名思义,ConsoleTraceListener旨在将分发给它的追踪日志输出到控制台上。如下面的代码片段所示,这个ConsoleTraceListener仅仅重写了Write和WriteLine方法,它们调用定义在Console类型上的同名方法将格式化好的日志消息输出到控制台上。

   1: public class ConsoleTraceListener : TraceListener

   2: {

   3:     public override void Write(string message) => Console.Write(message);

   4:     public override void WriteLine(string message) => Console.WriteLine(message);

   5: }

我们在作为程序入口的Main方法中创建了一个TraceSource对象。在调用构造函数的时候,除了指定TraceSource的名称(“Program”)之外,我们还设置了一个默认的追踪日志等级(Warning)。接下来我们创建了一个ConsoleTraceListener对象并将其注册到TraceSource对象上。在此之后,我们调用TraceSource的TraceEvent方法记录了三条追踪日志,它们采用的追踪事件类型分别是Information、Warining和Error。

   1: public class Program

   2: {

   3:     public static void Main(string[] args)

   4:     {

   5:         //注册EncodingProvider实现对中文编码的支持

   6:         Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

   7:  

   8:         TraceSource traceSource = new TraceSource(nameof(Program), SourceLevels.Warning);

   9:         traceSource.Listeners.Add(new ConsoleTraceListener());

  10:  

  11:         int eventId = 3721;

  12:         traceSource.TraceEvent(TraceEventType.Information, eventId, "升级到最新.NET Core版本({0})", "1.0.0");

  13:         traceSource.TraceEvent(TraceEventType.Warning, eventId, "并发量接近上限({0}) ", 200);

  14:         traceSource.TraceEvent(TraceEventType.Error, eventId, "数据库连接失败(数据库:{0},用户名:{1})", "TestDb", "sa");

  15:     }

  16: }

该程序运行之后,我们利用TraceSource记录的追踪日志将会被注册的ConsoleTraceListener按照如下图所示的形式输出到控制台上。由于我们在创建TraceSource的时候指定了一个默认的追踪日志等级Warning,所以只有不低于这个等级的两条日志才会显示在控制台上。

.NET Core的日志[5]:利用TraceSource写日志

二、TraceSourceLogger

.NET Core的日志模型利用一个定义在NuGet包“Microsoft.Extensions.Logging.TraceSource”中的TraceSourceLogger类型实现与TraceSource跟踪日志系统的整合。从如下面的代码片段我们不难看出,一个TraceSourceLogger对象实际上就是对一个TraceSource对象的封装,在实现的Log<State>方法中,它会调用TraceSource的TraceEvent方法来完成针对日志消息的写入工作。

   1: public class TraceSourceLogger : ILogger

   2: {

   3:     public TraceSourceLogger(TraceSource traceSource);

   4:     public IDisposable BeginScope<TState>(TState state);

   5:     public bool IsEnabled(LogLevel logLevel);

   6:     public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter);

   7: }

当我们调用TraceSource的TraceEvent方法来写追踪日志的时候,需要指定追踪日志的事件类型,该类型由提供的日志等级来决定,下表展示了日志等级与跟踪事件类型之间的映射关系很简单。 由于TraceSource通过调用其SourceSwitch的ShouldTrace方法来决定是否真正需要写入当前分发的追踪日志消息,所以当TraceSourceLogger的IsEnabled方法被调用的时候,它也会按照这样的映射关系将指定的日志等级转换成追踪事件类型,并将其作为参数调用这个ShouldTrace方法,这个方法的返回值就是IsEnabled方法的返回值。

日志等级

跟踪事件类型

Trace

Verbose

Debug

Verbose

Information

Information

Warning

Warning

Error

Error

Critical

Critical

TraceSourceLogger的BeginScope<TState>方法会返回一个TraceSourceScope对象,虽然这是一个共有的类型,但是这个对象并不做任何作用域的控制,其自身也不携带任何关于当前日志上下文的信息,所以TraceSourceLogger和前面介绍的DebugLogger和EventLogLogger一样,其实都不提供针对日志上下文的支持。

三、TraceSourceLoggerProvider

TraceSourceLogger对应的LoggerProvider类型为TraceSourceLoggerProvider。如下面的代码片段所示,当我们创建一个TraceSourceLoggerProvider对象时需要提供一个SourceSwitch和TraceListener对象(可选)。在实现的CreateLogger方法中,TraceSourceLoggerProvider会根据指定的名称创建一个TraceSource对象,它将采用初始化时指定的SourceSwitch,预先指定的TraceListener也会注册到这个TraceSource对象上,CreateLogger方法最终返回的将是根据这个TraceSource创建的TraceSourceLogger。

   1: public class TraceSourceLoggerProvider : ILoggerProvider

   2: {   

   3:     public TraceSourceLoggerProvider(SourceSwitch rootSourceSwitch);

   4:     public TraceSourceLoggerProvider(SourceSwitch rootSourceSwitch, TraceListener rootTraceListener);

   5:  

   6:     public ILogger CreateLogger(string name);

   7:     public void Dispose();   

   8: }

值得一提的是TraceSourceLoggerProvider并不会在CreateLogger方法中频繁地创建TraceSource对象,而是选择将创建的TraceSource会根据指定的名称被缓存起来。所以当CreateLogger方法被调用的时候,TraceSourceLoggerProvider会根据指定的名称查看缓存中是否存在一个现成的TraceSource,如果存在则直接根据它创建返回的TraceSourceLogger。只有在确定同名的TraceSource不曾创建的情况下,新的TraceSource才会被真正创建出来。我们可以调用如下两个扩展方法AddTraceSource根据指定的SourceSwitch(或者它的名称)和TraceListener来创建TraceSourceLoggerProvider并将其注册到指定的LoggerFactory上。

   1: public static class TraceSourceFactoryExtensions

   2: {

   3:     public static ILoggerFactory AddTraceSource(this ILoggerFactory factory, SourceSwitch sourceSwitch, TraceListener listener);

   4:     public static ILoggerFactory AddTraceSource(this ILoggerFactory factory, string switchName, TraceListener listener);

   5: }

接下来我们通过一个简单的实例来演示针对DebugLogger的日志记录。我们创建一个空的控制台应用,在添加必要的依赖之后,我们在Main方法中编写了如下一段程序。如下面的代码片段所示,我们采用依赖注入的方式创建了一个LoggerFactory,并调用扩展方法AddTraceSource方法创建并注册了一个TraceSourceLoggerProvider对象。在利用LoggerFactory创建出Logger对象之后,我们利用后者记录了三条日志消息。

   1: public class Program

   2: {

   3:     public static void Main(string[] args)

   4:     {

   5:         //注册EncodingProvider实现对中文编码的支持

   6:         Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

   7:  

   8:         ILogger logger = new ServiceCollection()

   9:                 .AddLogging()

  10:                 .BuildServiceProvider()

  11:                 .GetService<ILoggerFactory>()

  12:                 .AddTraceSource(new SourceSwitch(nameof(Program), "Warning"), new ConsoleTraceListener())

  13:                 .CreateLogger<Program>();

  14:  

  15:  

  16:         int eventId = 3721;

  17:  

  18:         logger.LogInformation(eventId, "升级到最新.NET Core版本({version})", "1.0.0");

  19:         logger.LogWarning(eventId, "并发量接近上限({maximum}) ", 200);

  20:         logger.LogError(eventId, "数据库连接失败(数据库:{Database},用户名:{User})", "TestDb", "sa");

  21:     }

  22: }

我们在调用扩展方法AddTraceSource创建并注册TraceSourceLoggerProvider是指定了一个针对Warning等级的SourceSwitch,而指定的TraceListener是一个自定义的ConsoleTraceListener,所以只有两条等级不低于Warning的日志消息会被这个ConsoleTraceListener按照上图所示的形式输出到控制台上。


.NET Core的日志[1]:采用统一的模式记录日志
.NET Core的日志[2]:将日志写入控制台
.NET Core的日志[3]:将日志写入Debug窗口
.NET Core的日志[4]:利用EventLog写日志
.NET Core的日志[5]:利用TraceSource写日志