前言
魯迅都說:沒有日志的系統不能上線(魯迅說:這句我沒說過,但是在理)!日志對於一個系統而言,特別重要,不管是用於事務審計,還是用於系統排錯,還是用於安全追蹤.....都扮演了很重要的角色;之前有很多第三方的日志框架也很給力,如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綜藝圈",識別關注跟我一起學~~~