注:本文隸屬於《理解ASP.NET Core》系列文章,請查看置頂博客或點擊此處查看全文目錄
快速上手
添加日志提供程序
在文章主機(Host)中,講到Host.CreateDefaultBuilder
方法,默認通過調用ConfigureLogging
方法添加了Console
、Debug
、EventSource
和EventLog
(僅Windows)共四種日志記錄提供程序(Logger Provider),然后在主機Build
過程中,通過AddLogging()
注冊了日志相關的服務。
.ConfigureLogging((hostingContext, logging) =>
{
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
if (isWindows)
{
logging.AddFilter<EventLogLoggerProvider>(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
:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.ClearProviders()
.AddConsole();
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
記錄日志
日志記錄提供程序均實現了接口ILoggerProvider
,該接口可以創建ILogger
實例。
通過注入服務ILogger<TCategoryName>
,就可以非常方便的進行日志記錄了。
該服務需要指定日志的類別,可以是任意字符串,但是我們約定使用所屬類的名稱,通過泛型體現。例如,在控制器ValuesController
中,日志類別就是ValuesController
類的完全限定類型名。
public class ValuesController : ControllerBase
{
private readonly ILogger<ValuesController> _logger;
public ValuesController(ILogger<ValuesController> logger)
{
_logger = logger;
}
[HttpGet]
public string Get()
{
_logger.LogInformation("ValuesController.Get");
return "Ok";
}
}
當請求Get
方法后,你就可以在控制台中看到看到輸出的“ValuesController.Get”
如果你想要顯式指定日志類別,則可以使用ILoggerFactory.CreateLogger
方法:
public class ValuesController : ControllerBase
{
private readonly ILogger _logger1;
public ValuesController(ILoggerFactory loggerFactory)
{
_logger1 = loggerFactory.CreateLogger("MyCategory");
}
}
配置日志
默認模板中,日志的配置如下(在appsettings.{Environment}.json文件中):
{
"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
。
針對指定的日志記錄提供程序進行配置
接下來看一下如何針對指定的日志記錄提供程序進行配置,先上示例:
{
"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
,如果想要修改,則必須顯式進行指定,如:
{
"Logging": {
"EventLog": {
"LogLevel": {
"Default": "Information"
}
}
}
}
配置的篩選原理
當創建ILogger<TCategoryName>
的對象實例時,ILoggerFactory
根據不同的日志記錄提供程序,將會:
- 查找匹配該日志記錄提供程序的配置。如果找不到,則使用通用配置。
- 然后匹配擁有最長前綴的配置類別。如果找不到,則使用
Default
配置。 - 如果匹配到了多條配置,則采用最后一條。
- 如果沒有匹配到任何配置,則使用
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
進行修改:
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<Startup>();
});
日志記錄過濾器
通過日志記錄過濾器,允許你書寫復雜的邏輯,來控制是否要記錄日志。
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging
// 針對所有 LoggerProvider 設置 Microsoft 最小日志級別,建議通過配置文件進行配置
.AddFilter("Microsoft", LogLevel.Trace)
// 針對 ConsoleLoggerProvider 設置 Microsoft 最小日志級別,建議通過配置文件進行配置
.AddFilter<ConsoleLoggerProvider>("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<ConsoleLoggerProvider>((category, logLevel) =>
{
if (category == typeof(ValuesController).FullName
&& logLevel <= LogLevel.Warning)
{
// false:不記錄日志
return false;
}
// true:記錄日志
return true;
});
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
日志消息模版
應用開發過程中,對於某一類的日志,我們希望它們的消息格式保持一致,僅僅是某些參數發生變化。這就要用到日志消息模板了。
舉個例子:
[HttpGet("{id}")]
public int Get(int id)
{
_logger.LogInformation("Get {Id}", id);
return id;
}
其中Get {Id}
就是一個日志消息模板,{Id}
則是模板參數(注意,請在里面書寫名稱,而不是數字,這樣更容易理解參數含義)。
不過,需要注意的是,{Id}
這個模板參數,僅僅是用於讓人容易理解其含義的,和后面的參數名沒有任何關系,模板值關心參數的順序。例如:
[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
安裝Nuget包:Install-Package Serilog.AspNetCore
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
{
"Logging": {
"File": {
"Path": "logs/host.log"
}
}
}
控制台日志格式配置
控制台日志記錄提供程序是我們開發過程中必不可少的,通過上面我們已經得知可以通過AddConsole()
進行添加。不過它的局限性比較大,日志格式我們都無法進行自定義。
因此,在.NET 5中,對控制台日志記錄提供程序進行了擴展,預置了三種日志輸出格式:Json、Simple、Systemd。
實際上,之前也有枚舉
ConsoleLoggerFormat
提供了Simple和Systemd格式,不過不能進行自定義,已經棄用了。
這些 Formatter 均繼承自抽象類ConsoleFormatter
,該抽象類構造函數接收一個“名字”參數,要求其實現類必須擁有名字。你可以通過靜態類ConsoleFormatterNames
獲取到內置的三種格式的名字。
public abstract class ConsoleFormatter
{
protected ConsoleFormatter(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
public string Name { get; }
public abstract void Write<TState>(in LogEntry<TState> 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
,該選項類包含以下三個屬性:
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()
一致。
.ConfigureLogging(logging =>
{
logging.ClearProviders()
.AddSimpleConsole();
}
示例輸出:
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
進行一些自定義配置:
.ConfigureLogging(logging =>
{
logging.ClearProviders()
.AddSimpleConsole(options =>
{
// 一條日志消息展示在同一行
options.SingleLine = true;
options.IncludeScopes = true;
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ";
options.UseUtcTimestamp = false;
});
}
示例輸出:
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,那你對它也一定不陌生。
.ConfigureLogging(logging =>
{
logging.ClearProviders()
.AddSystemdConsole();
}
示例輸出:
<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 入門教程:命令篇
JsonConsoleFormatter
通過擴展方法AddJsonConsole()
可以添加支持Json
格式的控制台日志記錄提供程序。
.ConfigureLogging(logging =>
{
logging.ClearProviders()
.AddJsonConsole(options =>
{
options.JsonWriterOptions = new JsonWriterOptions
{
// 啟用縮進,看起來更舒服
Indented = true
};
});
}
示例輸出:
{
"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}"
}
}
如果你同時添加了多種格式的控制台記錄程序,那么只有最后一個添加的生效。
以上介紹的是通過代碼進行控制台日志記錄提供程序的設置,不過我想大家應該更喜歡通過配置去設置日志記錄提供程序。下面是一個簡單地配置示例:
{
"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
}
}
}
}
}
ILogger<TCategoryName>對象實例的創建
講到這里,不知道你會不會對ILogger<TCategoryName>
對象實例的創建有疑惑:它到底是如何被new
出來的呢?
要解決這個問題,我們先從AddLogging()
擴展方法入手:
public static class LoggingServiceCollectionExtensions
{
public static IServiceCollection AddLogging(this IServiceCollection services)
{
return AddLogging(services, builder => { });
}
public static IServiceCollection AddLogging(this IServiceCollection services, Action<ILoggingBuilder> configure)
{
services.AddOptions();
// 注冊單例 ILoggerFactory
services.TryAdd(ServiceDescriptor.Singleton<ILoggerFactory, LoggerFactory>());
// 注冊單例 ILogger<>
services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>)));
// 批量注冊單例 IConfigureOptions<LoggerFilterOptions>
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<LoggerFilterOptions>>(
new DefaultLoggerLevelConfigureOptions(LogLevel.Information)));
configure(new LoggingBuilder(services));
return services;
}
}
你可能也猜到了,這個Logger<>
不會是LoggerFactory
創建的吧?要不然注冊個這玩意干嘛呢?
別着急,咱們接着先查看ILogger<>
服務的實現類Logger<>
:
public interface ILogger
{
void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter);
// 檢查能否記錄該日志等級的日志
bool IsEnabled(LogLevel logLevel);
IDisposable BeginScope<TState>(TState state);
}
public interface ILogger<out TCategoryName> : ILogger
{
}
public class Logger<T> : ILogger<T>
{
// 接口實現內部均是使用該實例進行操作
private readonly ILogger _logger;
// 果不其然,注入了 ILoggerFactory 實例
public Logger(ILoggerFactory factory)
{
// 還記得嗎?上面提到顯式指定日志類別時,也是這樣創建 ILogger 實例的
_logger = factory.CreateLogger(TypeNameHelper.GetTypeDisplayName(typeof(T), includeGenericParameters: false, nestedTypeDelimiter: '.'));
}
// ...
}
沒錯,你猜對了,那就來看看這個LoggerFactory
吧(只列舉核心代碼):
public interface ILoggerFactory : IDisposable
{
ILogger CreateLogger(string categoryName);
void AddProvider(ILoggerProvider provider);
}
public class LoggerFactory : ILoggerFactory
{
// 用於單例化 Logger<>
private readonly Dictionary<string, Logger> _loggers = new Dictionary<string, Logger>(StringComparer.Ordinal);
// 存放 ILoggerProviderRegistrations
private readonly List<ProviderRegistration> _providerRegistrations = new List<ProviderRegistration>();
private readonly object _sync = new object();
public LoggerFactory(IEnumerable<ILoggerProvider> providers, IOptionsMonitor<LoggerFilterOptions> filterOption, IOptions<LoggerFactoryOptions> 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>
即可。 - 不支持在
Startup.ConfigureServices
方法中使用ILogger
,因為此時DI容器還未配置完成。 - 沒有異步的日志記錄方法。日志記錄動作執行應該很快,不值的犧牲性能使用異步方法。如果日志記錄動作比較耗時,如記錄到MSSQL中,那么請不要直接寫入MSSQL。你應該考慮先將日志寫入到快速存儲介質,如內存隊列,然后通過后台工作線程將其從內存轉儲到MSSQL中。
- 無法使用日志記錄 API 在應用運行時更改日志記錄配置。不過,一些配置提供程序(如文件配置提供程序)可重新加載配置,這可以立即更新日志記錄配置。
小結
Host.CreateDefaultBuilder
方法中,默認添加了Console
、Debug
、EventSource
和EventLog
(僅Windows)共四種日志記錄提供程序(Logger Provider)。- 通過注入服務
ILogger<TCategoryName>
,可以方便的進行日志記錄。 - 可以通過代碼或配置對日志記錄提供程序進行設置,如
LogLevel
、FormatterName
等。 - 可以通過擴展方法
AddFilter
添加日志記錄過濾器,允許你書寫復雜的邏輯,來控制是否要記錄日志。 - 支持日志消息模板。
- 對於控制台記錄日志程序,.NET框架內置了
Simple
(默認)、Systemd
、Json
三種日志輸出格式。 - .NET 6 預覽版中新增了一個稱為“編譯時日志記錄源生成”的功能,該功能非常實用,有興趣的可以先去了解一下。
- 最后,給大家列舉一些常用的日志開源中間件: