.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<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方法自行附加上去的。

 



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

作者:蒋金楠
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文链接

时间: 2024-08-01 10:28:13

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

.NET Core的日志[4]:将日志写入EventLog

面向Windows的编程人员应该不会对Event Log感到陌生,以至于很多人提到日志,首先想到的就是EventLog.EventLog不仅仅记录了Windows系统自身针对各种事件的日志,我们的应用也可以利用提供的API将日志消息写到EventLog中.与EventLog相关的API都定义在System.Diagnostics.EventLog这个类型中,我们不仅仅可以利用它读取.写入和删除日志,还可以使用它来创建和删除Event Source..NET Core的日志模型利用EventLog

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

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

日志系列--程序日志处理挑战与方案

程序日志(AppLog)有什么特点? 内容最全:程序日志是由程序员给出,在重要的地点.变量数值以及异常都会有记录,可以说线上90%以上Bug都是依靠程序日志输出定位到 格式比较随意:代码往往经过不同人开发,每个程序员都有自己爱好的格式,一般非常难进行统一,并且引入的一些第三方库的日志风格也不太一样 有一定共性:虽然格式随意,但一般都会有一些共性的地方,例如对Log4J日志而言,会有如下几个字段是必须的: 时间 级别(Level) 所在文件或类(file or class) 行数(Line Num

Mysql日志文件和日志类型介绍_Mysql

日志文件类型 MySQL有几个不同的日志文件,可以帮助你找出mysqld内部发生的事情: 日志文件 记入文件中的信息类型 错误日志 记录启动.运行或停止mysqld时出现的问题. 查询日志 记录建立的客户端连接和执行的语句. 更新日志 记录更改数据的语句.不赞成使用该日志. 二进制日志 记录所有更改数据的语句.还用于复制. 慢日志 记录所有执行时间超过long_query_time秒的所有查询或不使用索引的查询. 默认情况下,所有日志创建于mysqld数据目录中.通过刷新日志,你可以强制 mys

Apache日志查看与日志格式配置参数详解

一.定义日志格式 很久以前,日志文件只有一种格式,这就是"公共格式",许多人已经习惯于使用这种格式.随后出现了定制日志格式,而且看起来定制日志格式更很受欢迎,即使公共日志格式本身也重新用定制日志格式定义.本文介绍的就是如何随心所欲地定制日志文件的格式.如何让日志文件记录自己想要的信息. 定制日志文件的格式涉及到两个指令,即LogFormat指令和CustomLog指令,默认httpd.conf文件提供了关于这两个指令的几个示例. LogFormat指令定义格式并为格式指定一个名字,以后

解析Nginx中的日志模块及日志基本的初始化和过滤配置_nginx

无论在任何项目中,日志都是一个非常重要的模块,无论是问题定位还是日常信息的管理,都离不开他 在nginx中,ngx_errlog_module模块专门用于处理nginx日志信息,是nginx的core模块之一 在 main 函数中,时间初始化结束后马上进行的就是日志模块的初始化 日志结构: 日志模块的初始化主要做的事情就是初始化全局变量 ngx_log,并创建 errlog 文件 ngx_log_s 结构ngx_log 变量是一个 ngx_log_s 结构体,定义在 core/ngx_log.h

MYSQL启用日志,查看日志,利用Mysqlbinlog工具恢复MySQL数据库

MYSQL启用日志 [root@jianshe99]# whereis my.ini [root@jianshe99]# vi /etc/my.cnf [mysqld] datadir=/var/lib/mysql socket=/var/lib/mysql/mysql.sock user=mysql # Default to using old password format for compatibility with mysql 3.x # clients (those using the

log-如何配置tomcat把访问日志也输出到tomcat命令窗口

问题描述 如何配置tomcat把访问日志也输出到tomcat命令窗口 目前我用的是tomcat7或8,请问各位配置把localhost_access_log.txt的日志也输出到tomcat命令窗口. tomcat命令窗口的日志存放在Tomcat 8.0logs目录下,该目录下有多个文件?,都是记录日志的,我发现 localhost_access_log.txt的?日志只会记录在该文件中,请问怎么配置让它也可以输出到tomcat命令窗口? 解决方案 配置Tomcat的访问日志格式化输出配置Tom

急需显示我的日志,添加日志和删除日志的代码

问题描述 我在做日志记事本,我想问哈怎么显示我的日志.添加日志,还有删除日志的代码...我想实现象qq空间里那样的功能.点击我的日志然后就在textbox中显示日志的时间,名称,如果再点击日志名称则显示日志内容.如果点击添加日志则显示日志的标题,时间.及内容框等.点击删除日志时则删除点击的日志....急急...谢谢哈 解决方案 解决方案二:楼主,你问的这个问题并不难,但是你没有说清楚很多问题你的日志打算保存在那里?文本文件orXML文件or数据库?结构是如何的?不了解这些我写出来的未必是你想要的