.NET Core的日志[3]:将日志写入Debug窗口


定义在NuGet包“Microsoft.Extensions.Logging.Debug”中的DebugLogger会直接调用Debug的WriteLine方法来写入分发给它的日志消息。如果需要使用DebugLogger来写日志,我们需要将它的提供者DebugLoggerProvider注册到LoggerFactory上。由于定义在Debug类型中的所有方法都是针对Debug编译模式的,所以在只有针对Debug模式编译的应用中使用DebugLogger才有意义。这里将的“Debug编译模式”涉及到一个叫做“条件编译”的话题。 本文已经同步到《ASP.NET Core框架揭秘》之中]

目录
一、Debug类型与条件编译
二、DebugLogger
三、DebugLoggerProvider

一、Debug类型与条件编译

DebugLogger适用于.NET Framework和.NET Core应用,我们说DebugLogger最终是通过调用Debug类型的静态方法WriteLine来写入分发给它的日志消息,但是使用的这个Debug类型在.NET Framework和.NET Core应用下其实是两个完全不同的类型。针对.NET Framework的Debug类型定义在程序集“System.dll”下,而针对.NET Core的Debug类型则承载于“System.Diagnostics.Debug”这个NuGet包中,这两个Debug方法具有不同的API定义。

这两个Debug类型针对日志的写入机制也不尽相同,针对.NET Framework的Debug类型定会利用注册到Debug.Listeners属性TraceListener来写日志,默认注册的DefaultTraceListener会通过调用Win32函数OutputDebugString将格式化的日志消息输出给Debug监视器(Debug Monitor)。对于针对针对.NET Core的Debug类型来说,它针对不同的平台具有不同的实现,针对Windows平台下日志消息依然是通过调用OutputDebugString这Win32函数来写入的。

虽然两个Debug类型在API定义和写入日志的实现都不同,但是对于被DebugLogger用来写日志的WriteLine方法来说,它们都具有如下所示的定义方式。该方法具有两个参数,分别代表写入日志的文本消息和类型。我们可以看到这个方法上标注了一个类型为ConditionalAttribute的特性,它具有一个值为“DEBUG”的参数。这个ConditionalAttribute特性就与我们所说的“条件编译”有关。

   1: public static class Debug
   2: {
   3:     [Conditional("DEBUG")]
   4:     public static void WriteLine(string message,string category);
   5: }

所谓的“条件编译”,就是说编译器在进行编译的时候会根据指定的条件来过滤参与编译的源代码,这个源代码过滤条件是在编译时指定的符号化的字符串,我们称它为“条件编译符(Conditional Compilation Symbol)”,上面代码片段中作为ConditionalAttribute特性参数的“DEBUG”就是条件编译符。如果我们使用Visual Studio作为IDE,我们可以利用它以可视化的方式来为某个的项目设置一个或者多个就是条件编译符。我们只需要右击某个项目并在弹出的上下文菜单中选择“属性(Properties)”,然后按照如下图所示方式在显示的项目属性窗口中选择“生成(Build)”选项卡。

如图8所示,我们可以定义任意字符串作为条件编译符(比如“UAT”,“SIT”)。除此之外,Visual Studio还为我们预设了“DEBUG”和“TRACE”这两个常用的条件编译符,如果需要我们只需要选择相应的复选框(“Define DEBUG/TRACE constant”)即可。我们通过这种方法设置的条件编译符最终会作为编译选项以如下的方式写入到project.json文件中,具体的配置项目为“buildOptions/define”,换句话说,我们完全可以直接编辑project.json文件的方式来定义条件编译符。

   1: {
   2:   ...
   3:   "buildOptions": {
   4:     "define": [ "DEBUG", "TRACE", "UAT, SIT" ]
   5:   }
   6: }

从某种意义来说,条件编译符实际上是为应用定义相应的“部署场景”,比如我们在上边定义的条件编译符“UAT”和“SIT”就是针对两种不同类型(用户接收测试和系统集成测试)的测试部署场景。如果我们需要编写针对具有某种部署场景的程序,可以采用预编译指令“#if/#endif”来实现。如果编译器在编译如下一段代码的时候,只有指定的条件编译符包含“DEBUG”的情况下,调用WriteDebug方法的这段代码才会参与编译,否则这段代码将直接被忽略。

   1: #if DEBUG
   2:     WriteDebug(message);
   3: #endif

完全采用预编译指令“#if/#endif”来编写针对具体某个条件编译符的代码其实是很繁琐的。如果某个方法总是针对具体某个条件编译符,我们可以直接在这样的方法上标注一个ConditionalAttribute特性,并将对应的条件编译符作为其参数即可。比如上面这个WriteDebug方法就可以采用如下的方式来定义,它可以作为一个普通的方法来调用,而无需再使用任何预编译指令。

   1: [Conditional(“DEBUG”)]
   2: public static void WriteBug(string message);

编译器在编译我们的程序的时候,如果程序中调用了某个标注了ConditionalAttribute特性的方法并且指定的条件编译符与当前不一致,针对该方法调用的代码将被自动忽略。定义在Debug类型上的WriteLine方法上就标注了这么一个ConditionalAttribute特性,指定的编译符为“DEBUG”,大家应该知道为什么DebugLogger为什么只有针对Debug模式编译生成的应用才后意义了吧。

二、DebugLogger

在了解了Debug类型和条件编译的背景知识后,我们来正式认识一下DebugLogger类型。我们采用如下一段现对简介的代码模拟了DebugLogger的定义。当我们调用构造函数创建一个DebugLogger对象的时候需要指定Logger的名称和进行日志过滤的Func对象,后者是可选的。DebugLogger调用Debug的WriteLine方法来进行日志写入体现在它的Log方法中,写入的日志消息将DebugLogger的名称作为日志类型。

   1: public class DebugLogger : ILogger
   2: {
   3:     private readonly Func<string, LogLevel, bool> _filter;
   4:     private readonly string _name;
   5:  
   6:     public DebugLogger(string name, Func<string, LogLevel, bool> filter)
   7:     {
   8:         _name = string.IsNullOrEmpty(name) ? "DebugLogger" : name;
   9:         _filter = filter?? ((cate, level) => true);
  10:     }
  11:  
  12:     public DebugLogger(string name) : this(name, null)
  13:     {}
  14:  
  15:  
  16:     public IDisposable BeginScope(TState state)
  17:     {
  18:         return NoopDisposable.Instance;
  19:     }
  20:  
  21:     public bool IsEnabled(LogLevel logLevel)
  22:     {
  23:         return Debugger.IsAttached && _filter(_name, logLevel);
  24:     }
  25:  
  26:     public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Funcstring> formatter)
  27:     {
  28:         if (this.IsEnabled(logLevel))
  29:         {
  30:             string message = formatter(state, exception);
  31:             message = $"{logLevel}: {message}";
  32:             if (exception != null)
  33:             {
  34:                 message = $"{message}{Environment.NewLine}{Environment.NewLine}{exception}";
  35:             }
  36:             Debug.WriteLine(message, _name);
  37:         }
  38:     }
  39:  
  40:     private class NoopDisposable : IDisposable
  41:     {
  42:         public static DebugLogger.NoopDisposable Instance = new DebugLogger.NoopDisposable();
  43:         public void Dispose()
  44:         {}
  45:     }
  46: }

上面这段代码和体现了DebugLogger进行日志记录的一些细节特性:

  • 如果调用构造函数指定的名称为Null或者是一个空字符串,创建的DebugLogger对象将使用它的类型名(“DebugLogger”)来命名。如果作为日志过滤器的Func对象没有显式指定,意味着不需要对日志进行过滤。
  • DebugLogger并不支持日志上下文,所以它的BeginScope方法返回的NoopDisposable对象并承载任何上下文信息,这也是DebugLogger的构造函数不像ConsoleLogger一样具有一个includeScope参数的原因。
  • DebugLogger的IsEanbled方法不仅仅利用构造时指定的作为日志过滤器的Func对象来决定是否真正写入日志,还需要考虑调试器是否附加到当前进程(Debugger.IsAttached),只有这个两个条件都满足的情况下,这个方法才会返回True。
  • DebugLogger的Log方法在真正写入日志的过程中,它会利用指定的作为格式化器的Func对象将承载原始日志信息的对象和异常(对应参数state和exception)格式成一个完整的字符串作为最终写入的日志消息。但是在指定的Exception对象不为Null的情况下,它又会在这个格式好的日志消息上附加上异常信息,这其实是不太合理的。

三、DebugLoggerProvider

DebugLogger对应的LoggerProvider是一个DebugLoggerProvider对象。如下面的代码片段所示,DebugLoggerProvider提供DebugLogger的逻辑非常简单,它只需要在实现的CreateLogger方法中调用构造函数创建并返回一个DebugLogger对象即可,提供的作为日志过滤器的Func对象在自身的构造函数中由对应的参数指定。

   1: public class DebugLoggerProvider : ILoggerProvider, IDisposable
   2: {
   3:     private readonly Func<string, LogLevel, bool> _filter;
   4:     public DebugLoggerProvider(Func<string, LogLevel, bool> filter)
   5:     {
   6:         _filter = filter;
   7:     }
   8:  
   9:     public ILogger CreateLogger(string name)
  10:     {
  11:         return new DebugLogger(name, _filter);
  12:     }
  13:  
  14:     public void Dispose()
  15:     {}
  16: }

针对DebugLoggerProvider的注册可以通过如下三个针对ILoggerFactory接口的扩展方法AddDebug来完成。我们调用这些方法时可以为注册的DebugLoggerProvider指定作为日志过滤器的Func对象,也可以指定一个最低的日志等级。如果这两者都没有指定,从给出的代码片段可以看出该方法会默认将Information作为最低日志等级。也就是说,当我们调用AddDebug方法时如果没有指定任何日志过滤条件,等级为Debug的日志消息并不会被记录下来,这一点也是我们个人觉得不太合理的地方。

   1: public static class DebugLoggerFactoryExtensions
   2: {
   3:     public static ILoggerFactory AddDebug(this ILoggerFactory factory)
   4:     {
   5:         return factory.AddDebug(LogLevel.Information);
   6:     }
   7:  
   8:     public static ILoggerFactory AddDebug(this ILoggerFactory factory, LogLevel minLevel)
   9:     {
  10:         return factory.AddDebug((cate, level) => level >= minLevel);
  11:     }
  12:  
  13:     public static ILoggerFactory AddDebug(this ILoggerFactory factory, Func<string, LogLevel, bool> filter)
  14:     {
  15:         factory.AddProvider(new DebugLoggerProvider(filter));
  16:         return factory;
  17:     }
  18: }

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

   1: public class Program
   2: {
   3:     public static void Main(string[] args)
   4:     {
   5:         ILogger logger = new ServiceCollection()
   6:             .AddLogging()
   7:             .BuildServiceProvider()
   8:             .GetService()
   9:             .AddDebug()
  10:             .CreateLogger();
  11:  
  12:         logger.LogDebug("这是一个等级为Debug的日志");
  13:         logger.LogInformation("这是一个等级为Information的日志");
  14:         logger.Log(LogLevel.Error, 3721, "这是一个等级为Error的日志",new FileNotFoundException("目标文件不存在"),
  15:         (state, exception) => $"{state}{Environment.NewLine}{exception}");
  16:     }
  17: }

记录的三条日志具有不同的等级(分别为Debug、Information和Error)。第三条日志的记录是调用Logger对象的Log方法实现的,我们在调用该方法时指定了所有的承载日志消息所有的信息(日志等级、事件ID、日志原始消息和异常)和作为格式化器的Func对象。值得一提是作为格式化器的这个委托对象已经考虑到了针对异常消息的格式化。

现在直接利用Visual Studio在Debug模式下编译并运行这个程序,我们会在输出窗口中看到写入的日志。如下图所示,Visual Studio的输出窗口只显示了两条等级分别为Information和Error的日志,等级为Debug的日志并没有被记录下来。对于记录的第二条日志,我们发现异常的信息被重复记录,前者是的内容是源于我们指定的格式化器,后者则是DebugConsoleLogger的Log方法自行附加上去的。

 


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