在.Net Core中系統自帶的集成了日志系統,看一下如何使用:
第一步先添加LogProvider,這個是為了告訴容器我們日志輸出的來源,LogProvider的目的是創建Logger,在Asp.Net Core中默認添加了3個Provider:
.ConfigureLogging((hostingContext, logging) => { logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); logging.AddConsole(); logging.AddDebug(); logging.AddEventSourceLogger(); })
這里添加的LoggerProvider最終都會被注入到LoggerFactory中,用於創建ILogger。
上述代碼里還看到了AddConfiguration,這是通過配置添加日志過濾條件。后面會具體講解。
添加完Provider之后,就可以從容器中獲取日志對象,獲取的時候有兩種類型可以獲取:
- ILogger<T>:直接獲取日志輸出對象。
- ILoggerFactory:獲取日志工廠,然后調用ILogger CreateLogger(string categoryName);這個方法產生一個日志輸出對象。實際上第一種方式,直接獲取的ILogger<T,>內部也包含了一個ILogger對象,它最終也是調用的內部的ILogger進行操作,自己本身沒有干任何事情。可以看一下ILogger<T>的實現類代碼:
public class Logger<T> : ILogger<T> { private readonly ILogger _logger; /// <summary> /// Creates a new <see cref="Logger{T}"/>. /// </summary> /// <param name="factory">The factory.</param> public Logger(ILoggerFactory factory) { if (factory == null) { throw new ArgumentNullException(nameof(factory)); } _logger = factory.CreateLogger(TypeNameHelper.GetTypeDisplayName(typeof(T))); } IDisposable ILogger.BeginScope<TState>(TState state) { return _logger.BeginScope(state); } bool ILogger.IsEnabled(LogLevel logLevel) { return _logger.IsEnabled(logLevel); } void ILogger.Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { _logger.Log(logLevel, eventId, state, exception, formatter); } }
從這里看出,如果以泛型的方式去獲取,categoryName就是這個類的全稱,基本上就等於typeof(T).FullName,這個會在后續的日志過濾中起到效果。
在Controller中使用日志
這是一個最簡單的例子,實際記錄日志的地方,一般不會在構造里面:
public class HomeController : Controller { public HomeController(ILogger<HomeController> loggerT, ILoggerFactory loggerFactory) { var testLog = loggerFactory.CreateLogger("Test"); loggerT.LogInformation("Info"); testLog.LogInformation("Info"); } }
講到這里你可能會有一些疑問:我添加了多個LogProvider,自然會產生多個ILogger,那這里通過工廠類產生的ILogger到底是哪一個?答案是:一個都不是!這里通過工廠產生的ILogger是系統的默認實現Microsoft.Extensions.Logging.Logger。而在這個默認實現Logger里,有一個LoggerInformation數組。LoggerInformation包含了這些LogProvider產生的ILogger和一些過濾條件,在不添加任何過濾條件的情況下,上述的代碼同時會記錄到3個不同的地方。
添加了多少個LogProvider,這個LoggerInformation數組的長度就為多少。LoggerInformation和LogProvider一一對應。
另外,如果想以注入的方式去獲取ILogger是行不通的,因為在初始化的時候,不會向容器中注冊ILogger的實現,只會注冊ILoggerFactory和ILogger<T>。值得一提的是,每產生一個ILogger(默認實現Logger)都會根據CategoryName緩存起來。
日志過濾
在講日志過濾的時候,我們要先明確日志過濾的篩選的條件有哪些:
- Category:這個值就是在創建ILogger(系統默認的實現Logger)時,傳遞的那個categoryName參數。對於以ILogger<T>方式獲取的日志對象,這個值就是T的類型全稱。
- ProviderType:這個值就很簡單,就是創建該ILogger的LogProvider的類型。
- LogLevel:日志記錄的級別,這是一個系統定義的枚舉類型:
public enum LogLevel { /// <summary> /// Logs that contain the most detailed messages. These messages may contain sensitive application data. /// These messages are disabled by default and should never be enabled in a production environment. /// </summary> Trace = 0, /// <summary> /// Logs that are used for interactive investigation during development. These logs should primarily contain /// information useful for debugging and have no long-term value. /// </summary> Debug = 1, /// <summary> /// Logs that track the general flow of the application. These logs should have long-term value. /// </summary> Information = 2, /// <summary> /// Logs that highlight an abnormal or unexpected event in the application flow, but do not otherwise cause the /// application execution to stop. /// </summary> Warning = 3, /// <summary> /// Logs that highlight when the current flow of execution is stopped due to a failure. These should indicate a /// failure in the current activity, not an application-wide failure. /// </summary> Error = 4, /// <summary> /// Logs that describe an unrecoverable application or system crash, or a catastrophic failure that requires /// immediate attention. /// </summary> Critical = 5, /// <summary> /// Not used for writing log messages. Specifies that a logging category should not write any messages. /// </summary> None = 6, }
這幾個參數和ILogger(LogProvider產生的)會一起被保存到LoggerInformation這個對象中。在LoggerInformation還有一個屬性也是用做日志過濾的:
public Func<string, string, LogLevel, bool> Filter { get; set; }
這是一個委托需要三個參數,分別代表Category,ProviderType,LogLevel,返回布爾值,代表是否啟用該日志類型。當有了這些信息后,每當使用ILogger(系統的默認實現Logger)進行日志書寫時,會循環遍歷LoggerInformation數組對象,這樣就可以獨立控制每一個LogProvider產生的ILogger對象,根據不同的Provider,Category和LogLevel來決定是否需要輸出日志。
接下來的問題就是,如何配置過濾規則?配置過濾規則唯一途徑就是配置LoggerFilterOptions這個對象。如何自己配置一個Option對象,請參考我同系列的前一篇文章。
系統提供了一系列的的擴展方法幫我們去配置這個對象,詳情可以參考:https://github.com/aspnet/Extensions/blob/master/src/Logging/Logging/src/FilterLoggingBuilderExtensions.cs,也有基於配置的擴展,其核心代碼是:https://github.com/aspnet/Extensions/blob/master/src/Logging/Logging.Configuration/src/LoggerFilterConfigureOptions.cs 那這個LoggerFilterOptions又是什么呢,一起看一下源代碼:
public class LoggerFilterOptions { /// <summary> /// Gets or sets the minimum level of log messages if none of the rules match. /// </summary> public LogLevel MinLevel { get; set; } /// <summary> /// Gets the collection of <see cref="LoggerFilterRule"/> used for filtering log messages. /// </summary> public IList<LoggerFilterRule> Rules { get; } = new List<LoggerFilterRule>(); }
系統提供的所有擴展方法,都是往這個對象的Rules添加Rule,然后在創建一個ILogger(LogProvider創建的)的時候根據一定的規則去匹配到屬於這個ILogger(LogProvider創建的)的那條Rule,賦值給LoggerInformation的Filter屬性。也就是說,即使有很多條Rule,但是最終只會找到最符合條件的那條Rule。如果一條都找不到,那就使用MinLevel。默認情況下MinLevel是Information。而這個LoggerFilterRule包含的屬性,就是前面提到的那幾個篩選條件:
public class LoggerFilterRule { public LoggerFilterRule(string providerName, string categoryName, LogLevel? logLevel, Func<string, string, LogLevel, bool> filter) { ProviderName = providerName; CategoryName = categoryName; LogLevel = logLevel; Filter = filter; } /// <summary> /// Gets the logger provider type or alias this rule applies to. /// </summary> public string ProviderName { get; } /// <summary> /// Gets the logger category this rule applies to. /// </summary> public string CategoryName { get; } /// <summary> /// Gets the minimum <see cref="LogLevel"/> of messages. /// </summary> public LogLevel? LogLevel { get; } /// <summary> /// Gets the filter delegate that would be applied to messages that passed the <see cref="LogLevel"/>. /// </summary> public Func<string, string, LogLevel, bool> Filter { get; } public override string ToString() { return $"{nameof(ProviderName)}: '{ProviderName}', {nameof(CategoryName)}: '{CategoryName}', {nameof(LogLevel)}: '{LogLevel}', {nameof(Filter)}: '{Filter}'"; } }
那查找Rule的匹配規則又是什么呢?核心代碼如下(可以直接看后面的解釋):
internal class LoggerRuleSelector { public void Select(LoggerFilterOptions options, Type providerType, string category, out LogLevel? minLevel, out Func<string, string, LogLevel, bool> filter) { filter = null; minLevel = options.MinLevel; // Filter rule selection: // 1. Select rules for current logger type, if there is none, select ones without logger type specified // 2. Select rules with longest matching categories // 3. If there nothing matched by category take all rules without category // 3. If there is only one rule use it's level and filter // 4. If there are multiple rules use last // 5. If there are no applicable rules use global minimal level var providerAlias = ProviderAliasUtilities.GetAlias(providerType); LoggerFilterRule current = null; foreach (var rule in options.Rules) { if (IsBetter(rule, current, providerType.FullName, category) || (!string.IsNullOrEmpty(providerAlias) && IsBetter(rule, current, providerAlias, category))) { current = rule; } } if (current != null) { filter = current.Filter; minLevel = current.LogLevel; } } private static bool IsBetter(LoggerFilterRule rule, LoggerFilterRule current, string logger, string category) { // Skip rules with inapplicable type or category if (rule.ProviderName != null && rule.ProviderName != logger) { return false; } if (rule.CategoryName != null && !category.StartsWith(rule.CategoryName, StringComparison.OrdinalIgnoreCase)) { return false; } if (current?.ProviderName != null) { if (rule.ProviderName == null) { return false; } } else { // We want to skip category check when going from no provider to having provider if (rule.ProviderName != null) { return true; } } if (current?.CategoryName != null) { if (rule.CategoryName == null) { return false; } if (current.CategoryName.Length > rule.CategoryName.Length) { return false; } } return true; } }
在配置規則時ProviderName默認應該是類的全稱,而上面提到的ProviderAlias指的是該類的別稱,在類上面添加ProviderAliasAttribute為它定義一個別稱,方便配置。
再來看IsBetter方法:
- 如果傳進來的Rule應用於某一個具體的Provider,並且和當前Log的Provider不一致,則直接返回False,因為不匹配
- 如果傳進來的的Rule應用於某一個具體的Category(默認是類的全名),並且和當前Log的Category不匹配,則返回False,因為不匹配
- 如果當前的Rule應用於某一個具體的Provider,並且傳進來的Rule不是應用於某一個具體的Provider,則返回False,因為當前的優先級更高
- 如果當前的Rule不是應用於某一個具體的Provider,並且傳進來的Rule是應用於某一個具體的Provider(由於第一點的存在,傳進來的Rule的Provider一定是和當前Log一致的,由於第二點的存在,如果傳進來的Rule包含Category,則一定和當前的Log一致),則返回True,因為傳進來的優先級更高
- 如果當前的Rule是應用於某一個具體的Category,並且傳遞進來的Rule不是應用於某一個具體的Category,則返回False,因為當前優先級更高
- 如果當前的Rule是應用於某一個具體的Category,並且當前傳遞進來的Rule也是應用於某一個具體的Category,但是當前的匹配度更高(字符串長度越長),則返回False,因為當前的優先級更高
- 其余都返回True
那么最終Rule的匹配度,由低到高應該是:
- ProviderName: null,CategoryName:null
- ProviderName: null,CategoryName:Matched(匹配的長度越長,說明越匹配)
- ProviderName: Matched,CategoryName:null
- ProviderName: Matched,CategoryName:Matched(匹配的長度越長,說明越匹配)
CategoryName的匹配度指的是字符串長度。如果一個Rule都沒找到,那就使用MinLevel。
最佳實踐
使用配置文件,去配置日志過濾條件,注意:使用配置文件時,只能設置特定的Provider和Category的LogLevel,不能指定委托類型的Filter。想要使用Filter,只能用編碼的方式配置。來看一個日志配置實例:
"Logging": { "LogLevel": { "Default": "Debug" }, "Console": { "LogLevel": { "Default": "Warning", "CoreLogging.Controllers": "Error", "CoreLogging.Controllers.HomeController": "Info" } } }
在Logging下面的直接子節點LogLevel下面配置的,代表Proivder為null,同時子節點的名稱代表Category,值代表LogLevel。
而Logging下面的其它直接子節點,節點名稱代表ProviderName。其下的LogLevel子節點下面的子節點,名稱代表Category,值代表LogLevel。
另外Category的值為Default,最終解析的時候會解析成null,不區分大小寫。
這里的Console可以使用Microsoft.Extensions.Logging.Console.ConsoleLoggerProvider代替,因為ConsoleLoggerProvider有一個特性別稱Console,所以可以不寫全稱。[ProviderAlias("Console")]
最終這個配置會被解釋成:
- 非Console的日志記錄時,只記錄日志級別高於Debug
- Console的日志,如果Category不以CoreLogging.Controllers開頭,則只記錄級別高於Warning的
- Console的日志,如果Category以CoreLogging.Controllers開頭但不以CoreLogging.Controllers.HomeController開頭,則只記錄級別高於Error的
- Console的日志,如果Category以CoreLogging.Controllers.HomeController開頭,則只記錄級別高於Info的
文中提到的源碼都在https://github.com/zhurongbo111/AspNetCoreDemo/tree/master/03-Logging