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

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

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

三、DebugLoggerProvider

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

   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<string, LogLevel, bool>对象,也可以指定一个最低的日志等级。如果这两者都没有指定,从给出的代码片段可以看出该方法会默认将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<ILoggerFactory>()
   9:             .AddDebug()
  10:             .CreateLogger<Program>();
  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<TState, Exception, string>对象。值得一提是作为格式化器的这个委托对象已经考虑到了针对异常消息的格式化。

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏编程

Java并发编程实战

在我们的实际应用当中可能经常会遇到这样一个场景:多个线程读或者、写相同的数据,访问相同的文件等等。对于这种情况如果我们不加以控制,是非常容易导致错误的。在jav...

2575
来自专栏大内老A

WCF技术剖析之十一:异步操作在WCF中的应用(下篇)

说完了客户端的异步服务调用(参阅WCF技术剖析之十一:异步操作在WCF中的应用(上篇)),我们在来谈谈服务端如何通过异步的方式为服务提供实现。在定义服务契约的时...

2109
来自专栏大内老A

如何在ASP.NET Core应用中实现与第三方IoC/DI框架的整合?

我们知道整个ASP.NET Core建立在以ServiceCollection/ServiceProvider为核心的DI框架上,它甚至提供了扩展点使我们可以与...

2015
来自专栏cs

c++那些事儿7.0 I/O流,文件操作

知识点综述: ---- C++ I/O: 在iostream头文件中定义 istream //通用输入流和其它输入流基类。 ...

3547
来自专栏开发与安全

linux网络编程之socket(六):利用recv和readn函数实现readline函数

在前面的文章中,我们为了避免粘包问题,实现了一个readn函数读取固定字节的数据。如果应用层协议的各字段长度固定,用readn来读是非常方便的。例如设计一种客户...

2560
来自专栏技术墨客

Hazelcast集群服务(4)——分布式Map

    在第一篇介绍Hazelcast的文章已经提到,Hazelcast为Java中绝大部分数据结构提供了分布式实现。我们常用的Map、List、Queue等数...

2683
来自专栏高性能服务器开发

我是一个线程(节选)

多线程编程在现代软件开发中是如此的重要,以至于熟练使用多线程编程是一名合格的后台开发人员的基本功,注意,我这里用的是基本功一词。它是如此的重要,所以您应该掌握它...

1543
来自专栏Coding01

看 Laravel 源代码了解 Container

自从上文《看 Laravel 源代码了解 ServiceProvider 的加载》,我们知道 Application (or Container) 充当 Lar...

3595
来自专栏微信公众号:Java团长

各大公司Java后端开发面试题总结

ThreadLocal(线程变量副本) Synchronized实现内存共享,ThreadLocal为每个线程维护一个本地变量。 采用空间换时间,它用于线程间的...

1311
来自专栏MasiMaro 的技术博文

VC++ 崩溃处理以及打印调用堆栈

一般当程序发生异常时,用户代码停止执行,并将CPU的控制权转交给操作系统,操作系统接到控制权后,将当前线程的环境保存到结构体CONTEXT中,然后查找针对此异常...

3714

扫码关注云+社区

领取腾讯云代金券