前言
魯迅都說:沒有日誌的系統不能上線(魯迅說:這句我沒說過,但是在理)!日誌對於一個系統而言,特別重要,不管是用於事務審計,還是用於系統排錯,還是用於安全追蹤.....都扮演了很重要的角色;之前有很多第三方的日誌框架也很給力,如Log4Net、NLog和Serilog等,在.NetCore中也整合了日誌模型,使用便捷,同時很方便的與第三方日誌框架進行整合擴充套件;
正文
例項演示之前,先了解一下日誌級別,後續如果不想輸出全部日誌,可以通過日誌級別進行過濾,同時通過日誌級別可以標註日誌內容的重要程度:
namespace Microsoft.Extensions.Logging
{
// 日誌級別從下往上遞增,所以根據級別可以過濾掉低階別的日誌資訊
public enum LogLevel
{
Trace,
Debug,
Information,
Warning,
Error,
Critical,
None
}
}
來一個控制檯程式例項演示:
執行結果:
咋樣,使用還是依舊簡單,這裡是控制檯程式,還需要寫配置框架和依賴注入相關的程式碼邏輯,如果在WebAPI專案,直接就可以使用日誌記錄了,如下:
對於WebAPI專案而言,在專案啟動流程分析的時候,就提到內部已經註冊了相關服務了,所以才能這樣如此簡單的使用;
難道日誌就這樣結束了嗎?猜想看到這的小夥伴也不甘心,是的,得進一步瞭解,不需要特別深入,但至少得知道關鍵嘛,對不對?
老規矩,程式中能看到日誌相關點,當然就從這開始,看看是如何註冊日誌啊相關服務的:
對應程式碼:
namespace Microsoft.Extensions.DependencyInjection
{
// IServiceCollection的擴充套件方法,用於註冊日誌相關服務
public static class LoggingServiceCollectionExtensions
{
public static IServiceCollection AddLogging(this IServiceCollection services)
{
return services.AddLogging(delegate
{
});
}
// 核心方法,上面的方法就是呼叫下面這個
public static IServiceCollection AddLogging(this IServiceCollection services, Action<ILoggingBuilder> configure)
{
if (services == null)
{
throw new ArgumentNullException("services");
}
// 為了支援Options選項,得註冊Options相關服務,上篇講過
services.AddOptions();
// 註冊ILoggerFactory
services.TryAdd(ServiceDescriptor.Singleton<ILoggerFactory, LoggerFactory>());
// 註冊ILogger
services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>)));
// 註冊日誌級別過濾,並預設設定級別為Information
services.TryAddEnumerable(ServiceDescriptor.Singleton((IConfigureOptions<LoggerFilterOptions>)new DefaultLoggerLevelConfigureOptions(LogLevel.Information)));
// 執行傳入的委託方法
configure(new LoggingBuilder(services));
return services;
}
}
}
日誌相關服務註冊瞭解了,那接著看看關鍵實現,其實日誌記錄有三個核心型別:ILogger、ILoggerFactory和ILoggerProvider,對應的實現分別是Logger、LoggerFactory、xxxLoggerProvider;
- xxxLoggerProvider:針對於不同的目的地建立對應的xxxLogger,這裡的xxxLogger負責在目的地(檔案、資料庫、控制檯等)寫入內容;
- LoggerFactory:負責建立Logger,其中包含所有註冊的xxxLoggerProvider對應Logger;
- Logger:以上兩種;
扒開這三個型別的定義,簡單看看都定義了什麼....
-
ILogger/Logger
namespace Microsoft.Extensions.Logging { public interface ILogger { // 記錄日誌方法,其中包含日誌級別、事件ID、寫入的內容、格式化內容等 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); } }
Logger中挑了比較關鍵的屬性和方法簡單說說
internal class Logger : ILogger { // 用於快取真正Logger記錄器的 public LoggerInformation[] Loggers { get; set; } public MessageLogger[] MessageLoggers { get; set; } // 這個用於快取日誌作用域Loggers public ScopeLogger[] ScopeLoggers { get; set; } // Log日誌記錄方法 public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { var loggers = MessageLoggers; if (loggers == null) { return; } List<Exception> exceptions = null; // 遍歷對應的Loggers for (var i = 0; i < loggers.Length; i++) { ref readonly var loggerInfo = ref loggers[i]; if (!loggerInfo.IsEnabled(logLevel)) { continue; } // 執行內部方法 LoggerLog(logLevel, eventId, loggerInfo.Logger, exception, formatter, ref exceptions, state); } if (exceptions != null && exceptions.Count > 0) { ThrowLoggingError(exceptions); } static void LoggerLog(LogLevel logLevel, EventId eventId, ILogger logger, Exception exception, Func<TState, Exception, string> formatter, ref List<Exception> exceptions, in TState state) { try { // 記錄日誌內容 logger.Log(logLevel, eventId, state, exception, formatter); } catch (Exception ex) { if (exceptions == null) { exceptions = new List<Exception>(); } exceptions.Add(ex); } } } }
ILoggerFactory/LoggerFactory
namespace Microsoft.Extensions.Logging { // 建立 ILogger和註冊LoggerProvider public interface ILoggerFactory : IDisposable { // 根據名稱建立ILogger ILogger CreateLogger(string categoryName); // 註冊ILoggerProvider void AddProvider(ILoggerProvider provider); } } ........省略方法-私下研究......
// LoggerFactory挑了幾個關鍵方法進行說明 // 建立Logger public ILogger CreateLogger(string categoryName) { if (CheckDisposed()) { throw new ObjectDisposedException(nameof(LoggerFactory)); } lock (_sync) { if (!_loggers.TryGetValue(categoryName, out var logger)) { // new一個Logger,這是LoggerFactory管理的Logger logger = new Logger { // 根據註冊的xxxLoggerProvider建立具體的xxxLogger // 並將其快取到LoggerFactory建立的Logger對應的Loggers屬性中 Loggers = CreateLoggers(categoryName), }; // 根據訊息級別和作用域範圍,賦值對應的MessageLoggers、ScopeLoggers (logger.MessageLoggers, logger.ScopeLoggers) = ApplyFilters(logger.Loggers); // 同時將建立出來的logger快取在字典中 _loggers[categoryName] = logger; } return logger; } } // 這個用於註冊具體的xxxLoggerProvider public void AddProvider(ILoggerProvider provider) { if (CheckDisposed()) { throw new ObjectDisposedException(nameof(LoggerFactory)); } lock (_sync) { // 將傳入的provider封裝了結構體進行快取 AddProviderRegistration(provider, dispose: true); // 同時建立對應的logger,建立過程和上面一樣 foreach (var existingLogger in _loggers) { var logger = existingLogger.Value; var loggerInformation = logger.Loggers; // 在原來基礎上增加具體的xxxLogger var newLoggerIndex = loggerInformation.Length; Array.Resize(ref loggerInformation, loggerInformation.Length + 1); loggerInformation[newLoggerIndex] = new LoggerInformation(provider, existingLogger.Key); logger.Loggers = loggerInformation; (logger.MessageLoggers, logger.ScopeLoggers) = ApplyFilters(logger.Loggers); } } } // 封裝對應的xxxLoggerProvider,然後進行快取 private void AddProviderRegistration(ILoggerProvider provider, bool dispose) { // 先封裝成結構體,然後在快取,方便後續生命週期管理 _providerRegistrations.Add(new ProviderRegistration { Provider = provider, ShouldDispose = dispose }); // 判斷是否繼承了ISupportExternalScope if (provider is ISupportExternalScope supportsExternalScope) { if (_scopeProvider == null) { _scopeProvider = new LoggerExternalScopeProvider(); } supportsExternalScope.SetScopeProvider(_scopeProvider); } } // 建立具體的xxxLogger private LoggerInformation[] CreateLoggers(string categoryName) { // 根據註冊的xxxLoggerProvider個數初始化一個陣列 var loggers = new LoggerInformation[_providerRegistrations.Count]; // 遍歷註冊的xxxLoggerProvider,建立具體的xxxLogger for (var i = 0; i < _providerRegistrations.Count; i++) { // 建立具體的xxxLogger loggers[i] = new LoggerInformation(_providerRegistrations[i].Provider, categoryName); } return loggers; } ........省略方法-私下研究......
-
ILoggerProvider/xxxLoggerProvider
namespace Microsoft.Extensions.Logging { public interface ILoggerProvider : IDisposable { // 根據名稱建立對應的Logger ILogger CreateLogger(string categoryName); } }
namespace Microsoft.Extensions.Logging.Console { [ProviderAlias("Console")] public class ConsoleLoggerProvider : ILoggerProvider, ISupportExternalScope { // 支援Options動態監聽 private readonly IOptionsMonitor<ConsoleLoggerOptions> _options; // 快取對應的xxxLogger private readonly ConcurrentDictionary<string, ConsoleLogger> _loggers; // 日誌處理 private readonly ConsoleLoggerProcessor _messageQueue; private IDisposable _optionsReloadToken; private IExternalScopeProvider _scopeProvider = NullExternalScopeProvider.Instance; // 建構函式,初始化 public ConsoleLoggerProvider(IOptionsMonitor<ConsoleLoggerOptions> options) { _options = options; _loggers = new ConcurrentDictionary<string, ConsoleLogger>(); ReloadLoggerOptions(options.CurrentValue); _optionsReloadToken = _options.OnChange(ReloadLoggerOptions); _messageQueue = new ConsoleLoggerProcessor(); // 判斷是否是Windows系統,因為即日至的方式不一樣 if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { // 如果是windows _messageQueue.Console = new WindowsLogConsole(); _messageQueue.ErrorConsole = new WindowsLogConsole(stdErr: true); } else { // 如果是其他平臺 _messageQueue.Console = new AnsiLogConsole(new AnsiSystemConsole()); _messageQueue.ErrorConsole = new AnsiLogConsole(new AnsiSystemConsole(stdErr: true)); } } private void ReloadLoggerOptions(ConsoleLoggerOptions options) { foreach (var logger in _loggers) { logger.Value.Options = options; } } // 根據名稱獲取或建立對應xxxLogger public ILogger CreateLogger(string name) { // 根據名稱獲取,如果沒有,則根據傳入的委託方法進行建立 return _loggers.GetOrAdd(name, loggerName => new ConsoleLogger(name, _messageQueue) { Options = _options.CurrentValue, ScopeProvider = _scopeProvider }); } ......省略一些方法,私下研究....... } }
想了想,這裡就不一一針對不同目的地(比如Trace、EventLog)扒程式碼看了,不然說著說著就變成了程式碼解讀了,如果有興趣,可以私下照著以下思路去看看程式碼:
每一個目的地日誌記錄都會有一個實現xxxLoggerProvider和對應的記錄器xxxLogger(真實記錄日誌內容),LoggerFactory建立的Logger(暴露給程式設計師使用的)包含了對應的具體的記錄器,比如以寫入日誌控制檯為例:
有一個ConsoleLoggerProvider的實現和對應的ConsoleLogger,ConsoleLoggerProvider負責通過名稱建立對應的ConsoleLogger,而LoggerFactory建立出來的Logger就是包含已註冊ConsoleLoggerProvider建立出來的ConsoleLogger;從而我們呼叫記錄日誌方法的時候,其實最終是呼叫ConsoleLoggerProvider建立的ConsoleLogger物件方法;
總結
本來想著日誌應該用的很頻繁了,直接舉例演示就OK了,但是寫著寫著,用的多不一定清除關鍵步驟,於是又扒了下程式碼,挑出了幾個關鍵方法簡單的說說,希望使用的小夥伴不困惑,深入研究就靠私下好好瞅瞅程式碼了;
下一節例項演示日誌的使用、日誌的作用域、整合第三方日誌框架進行日誌擴充套件.....
------------------------------------------------
一個被程式搞醜的帥小夥,關注"Code綜藝圈",識別關注跟我一起學~~~