日志框架必要的包:
1、Microsoft.Extensions.Logging
2、Microsoft.Extensions.Logging.Console
3、Microsoft.Extensions.Logging.Debug
4、Microsoft.Extensions.Logging.TraceSource
5、Microsoft.Extensions.Configuration.Json
代码通过一个控制台程序,展示从读取配置到整个日志的记录器的构造和日志记录的过程
首先从文件读取配置
Copy IConfigurationBuilder configBuilder = new ConfigurationBuilder();
configBuilder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
var config = configBuilder.Build();
接着构造容器,注入对象
Copy IServiceCollection serviceCollection = new ServiceCollection();// 构造容器
// 用工厂模式将配置对象注册到容器管理
// 注入的时候使用了一个委托,意味着容器可以帮我们管理这个对象的生命周期
serviceCollection.AddSingleton<IConfiguration>(p => config);
// 如果将实例直接注入,容器不会帮我们管理
//serviceCollection.AddSingleton<IConfiguration>(config);
// AddLogging 往容器里面注册了几个关键对象:
// ILoggerFactory,泛型模板 typeof (ILogger<>),Logger 的过滤配置 IConfigureOptions<LoggerFilterOptions>
// 最后一行,configure((ILoggingBuilder) new LoggingBuilder(services)); 就是整个注入我们的委托
serviceCollection.AddLogging(builder =>
{
builder.AddConfiguration(config.GetSection("Logging"));// 注册 Logging 配置的 Section
builder.AddConsole();// 先使用一个 Console 的日志输出提供程序
});
AddLogging 源码
Copy public static IServiceCollection AddLogging(
this IServiceCollection services,
Action<ILoggingBuilder> configure)
{
if (services == null)
throw new ArgumentNullException(nameof (services));
services.AddOptions();
services.TryAdd(ServiceDescriptor.Singleton<ILoggerFactory, LoggerFactory>());
services.TryAdd(ServiceDescriptor.Singleton(typeof (ILogger<>), typeof (Logger<>)));
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<LoggerFilterOptions>>((IConfigureOptions<LoggerFilterOptions>) new DefaultLoggerLevelConfigureOptions(LogLevel.Information)));
configure((ILoggingBuilder) new LoggingBuilder(services));
return services;
}
配置文件,appsettings.json
Copy {
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
},
"Console": {
"LogLevel": {
"Default": "Information",
"Program": "Trace",
"alogger": "Trace",
"LoggingSimpleDemo.OrderService": "None"
}
}
}
}
Logging 里面定义了 Log 的级别,Key 代表 Log 的名称,Value 代表 Logger 的级别
Console 是指针对 Console 的输出提供程序配置的日志级别
下面看一下日志级别的定义,按照严重程度从低到高
Copy namespace Microsoft.Extensions.Logging
{
public enum LogLevel
{
Trace,
Debug,
Information,
Warning,
Error,
Critical,
None,
}
}
也就是说我们可以指定日志输出的最低级别
接着 BuildServiceProvider,从容器里面获取 ILoggerFactory
Copy IServiceProvider service = serviceCollection.BuildServiceProvider();
ILoggerFactory loggerFactory = service.GetService<ILoggerFactory>();
ILoggerFactory 的定义
Copy namespace Microsoft.Extensions.Logging
{
public interface ILoggerFactory : IDisposable
{
// 输入的名称是 Logger 的名称,输出的结果是一个 ILogger 的对象,代表日志记录器
ILogger CreateLogger(string categoryName);
// 这个方法通常不会用到它,因为通常情况下注册容器提供程序会在 AddLogging 委托里面去注册,而不会用 AddProvider 方法
void AddProvider(ILoggerProvider provider);
}
}
获取到 ILoggerFactory 之后就可以创建日志记录器
Copy ILogger alogger = loggerFactory.CreateLogger("alogger");
alogger.LogDebug(2001, "aiya");
alogger.LogInformation("hello");
var ex = new Exception("出错了");
alogger.LogError(ex, "出错了");
因为配置文件中 alogger 的级别是 Trace
所以这三行都会被打印出来
启动程序,输出如下:
Copy dbug: alogger[2001]
aiya
info: alogger[0]
hello
fail: alogger[0]
出错了
System.Exception: 出错了
方括号的内容是 EventID,也就是针对每一个记录的位置事件,可以为它分配一个事件 ID,代码中在 LogDebug 的时候定义了一个事件 ID 是2001
假如说把 alogger 的日志级别调整成 Information
Copy "alogger": "Information",
那么 Debug 级别的信息没有输出的
Copy info: alogger[0]
hello
fail: alogger[0]
出错了
System.Exception: 出错了
除了使用 CreateLogger 指定 logger 的名称,实际上还可以借助容器来构造 logger,通常情况下我们会定义自己的类
Copy namespace LoggingSimpleDemo
{
public class OrderService
{
ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}
public void Show()
{
_logger.LogInformation("Show Time{time}", DateTime.Now);
}
}
}
接着,将 OrderService 注入到容器中
Copy serviceCollection.AddTransient<OrderService>();
IServiceProvider service = serviceCollection.BuildServiceProvider();
var order = service.GetService<OrderService>();
order.Show();
日志级别设置为 Trace
Copy "LoggingSimpleDemo.OrderService": "Trace"
启动程序,输出如下:
Copy info: LoggingSimpleDemo.OrderService[0]
Show Time03/06/2020 23:41:38
这样做的意义是什么呢?
通常情况下并不会用 ILoggerFactory 来构造日志记录器,而是用强类型的这种依赖注入的方式来去管理我们的日志,也就是说用构造函数将泛型的 ILogger 注入进来的方式
这样的方式有个好处就是我们不需要去为 logger 定义名字,它会默认将我们类型的名称作为记录器的名字,命名空间加上类名 LoggingSimpleDemo.OrderService ,那也就是可以在配置文件里面设置日志级别
Copy "LoggingSimpleDemo.OrderService": "None"
这样子就没有输出
这里面有一个小技巧,需要大家特别注意,就是当我们在记录日志的时候,尽量使用模板的方式
Copy _logger.LogInformation("Show Time{time}", DateTime.Now);
以下两种方式效果相同,但是字符串拼接的时机不同
Copy _logger.LogInformation("Show Time{time}", DateTime.Now);
_logger.LogInformation($"Show Time{DateTime.Now}");
第一行代码是在我们决定要输出的时候,也就是在 LogInformation 内部 console 要输出的时候才做拼接的动作
第二行代码是指我们在字符串拼接好以后,输入给了 LogInformation
如果我们把日志级别关掉
Copy "LoggingSimpleDemo.OrderService": "None"
两行代码都不会有输出,但是第一行代码字符串拼接的动作不会执行,第二行代码已经执行了,第一行代码节省了运行资源
另外一个就是,在记录日志的时候,不要把敏感信息记录到日志中,记录日志的目的是为了调试或者定位问题
总结一下
1、日志级别定义
日志级别会从严重程度的低到高定义,可以决定输出的最低级别
2、日志对象获取
可以通过 ILoggerFactory 的方式获取日志对象,对它指定一个名字,也可以通过 ILogger 泛型的模式,从容器中获取日志对象,最推荐的就是强类型的泛型模式
3、日志过滤的配置逻辑
可以针对 logger 的名称来进行任意的配置,日志的开关以及日志的级别
4、日志记录的方法
LogInformation,LogDebug,还有一些小技巧,使用模板的方式记录日志,而不是提前拼接字符串输入给日志系统
5、避免记录敏感信息,如密码、密钥,规避安全风险
GitHub源码链接:https://github.com/MingsonZheng/DotNetCoreDevelopmentActualCombat/tree/main/LoggingSimpleDemo