M.E.Logging Deep Dive

1 Overview

上周在工作中为 SDK 增加了 M.E.Logging 的支持,由于是为 Legacy 的代码中增加日志功能,所以遇到了一些挑战,主要有:

应该选择 ILogger<T>

可以通过封装一层 Log 方法,提前执行 IsEnabled 来避免日志字符串构建

由于 C# 支持 optional parameter , 当如果已有的构造函数的包含默认参数,比如

new MyClass("fenga") 编译器无法选择正确的构造函数,因此出现了编译错误,解决方法就将构造函数合并成一个 Options 对象

Asp.NET Core 应用程序中,在 Startup.cs 类的 ConfigureService 方法可以为应用程序添加服务。

但是如果是在 IConfiguration 中的服务呢?因为在 Asp.NET Core 应用程序启动的时候,首选构建的是 IConfiguration , 此刻注入的 ILoggerFactory 还没有创建好。因此该如何处理这个问题呢?

1. 创建一个 DeferredLoggerFactory , 所有它先暂时缓存所有的执行的日志

2. 在 IHostBuilder 中注入服务

3. 创建一个 IHostedServiceHost 中,该服务是用注入的 ILoggerFactory 替换 DeferredLoggerFactory

2. ILogger/ILoggerProvider/ILoggerFactory

API 定义上来看,三者之间的关系如下

但是这样的话,仍然让人感觉到疑惑,为什么有 ILoggerFactory 这个接口的存在?日志采用的是 订阅者-发布者 模式,通常来讲,有一个发布者但是包含有多个订阅者。每个 ILoggerProvider 创建的 ILogger 对象是订阅者,而 ILoggerFactory 创建的 ILogger 则是一个发布者,那么它们三者之间的逻辑关系如下:

所以 ILoggerFactory 创建的 ILogger 对象其实是各个 ILoggerProvider 创建的 ILogger 对象的集合,这样 ILogger 会下发所有日志输出请求到具体日志上。

除此之外,我们在创建 ILogger 对象的时候,有时候需要传入 CategoryName , 我们需要通过 Category 来指定该日志的来源,通常我们将日志所在的组件,服务或者类型作为日志类别。因为它是一个字符串,但是由于字符串容易出错,所以我们选择提供了带类型的日志 ILogger<T> , Logger<T> 是其一个默认实现

它只不过封装了一个普通的 ILogger 对象,而且 CategoryName 通过 TypeNameHelper 获得。

3. Logger

M.E.Logging 包中定义了一个 内部类型 Logger ,它是 LoggerFactory 类的CreateLogger(string categoryName) 方法返回的对象。刚刚的分析我们知道,该对象包含了所有 LoggerProvider 创建的对象。

该对象包含了三个数组

接下来我们讨论一下 ILogger 接口中的 IsEnabledLog<TState> 两个方法的具体实现。

IsEnabled

High Level 角度来看,只要 Logger 数组中,只要有一个 能够返回 true , 则返回true, 但是 MessageLogger 中包含了优先级更高的判断,所以要先对 MessagerLogger[] 数组中每个对象,首先调用 MessageLoggerIsEnabled , 如如果返回 false , 则判断下一个;如果 true , 则 fallbackILogger 对象的 IsEnabled 判断。

Log<TState>

同样的,一次迭代 MessageLogger[] 数组中对象,首先如果 MessageLoggerIsEnabled 返回 false , 则跳过这次日志输出,否则调用 LoggerLog 方法完成输出。

4. LoggerFactory

LoggerFactoryILoggerFactory 的默认实现,几乎所有的细节都隐藏在这些类中。

CreateLogger

CreateLoggerLoggerFactory 最重要的方法,主要流程如下:

5. FilterOptions/FilterRule

Logger 并不是将所有的日志都会输出,而是只有满足要输出日志的 Level 大于 MinLevel 的日志才会得到输出。

LoggerFilterOptions 用来定义不同的 CategoryName, ProviderTypeMinLevel 的定义,主要包含下面这些字段

LoggerFilterRule 类则包含了:

那么谁来消费 FilterOptionFilterRule 两个对象呢?答案是 LoggerRuleSelector , 它接受 LoggerFilterOptions, providerType, catetory 返回 minLevelFunc<string, string, LogLevel, bool>

它的筛选规则是这样的

那么该如何配置这些信息呢?通过 ILoggingBuilder 的扩展方法来注入 FilterOptions

6 Scope

Scope 是一个非常有趣的概念,我们知道我们的日志的输出的顺序的不做任何保证的,比如采用远程之类的 LoggerProvider , 网络的延迟肯定不会保证日志顺序按照程序执行的顺序。

那么我们该如何在一堆日志找到上下文相关的日志记录呢?答案就是在日志中增加各自父子关系的标识符。体现在 ILogger 接口就是 BeginScope<TState> 这个方法。

A software developer in Microsoft at Suzhou. Most articles spoken language is Chinese. I will try with English when I’m ready

A software developer in Microsoft at Suzhou. Most articles spoken language is Chinese. I will try with English when I’m ready