CodeSnippet.Cn
代码片段
Csharp
架构设计
.NetCore
西班牙语
kubernetes
MySql
Redis
Algorithm
Ubuntu
Linux
Other
.NetMvc
VisualStudio
Git
pm
Python
WPF
java
Plug-In
分布式
CSS
微服务架构
JavaScript
DataStructure
Shared
理解ASP.NET Core - 日志(Logging)
0
.NetCore
小笨蛋
发布于:2022年11月30日
更新于:2022年11月30日
168
#custom-toc-container
### 快速上手 #### 添加日志提供程序 在(Host)中,Host.CreateDefaultBuilder方法,默认通过调用ConfigureLogging方法添加了Console、Debug、EventSource和EventLog(仅Windows)共四种日志记录提供程序(Logger Provider),然后在主机Build过程中,通过AddLogging()注册了日志相关的服务。 ```csharp .ConfigureLogging((hostingContext, logging) => { bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); if (isWindows) { logging.AddFilter
(level => level >= LogLevel.Warning); } // 添加 Logging 配置 logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); // ConsoleLoggerProvider logging.AddConsole(); // DebugLoggerProvider logging.AddDebug(); // EventSourceLoggerProvider logging.AddEventSourceLogger(); if (isWindows) { // 在Windows平台上,添加 EventLogLoggerProvider logging.AddEventLog(); } logging.Configure(options => { options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId | ActivityTrackingOptions.TraceId | ActivityTrackingOptions.ParentId; }); }) public class HostBuilder : IHostBuilder { private void CreateServiceProvider() { var services = new ServiceCollection(); // ... services.AddLogging(); // ... } } ``` 如果不想使用默认添加的日志提供程序,我们可以通过ClearProviders清除所有已添加的日志记录提供程序,然后添加自己想要的,如Console: ```csharp public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureLogging(logging => { logging.ClearProviders() .AddConsole(); }) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup
(); }); ``` #### 记录日志 日志记录提供程序均实现了接口ILoggerProvider,该接口可以创建ILogger实例。 通过注入服务ILogger
,就可以非常方便的进行日志记录了。 该服务需要指定日志的类别,可以是任意字符串,但是我们约定使用所属类的名称,通过泛型体现。例如,在控制器ValuesController中,日志类别就是ValuesController类的完全限定类型名。 ```csharp public class ValuesController : ControllerBase { private readonly ILogger
_logger; public ValuesController(ILogger
logger) { _logger = logger; } [HttpGet] public string Get() { _logger.LogInformation("ValuesController.Get"); return "Ok"; } } ``` 当请求Get方法后,你就可以在控制台中看到看到输出的“ValuesController.Get” 如果你想要显式指定日志类别,则可以使用ILoggerFactory.CreateLogger方法: ```csharp public class ValuesController : ControllerBase { private readonly ILogger _logger1; public ValuesController(ILoggerFactory loggerFactory) { _logger1 = loggerFactory.CreateLogger("MyCategory"); } } ``` ### 配置日志 默认模板中,日志的配置如下(在appsettings.{Environment}.json文件中): ```csharp { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } } } ``` #### 针对所有日志记录提供程序进行配置 LogLevel,顾名思义,就是指要记录的日志的最低级别(即要记录大于等于该级别的日志),想必大家都不陌生。下方会详细介绍日志级别。 LogLevel中的字段,如上面示例中的“Default”、“Microsoft”等,表示日志的类别,也就是咱们上面注入ILogger时指定的泛型参数。可以为每种类别设置记录的最小日志级别,也就是这些类别所对应的值。 下面详细解释一下示例中的三种日志类别。 **Default** 默认情况下,如果分类没有进行特别配置(即没有在LogLevel中配置),则应用Default的配置。 **Microsoft** 所有分类以Microsoft开头的日志均应用Microsoft的配置。例如,Microsoft.AspNetCore.Routing.EndpointMiddleware类别的日志就会应用该配置。 **Microsoft.Hosting.Lifetime** 所有分类以Microsoft.Hosting.Lifetime开头的日志均应用Microsoft.Hosting.Lifetime的配置。例如,分类Microsoft.Hosting.Lifetime就会应用该配置,而不会应用Microsoft,因为Microsoft.Hosting.Lifetime比Microsoft更具体。 OK,以上三种日志类别就说这些了。 回到示例,你可能没有注意到,这里面没有针对某个日志记录提供程序进行单独配置(如:Console只记录Error及以上级别日志,而EventSource则需要记录记录所有级别日志)。像这种,如果没有针对特定的日志记录提供程序进行配置,则该配置将会应用到所有日志记录提供程序。 > Windows EventLog 除外。EventLog必须显式地进行配置,否则会使用其默认的LogLevel.Warning。 #### 针对指定的日志记录提供程序进行配置 接下来看一下如何针对指定的日志记录提供程序进行配置,先上示例: ```csharp { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" }, "Console": { "LogLevel": { "Default": "Error" } }, "Debug": { "LogLevel": { "Microsoft": "None" } }, "EventSource": { "LogLevel": { "Default": "Trace", "Microsoft": "Trace", "Microsoft.Hosting.Lifetime": "Trace" } } } } ``` 就像appsettings.{Environment}.json和appsettings.json之间的关系一样,Logging.{Provider}.LogLevel中的配置将会覆盖Logging.LogLevel中的配置。 例如Logging.Console.LogLevel.Default将会覆盖Logging.LogLevel.Default,Console日志记录器将默认记录Error及其以上级别的日志。 刚才提到了,Windows EventLog比较特殊,它不会继承Logging.LogLevel的配置。EventLog默认日志级别为LogLevel.Warning,如果想要修改,则必须显式进行指定,如: ```csharp { "Logging": { "EventLog": { "LogLevel": { "Default": "Information" } } } } ``` #### 配置的筛选原理 当创建ILogger
的对象实例时,ILoggerFactory根据不同的日志记录提供程序,将会: 1. 查找匹配该日志记录提供程序的配置。如果找不到,则使用通用配置。 2. 然后匹配拥有最长前缀的配置类别。如果找不到,则使用Default配置。 3. 如果匹配到了多条配置,则采用最后一条。 4. 如果没有匹配到任何配置,则使用MinimumLevel,这是个配置项,默认是LogLevel.Information。 > 可以在ConfigureLogging扩展中使用SetMinimumLevel方法设置MinimumLevel。 ### Log Level 日志级别指示了日志的严重程度,一共分为7等,从轻到重为(最后的None较为特殊): | 日志级别 | 值 | 描述 | | ------------- | --- | ------------------------------------------------------------------------------------------------------ | | `Trace` | 0 | 追踪级别,包含最详细的信息。这些信息可能包含敏感数据,默认情况下是禁用的,并且绝不能出现在生产环境中。 | | `Debug` | 1 | 调试级别,用于开发人员开发和调试。信息量一般比较大,在生产环境中一定要慎用。 | | `Information` | 2 | 信息级别,该级别平时使用较多。 | | `Warning` | 3 | 警告级别,一些意外的事件,但这些事件并不对导致程序出错。 | | `Error` | 4 | 错误级别,一些无法处理的错误或异常,这些事件会导致当前操作或请求失败,但不会导致整个应用出错。 | | `Critical` | 5 | 致命错误级别,这些错误会导致整个应用出错。例如内存不足等。 | | `None` | 6 | 指示不记录任何日志 | ### 日志记录提供程序 #### Console 日志将输出到控制台中。 #### Debug 日志将通过System.Diagnostics.Debug类进行输出,可以通过VS输出窗口查看。 在 Linux 上,可以在/var/log/message或/var/log/syslog下找到 #### EventSource 跨平台日志记录,在Windows上则使用 ETW #### Windows EventLog 仅在Windows系统下生效,可通过“事件查看器”进行日志查看。 默认情况下 - LogName为“Application” - SourceName为“NET Runtime” - MachineName为本地计算机的名称。 这些字段都可以通过EventLogSettings进行修改: ```csharp public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureLogging(logging => { logging.AddEventLog(settings => { settings.LogName = "My App"; settings.SourceName = "My Log"; settings.MachineName = "My Computer"; }) }) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup
(); }); ``` ### 日志记录过滤器 通过日志记录过滤器,允许你书写复杂的逻辑,来控制是否要记录日志。 ```csharp public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureLogging(logging => { logging // 针对所有 LoggerProvider 设置 Microsoft 最小日志级别,建议通过配置文件进行配置 .AddFilter("Microsoft", LogLevel.Trace) // 针对 ConsoleLoggerProvider 设置 Microsoft 最小日志级别,建议通过配置文件进行配置 .AddFilter
("Microsoft", LogLevel.Debug) // 针对所有 LoggerProvider 进行过滤配置 .AddFilter((provider, category, logLevel) => { // 由于下面单独针对 ConsoleLoggerProvider 添加了过滤配置,所以 ConsoleLoggerProvider 不会进入该方法 if (provider == typeof(ConsoleLoggerProvider).FullName && category == typeof(ValuesController).FullName && logLevel <= LogLevel.Warning) { // false:不记录日志 return false; } // true:记录日志 return true; }) // 针对 ConsoleLoggerProvider 进行过滤配置 .AddFilter
((category, logLevel) => { if (category == typeof(ValuesController).FullName && logLevel <= LogLevel.Warning) { // false:不记录日志 return false; } // true:记录日志 return true; }); }) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup
(); }); ``` ### 日志消息模版 应用开发过程中,对于某一类的日志,我们希望它们的消息格式保持一致,仅仅是某些参数发生变化。这就要用到日志消息模板了。 举个例子: ```csharp [HttpGet("{id}")] public int Get(int id) { _logger.LogInformation("Get {Id}", id); return id; } ``` 其中Get {Id}就是一个日志消息模板,{Id}则是模板参数(注意,请在里面书写名称,而不是数字,这样更容易理解参数含义)。 不过,需要注意的是,{Id}这个模板参数,仅仅是用于让人容易理解其含义的,和后面的参数名没有任何关系,模板值关心参数的顺序。例如: ```csharp [HttpGet("{id}")] public int Get(int id) { _logger.LogInformation("Get {Id} at {Time}", DateTime.Now, id); return id; } ``` 假设传入id = 1,它的输出是:Get 11/02/2021 11:42:14 at 1 > 日志消息模板是一项非常重要的功能,在众多开源日志中间件中,均有使用。 ### 主机构建期间的日志记录 ASP.NET Core框架不直接支持在主机构建期间进行日志记录。但是可以通过独立的日志记录提供程序进行日志记录,例如,使用第三方日志记录提供程序:[Serilog](https://github.com/serilog/serilog-aspnetcore "Serilog") 安装Nuget包:Install-Package Serilog.AspNetCore ```csharp public static void Main(string[] args) { // 从appsettings.json和命令行参数中读取配置 var config = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddCommandLine(args) .Build(); // 创建Logger Log.Logger = new LoggerConfiguration() .WriteTo.Console() // 输出到控制台 .WriteTo.File(config["Logging:File:Path"]) // 输出到指定文件 .CreateLogger(); try { CreateHostBuilder(args).Build().Run(); } catch(Exception ex) { Log.Fatal(ex, "Host terminated unexpectedly"); throw; } finally { Log.CloseAndFlush(); } } ``` appsettings.json ```csharp { "Logging": { "File": { "Path": "logs/host.log" } } } ``` ### 控制台日志格式配置 控制台日志记录提供程序是我们开发过程中必不可少的,通过上面我们已经得知可以通过AddConsole()进行添加。不过它的局限性比较大,日志格式我们都无法进行自定义。 因此,在.NET 5中,对控制台日志记录提供程序进行了扩展,预置了三种日志输出格式:Json、Simple、Systemd。 > 实际上,之前也有枚举ConsoleLoggerFormat提供了Simple和Systemd格式,不过不能进行自定义,已经弃用了。 这些 Formatter 均继承自抽象类ConsoleFormatter,该抽象类构造函数接收一个“名字”参数,要求其实现类必须拥有名字。你可以通过静态类ConsoleFormatterNames获取到内置的三种格式的名字。 ```csharp public abstract class ConsoleFormatter { protected ConsoleFormatter(string name) { Name = name ?? throw new ArgumentNullException(nameof(name)); } public string Name { get; } public abstract void Write
(in LogEntry
logEntry, IExternalScopeProvider scopeProvider, TextWriter textWriter); } public static class ConsoleFormatterNames { public const string Simple = "simple"; public const string Json = "json"; public const string Systemd = "systemd"; } ``` 你可以在使用AddConsole()时,配置ConsoleLoggerOptions的FormatterName属性,以达到自定义格式的目的,其默认值为“simple”。不过,为了方便使用,.NET 框架已经把内置的三种格式帮我们封装好了。 这些 Formatter 的选项类均继承自选项类ConsoleFormatterOptions,该选项类包含以下三个属性: ```csharp public class ConsoleFormatterOptions { // 启用作用域,默认 false public bool IncludeScopes { get; set; } // 设置时间戳的格式,显示在日志消息开头 // 默认为 null,不展示时间戳 public string TimestampFormat { get; set; } // 是否将时间戳时区设置为 UTC,默认是false,即本地时区 public bool UseUtcTimestamp { get; set; } } ``` #### SimpleConsoleFormatter 通过扩展方法AddSimpleConsole()可以添加支持Simple格式的控制台日志记录提供程序,默认行为与AddConsole()一致。 ```csharp .ConfigureLogging(logging => { logging.ClearProviders() .AddSimpleConsole(); } ``` 示例输出: ```csharp info: Microsoft.Hosting.Lifetime[0] Now listening on: http://localhost:5000 info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down. info: Microsoft.Hosting.Lifetime[0] Hosting environment: Development info: Microsoft.Hosting.Lifetime[0] Content root path: C:\Repos\WebApplication ``` 另外,你可以通过SimpleConsoleFormatterOptions进行一些自定义配置: ```csharp .ConfigureLogging(logging => { logging.ClearProviders() .AddSimpleConsole(options => { // 一条日志消息展示在同一行 options.SingleLine = true; options.IncludeScopes = true; options.TimestampFormat = "yyyy-MM-dd HH:mm:ss "; options.UseUtcTimestamp = false; }); } ``` 示例输出: ```csharp 2021-11-02 15:53:33 info: Microsoft.Hosting.Lifetime[0] Now listening on: http://localhost:5000 2021-11-02 15:53:33 info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down. 2021-11-02 15:53:33 info: Microsoft.Hosting.Lifetime[0] Hosting environment: Development 2021-11-02 15:53:33 info: Microsoft.Hosting.Lifetime[0] Content root path: C:\Repos\WebApplication ``` #### SystemdConsoleFormatter 通过扩展方法AddSystemdConsole()可以添加支持Systemd格式的控制台日志记录提供程序。如果你熟悉Linux,那你对它也一定不陌生。 ```csharp .ConfigureLogging(logging => { logging.ClearProviders() .AddSystemdConsole(); } ``` 示例输出: ```csharp <6>Microsoft.Hosting.Lifetime[0] Now listening on: http://localhost:5000 <6>Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down. <6>Microsoft.Hosting.Lifetime[0] Hosting environment: Development <6>Microsoft.Hosting.Lifetime[0] Content root path: C:\Repos\WebApplication ``` 前面的<6>表示日志级别info,如果你有兴趣了解Systemd,可以访问阮一峰老师的[Systemd 入门教程:命令篇](https://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-commands.html "Systemd 入门教程:命令篇") #### JsonConsoleFormatter 通过扩展方法AddJsonConsole()可以添加支持Json格式的控制台日志记录提供程序。 ```csharp .ConfigureLogging(logging => { logging.ClearProviders() .AddJsonConsole(options => { options.JsonWriterOptions = new JsonWriterOptions { // 启用缩进,看起来更舒服 Indented = true }; }); } ``` 示例输出: ```csharp { "EventId": 0, "LogLevel": "Information", "Category": "Microsoft.Hosting.Lifetime", "Message": "Now listening on: http://localhost:5000", "State": { "Message": "Now listening on: http://localhost:5000", "address": "http://localhost:5000", "{OriginalFormat}": "Now listening on: {address}" } } { "EventId": 0, "LogLevel": "Information", "Category": "Microsoft.Hosting.Lifetime", "Message": "Application started. Press Ctrl\u002BC to shut down.", "State": { "Message": "Application started. Press Ctrl\u002BC to shut down.", "{OriginalFormat}": "Application started. Press Ctrl\u002BC to shut down." } } { "EventId": 0, "LogLevel": "Information", "Category": "Microsoft.Hosting.Lifetime", "Message": "Hosting environment: Development", "State": { "Message": "Hosting environment: Development", "envName": "Development", "{OriginalFormat}": "Hosting environment: {envName}" } } { "EventId": 0, "LogLevel": "Information", "Category": "Microsoft.Hosting.Lifetime", "Message": "Content root path: C:\\Repos\\WebApplication", "State": { "Message": "Content root path: C:\\Repos\\WebApplication", "contentRoot": "C:\\Repos\\WebApplication", "{OriginalFormat}": "Content root path: {contentRoot}" } } ``` > 如果你同时添加了多种格式的控制台记录程序,那么只有最后一个添加的生效。 以上介绍的是通过代码进行控制台日志记录提供程序的设置,不过我想大家应该更喜欢通过配置去设置日志记录提供程序。下面是一个简单地配置示例: ```csharp { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" }, "Console": { "FormatterName": "json", "FormatterOptions": { "SingleLine": true, "IncludeScopes": true, "TimestampFormat": "yyyy-MM-dd HH:mm:ss ", "UseUtcTimestamp": false, "JsonWriterOptions": { "Indented": true } } } } } ``` ### ILoggerTCategoryName对象实例的创建 讲到这里,不知道你会不会对ILogger
对象实例的创建有疑惑:它到底是如何被new出来的呢? 要解决这个问题,我们先从AddLogging()扩展方法入手: ```csharp public static class LoggingServiceCollectionExtensions { public static IServiceCollection AddLogging(this IServiceCollection services) { return AddLogging(services, builder => { }); } public static IServiceCollection AddLogging(this IServiceCollection services, Action
configure) { services.AddOptions(); // 注册单例 ILoggerFactory services.TryAdd(ServiceDescriptor.Singleton
()); // 注册单例 ILogger<> services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>))); // 批量注册单例 IConfigureOptions
services.TryAddEnumerable(ServiceDescriptor.Singleton
>( new DefaultLoggerLevelConfigureOptions(LogLevel.Information))); configure(new LoggingBuilder(services)); return services; } } ``` 你可能也猜到了,这个Logger<>不会是LoggerFactory创建的吧?要不然注册个这玩意干嘛呢? 别着急,咱们接着先查看ILogger<>服务的实现类Logger<>: ```csharp public interface ILogger { void Log
(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func
formatter); // 检查能否记录该日志等级的日志 bool IsEnabled(LogLevel logLevel); IDisposable BeginScope
(TState state); } public interface ILogger
: ILogger { } public class Logger
: ILogger
{ // 接口实现内部均是使用该实例进行操作 private readonly ILogger _logger; // 果不其然,注入了 ILoggerFactory 实例 public Logger(ILoggerFactory factory) { // 还记得吗?上面提到显式指定日志类别时,也是这样创建 ILogger 实例的 _logger = factory.CreateLogger(TypeNameHelper.GetTypeDisplayName(typeof(T), includeGenericParameters: false, nestedTypeDelimiter: '.')); } // ... } ``` 没错,你猜对了,那就来看看这个LoggerFactory吧(只列举核心代码): ```csharp public interface ILoggerFactory : IDisposable { ILogger CreateLogger(string categoryName); void AddProvider(ILoggerProvider provider); } public class LoggerFactory : ILoggerFactory { // 用于单例化 Logger<> private readonly Dictionary
_loggers = new Dictionary
(StringComparer.Ordinal); // 存放 ILoggerProviderRegistrations private readonly List
_providerRegistrations = new List
(); private readonly object _sync = new object(); public LoggerFactory(IEnumerable
providers, IOptionsMonitor
filterOption, IOptions
options = null) { // ... // 注册 ILoggerProviders foreach (ILoggerProvider provider in providers) { AddProviderRegistration(provider, dispose: false); } // ... } public ILogger CreateLogger(string categoryName) { lock (_sync) { // 如果不存在,则 new if (!_loggers.TryGetValue(categoryName, out Logger logger)) { logger = new Logger { Loggers = CreateLoggers(categoryName), }; (logger.MessageLoggers, logger.ScopeLoggers) = ApplyFilters(logger.Loggers); // 单例化 Logger<> _loggers[categoryName] = logger; } return logger; } } private void AddProviderRegistration(ILoggerProvider provider, bool dispose) { _providerRegistrations.Add(new ProviderRegistration { Provider = provider, ShouldDispose = dispose }); // ... } private LoggerInformation[] CreateLoggers(string categoryName) { var loggers = new LoggerInformation[_providerRegistrations.Count]; // 循环遍历所有 ILoggerProvider for (int i = 0; i < _providerRegistrations.Count; i++) { loggers[i] = new LoggerInformation(_providerRegistrations[i].Provider, categoryName); } return loggers; } } ``` ### 注意 - 若要在Startup.Configure方法中记录日志,直接在参数上注入ILogger
即可。 - 不支持在Startup.ConfigureServices方法中使用ILogger,因为此时DI容器还未配置完成。 - 没有异步的日志记录方法。日志记录动作执行应该很快,不值的牺牲性能使用异步方法。如果日志记录动作比较耗时,如记录到MSSQL中,那么请不要直接写入MSSQL。你应该考虑先将日志写入到快速存储介质,如内存队列,然后通过后台工作线程将其从内存转储到MSSQL中。 - 无法使用日志记录 API 在应用运行时更改日志记录配置。不过,一些配置提供程序(如文件配置提供程序)可重新加载配置,这可以立即更新日志记录配置。 ### 小结 - Host.CreateDefaultBuilder方法中,默认添加了Console、Debug、EventSource和EventLog(仅Windows)共四种日志记录提供程序(Logger Provider)。 - 通过注入服务ILogger
,可以方便的进行日志记录。 - 可以通过代码或配置对日志记录提供程序进行设置,如LogLevel、FormatterName等。 - 可以通过扩展方法AddFilter添加日志记录过滤器,允许你书写复杂的逻辑,来控制是否要记录日志。 - 支持日志消息模板。 - 对于控制台记录日志程序,.NET框架内置了Simple(默认)、Systemd、Json三种日志输出格式。 - .NET 6 预览版中新增了一个称为“编译时日志记录源生成”的功能,该功能非常实用,有兴趣的可以先去了解一下。 - 最后,给大家列举一些常用的日志开源中间件: - [Serilog](https://github.com/serilog/serilog-aspnetcore "Serilog") - [Log4Net](https://github.com/huorswords/Microsoft.Extensions.Logging.Log4Net.AspNetCore "Log4Net") - [NLog](https://github.com/NLog/NLog.Extensions.Logging "NLog")
这里⇓感觉得写点什么,要不显得有点空,但还没想好写什么...
返回顶部
About
京ICP备13038605号
© 代码片段 2024