在 《ASP.NET Core 沉思录 - Logging 的两种介入方法》中我们介绍了 ASP.NET Core 中日志的基本设计结构。这一次我们来观察日志记录的格式,并进一步考虑如何在应用程序中根据不同的需求选择不同的日志记录形式。

太长不读:直接飞到文章最后 :-D

Microsoft.Extension.Logging 体系下的日志格式

为了便于阅读,我们仍然将 Microsoft.Extension.Logging 的基本设计结构放在这里:

奇怪的不合理之处

注:所谓奇怪就是这种不合理是只是从某一种特定视角看的。

对于记录日志而言,虽然一些具体的日志记录目标和记录的格式会有一些联系,但是日志记录的目标和日志记录的格式应该是两件事情。貌似 Microsoft.Extension.Logging 在此处进行了一些抽象。首先,日志具体的记录地点和记录格式全部由具体的 ILoggerProvider 创建的 ILogger 来完成。而对于日志的格式化方法,则使用 ILogger.Log 方法中的委托来完成。该委托中包含了一个 formatter 委托参数。该委托接收需要记录的对象,关联的异常实例并返回日志字符串。我们可以在其中定义自己的格式化逻辑。总结起来感觉是:

  • 特定的 ILoggerProvider 创建将日志记录到特定种类的目的地的日志记录器。例如,ConsoleLogger

  • 指定 ILogger.Log 方法中的 formatter 参数对日志对象进行格式化。

ILogger.Log 方法除了 formatter 之外还包含如下的参数:

  • logLevel:日志的级别。

  • eventId:当前事件的标识。

  • state:日志对象。

  • exception:关联的异常对象。

formatter 参数将使用其中的 state 参数和 exception 参数对日志进行格式化。这样通过替换 formatter 的逻辑就可以更改日志的形式了。例如,使用如下的逻辑就可以将 state 格式化为 JSON 形式:

// Capture output so that we can assert its contentvar writer = new StringWriter();Console.SetOut(writer);

// Normal initialization logicvar serviceCollection = new ServiceCollection();serviceCollection.AddLogging(    config => config.SetMinimumLevel(LogLevel.Debug).AddConsole());ServiceProvider provider = serviceCollection.BuildServiceProvider();var loggerFactory = provider.GetService<ILoggerFactory>();var logger = loggerFactory.CreateLogger("category");

// Write log messagelogger.Log(    LogLevel.Information,    1,    new {message = "Hello {name}", name = "World"},    null,    (state, exception) => JsonConvert.SerializeObject(state));

// This is very important. The console logger using a async processor to consume// the queued log message.Thread.Sleep(1000);

Assert.Equal("...(omitted)...", writer.ToString())

如果您尝试了上述范例程序就会感到这个设计好像有问题,而如果联系整个 Extension.Logging 体系则感觉问题就更大了:

  • formatter只是解决了日志 message 部分的格式化问题,而无法影响其他信息的格式化,例如 eventIdlogLevelexception 等。

  • 我们根本不会用到具体的 ILoggerProvider 而是会使用 ILoggerFactory 提供的 Logger 门面。这个 Logger 是一个组合 Logger,也就是它会将 ILogger&lt;&gt;.Log 调用分发出来。但是我们很少使用 ILogger&lt;&gt;.Log 方法,而会使用扩展方法使用 template message 进行日志记录,这意味着所有的子 ILogger 实现都会接到同样的 formatter。自此,不同的目标采用不同的消息格式的理想破灭了。

总结一下就是,日志的记录目标和日志的格式混合了起来。职责区分不清。message 的格式化职责交给了门面扩展方法;而另一部分格式化职责交给了具体的 ILoggerProvider

从另一种视角看的合理之处

我们换一个视角可能就会得到不一样的体验。首先我们更改分析问题的策略。从端到端的角度来思考问题。当我们记录日志的时候希望有哪几类信息呢?日志作为追踪一个事件的依据,应当能够清晰的说明这个事件。那么小学语文老师就告诉过我们,记录一件事情需要有:

  • 时间

  • 地点

  • 人物

  • 起因

  • 经过

  • 结果

如果我们将这些信息归一归类,我们就可以得到这些信息:

  • 物理世界的环境参数:时间

  • 判断事件严重程度的依据:结果

  • 事件过程的上下文参数:地点(例如 URI 或代表某种操作的入口)、人物(例如谁进行的操作)、起因(例如方法调用参数)、经过(例如调用的那个方法)

而要记录这些信息,则可以对应到程序中的以下几种形式的数据:

  • 物理世界的环境参数:例如 DateTimeDateTimeOffset

  • 判断事件严重程度的依据:例如 LogLevel

  • 事件过程上下文的参数:例如事件的类别 CategoryName;一个包含各种各样上下文参数的 object[] 对象;以及对人类友好,能够将这些参数串起来的消息模板。

至此,你能够看到这些参数正是 Microsoft.Extension.Logging 中门面扩展方法中需要你来提供的参数。它本来也没有希望你调用 ILogger.Log 方法。而是希望你调用扩展方法用最舒服的方式达成日志记录的目的。

而作为 ILoggerProvider 开发者,你并不一定必须得接受 formatter 生成的格式化后的日志消息。你可以选择处理每一个传入参数。具体的请参见 FormattedLogValues 类型的源代码。

阶段性总结

  • 日志记录和记叙文一样,只要满足了六要素就可以说清楚一件事情。而记录这六要素的形式正式 Extension.Logging 提供给我们的扩展方法的参数形式。

  • 分析问题从端到端分析是一种非常靠谱的分析方法。可以避免走弯路。

  • ILogger 的扩展方法负责生成日志消息,ILoggerProvider 和负责记录工作的 ILogger 实现负责格式化日志消息并将日志记录到特定的目标上去。

利用 SeriLog 实现灵活的日志记录形式

通过上述分析我们应该能够看到这种设计的合理性。但是不争的事实是 ILoggerProvider 一系包揽两种职能,并没有进一步抽象,有没有人来对日志记录的目标和日志记录的整体格式进行抽象呢?有!那就是被千万人喜爱的 SeriLog。它在 ILoggerProvider 一级抽象了 ITextFormatter 解决了这个问题:

我不会在这里介绍 SeriLog 的具体使用方法。网上教程一大堆大家去搜搜好了。我建议直接去官网。

例如,我可以将日志记录到 Console 中,默认情况下,这种日志的格式是给人看的:

// normal initialization logicvar serviceCollection = new ServiceCollection();serviceCollection.AddLogging(b =>{    Logger seriLogger = new LoggerConfiguration()        .WriteTo.Console()        .MinimumLevel.Debug()        .CreateLogger();

    b.AddSerilog(seriLogger);});ServiceProvider provider = serviceCollection.BuildServiceProvider();

// create loggervar loggerFactory = provider.GetService<ILoggerFactory>();var logger = loggerFactory.CreateLogger("category");

// write loglogger.LogInformation("Hello {name}", "world");

此时屏幕上会输出高亮版的,适于阅读的日志,类似这样:

[18:41:27 INF] Hello world

但是如果希望使用其他的格式,则可以通过 ITextFormatter 快速的转换格式:

var serviceCollection = new ServiceCollection();serviceCollection.AddLogging(b =>{    Logger seriLogger = new LoggerConfiguration()        // PLEASE NOTE that we use JsonFormatter as input paramter        .WriteTo.Console(new JsonFormatter())        .MinimumLevel.Debug()        .CreateLogger();

    b.AddSerilog(seriLogger);});

ServiceProvider provider = serviceCollection.BuildServiceProvider();

var loggerFactory = provider.GetService<ILoggerFactory>();var logger = loggerFactory.CreateLogger("category");

logger.LogInformation("Hello {name}", "world");

这样就会得到以下的日志:

{    "Timestamp":"2019-03-24T19:38:54.4833240+08:00",    "Level":"Information",    "MessageTemplate":"Hello {name}",    "Properties":{"name":"world","SourceContext":"category"}}

如果还需要 formatter 格式化之后的完整消息,可以在创建 JsonFormatter 时指定 new JsonFormatter(renderMessage: true) 这样就会得到包含完整可读消息的结果:

{    "Timestamp":"2019-03-24T19:44:51.0430260+08:00",    "Level":"Information",    "MessageTemplate":"Hello {name}",    "RenderedMessage":"Hello \"world\"",    "Properties":{"name":"world","SourceContext":"category"}}

这样,在实际的操作中。我们可以直接使用 Microsoft.Extension.Logging 默认体系对 ILoggerProvider 进行扩展达到对记录目标和记录格式的控制;也可以将其与 SeriLog 集成。通过 Sink 和 ITextFormatter 组合的方式分别对记录目标和记录格式进行控制。

总结

一图胜千言:

图-1 默认 Microsoft.Extension.Logging 类型及信息传递路径

图-2 集成 SeriLog 后类型及信息传递路径

如果您觉得本文对您有帮助,也欢迎分享给其他的人。我们一起进步。欢迎关注我的博客(https://clrdaily.com)和微信公众号:

ASP.NET Core 沉思录 - 结构化日志相关推荐

  1. ASP.NET Core 沉思录 - Logging 的两种介入方法

    ASP.NET Core 中依赖注入是一个很重要的环节.因为几乎所有的对象都是由它创建的(相关文章请参见<ASP.NET Core 沉思录 - ServiceProvider 的二度出生> ...

  2. 【ASP.NET Core 沉思录】CreateWebHostBuilder 是一个 Convension

    失踪人口回归.去年六月份开始,我开始翻译一千多页的<CSharp 7 in a Nutshell>到现在为止终于告一段落.我又回归了表世界.从这次开始我希望展开一个全新的主题.我叫它 AS ...

  3. ASP.NET Core 沉思录 - ServiceProvider 的二度出生

    ASP.NET Core 终于将几乎所有的对象创建工作都和依赖注入框架集成了起来.并对大部分的日常工作进行了抽象.使得整个框架扩展更加方便.各个部分的集成也更加容易.今天我们要思考的部分仍然是从一段每 ...

  4. ASP.NET Core 沉思录 - 环境的思考

    我的博客换新家啦,新的地址为:https://clrdaily.com :-D 今天我们来一起思考一下如何在不同的环境应用不同的配置.这里的配置不仅仅指 IConfiguration 还包含 IWeb ...

  5. Asp.Net Core中利用Seq组件展示结构化日志功能

    在一次.Net Core小项目的开发中,掌握的不够深入,对日志记录并没有好好利用,以至于一出现异常问题,都得跑动服务器上查看,那时一度怀疑自己肯定没学好,不然这一块日志不可能需要自己扒服务器日志来查看 ...

  6. .NET Core开发日志——结构化日志

    在.NET生态圈中,最早被广泛使用的日志库可能是派生自Java世界里的Apache log4net.而其后来者,莫过于NLog.Nlog与log4net相比,有一项较显著的优势,它支持结构化日志. 结 ...

  7. 如何利用NLog输出结构化日志,并在Kibana优雅分析日志?

    上文我们演示了使用NLog向ElasticSearch写日志的基本过程(输出的是普通文本日志),今天我们来看下如何向ES输出结构化日志.在Kibana中分析日志. 什么是结构化日志? 当前互联网.物联 ...

  8. asp.Net Core免费开源分布式异常日志收集框架Exceptionless安装配置以及简单使用图文教程...

    最近在学习张善友老师的NanoFabric 框架的时了解到Exceptionless : https://exceptionless.com/ !因此学习了一下这个开源框架!下面对Exceptionl ...

  9. ASP.NET Core应用程序容器化、持续集成与Kubernetes集群部署(三

    在上文ASP.NET Core应用程序容器化.持续集成与Kubernetes集群部署(二)中,我介绍了如何使用Azure DevOps为ASP.NET Core应用程序案例:tasklist搭建持续集 ...

最新文章

  1. 一文掌握Conda软件安装:虚拟环境、软件通道、加速solving、跨服务器迁移
  2. 使用Git,显示一个分支中的所有提交,但不显示其他分支中的所有提交
  3. oracle 查询表字段6,Oracle 查询库中所有表名、字段名、字段名说明,查询表的数据条数、表名、中文表名、...
  4. 用魔法打开科学,孩子惊叫连连,想不爱科学都难!
  5. 命令行下Apache日志统计举例
  6. 2018年《DevOps 现状报告》新鲜出炉:行业脉动与团队发展指导一览
  7. 上学的时候的一个作业
  8. js如何获取php中的变量的类型,js获取变量的类型
  9. 翻译任务中的BPE词表实践总结
  10. wow模型修改器_魔兽世界改模型 wow大脚模型修改器
  11. 实例学习之仿点点博客
  12. oracle对时间范围比较的语句
  13. 什么是 NLP (自然语言处理)
  14. 报错Takes no arguements解决办法
  15. Geometric deep learning: going beyond Euclidean data译文
  16. VB读取武林外传内存地址
  17. 南卫理公会大学 计算机排名,2019-2020南卫理公会大学世界排名多少【QS最新第701-750名】...
  18. python 发送邮件connect none_python发送邮件(smtplib)
  19. 北斗三号频点_北斗三号将为手机用户带来更好用户体验
  20. jsp002网络问卷调查系统

热门文章

  1. 影响程序员生涯的三个错误观念,你千万不要犯!
  2. 30分钟掌握ES6/ES2015核心内容 1
  3. 从Visual Studio中生成Linux设备
  4. 【Solidity】3.类型 - 深入理解Solidity
  5. lsattr/chattr
  6. Openfire3.10beta版源码在eclipse上部署编译
  7. 30分钟通过Kong实现.NET网关
  8. System.Text.Json 自定义 Conveter
  9. 如何使用 EF Core 按周 对数据分组?
  10. 基于事件驱动架构构建微服务第4部分:repositories