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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介:

定义在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)”选项卡。

image

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

9

 


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

作者:蒋金楠
微信公众账号:大内老A
微博: www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
1月前
|
存储 开发框架 JSON
ASP.NET Core OData 9 正式发布
【10月更文挑战第8天】Microsoft 在 2024 年 8 月 30 日宣布推出 ASP.NET Core OData 9,此版本与 .NET 8 的 OData 库保持一致,改进了数据编码以符合 OData 规范,并放弃了对旧版 .NET Framework 的支持,仅支持 .NET 8 及更高版本。新版本引入了更快的 JSON 编写器 `System.Text.UTF8JsonWriter`,优化了内存使用和序列化速度。
|
2月前
|
开发框架 监控 前端开发
在 ASP.NET Core Web API 中使用操作筛选器统一处理通用操作
【9月更文挑战第27天】操作筛选器是ASP.NET Core MVC和Web API中的一种过滤器,可在操作方法执行前后运行代码,适用于日志记录、性能监控和验证等场景。通过实现`IActionFilter`接口的`OnActionExecuting`和`OnActionExecuted`方法,可以统一处理日志、验证及异常。创建并注册自定义筛选器类,能提升代码的可维护性和复用性。
|
2月前
|
开发框架 .NET 中间件
ASP.NET Core Web 开发浅谈
本文介绍ASP.NET Core,一个轻量级、开源的跨平台框架,专为构建高性能Web应用设计。通过简单步骤,你将学会创建首个Web应用。文章还深入探讨了路由配置、依赖注入及安全性配置等常见问题,并提供了实用示例代码以助于理解与避免错误,帮助开发者更好地掌握ASP.NET Core的核心概念。
100 3
|
1月前
|
开发框架 JavaScript 前端开发
一个适用于 ASP.NET Core 的轻量级插件框架
一个适用于 ASP.NET Core 的轻量级插件框架
|
2月前
|
开发框架 NoSQL .NET
利用分布式锁在ASP.NET Core中实现防抖
【9月更文挑战第5天】在 ASP.NET Core 中,可通过分布式锁实现防抖功能,仅处理连续相同请求中的首个请求,其余请求返回 204 No Content,直至锁释放。具体步骤包括:安装分布式锁库如 `StackExchange.Redis`;创建分布式锁服务接口及其实现;构建防抖中间件;并在 `Startup.cs` 中注册相关服务和中间件。这一机制有效避免了短时间内重复操作的问题。
|
3月前
|
开发框架 监控 .NET
开发者的革新利器:ASP.NET Core实战指南,构建未来Web应用的高效之道
【8月更文挑战第28天】本文探讨了如何利用ASP.NET Core构建高效、可扩展的Web应用。ASP.NET Core是一个开源、跨平台的框架,具有依赖注入、配置管理等特性。文章详细介绍了项目结构规划、依赖注入配置、中间件使用及性能优化方法,并讨论了安全性、可扩展性以及容器化的重要性。通过这些技术要点,开发者能够快速构建出符合现代Web应用需求的应用程序。
58 0
|
3月前
|
缓存 数据库连接 API
Entity Framework Core——.NET 领域的 ORM 利器,深度剖析其最佳实践之路
【8月更文挑战第28天】在软件开发领域,高效的数据访问与管理至关重要。Entity Framework Core(EF Core)作为一款强大的对象关系映射(ORM)工具,在 .NET 开发中扮演着重要角色。本文通过在线书店应用案例,展示了 EF Core 的核心特性和优势。我们定义了 `Book` 实体类及其属性,并通过 `BookStoreContext` 数据库上下文配置了数据库连接。EF Core 提供了简洁的 API,支持数据的查询、插入、更新和删除操作。
116 0
|
2月前
|
开发框架 前端开发 JavaScript
ASP.NET MVC 教程
ASP.NET 是一个使用 HTML、CSS、JavaScript 和服务器脚本创建网页和网站的开发框架。
43 7
|
2月前
|
存储 开发框架 前端开发
ASP.NET MVC 迅速集成 SignalR
ASP.NET MVC 迅速集成 SignalR
63 0
|
3月前
|
开发框架 前端开发 .NET
ASP.NET MVC WebApi 接口返回 JOSN 日期格式化 date format
ASP.NET MVC WebApi 接口返回 JOSN 日期格式化 date format
49 0
下一篇
无影云桌面