跟我一起學.NetCore之日誌(Log)模型核心

Code綜藝圈發表於2020-08-27

前言

魯迅都說:沒有日誌的系統不能上線(魯迅說:這句我沒說過,但是在理)!日誌對於一個系統而言,特別重要,不管是用於事務審計,還是用於系統排錯,還是用於安全追蹤.....都扮演了很重要的角色;之前有很多第三方的日誌框架也很給力,如Log4Net、NLog和Serilog等,在.NetCore中也整合了日誌模型,使用便捷,同時很方便的與第三方日誌框架進行整合擴充套件;

正文

例項演示之前,先了解一下日誌級別,後續如果不想輸出全部日誌,可以通過日誌級別進行過濾,同時通過日誌級別可以標註日誌內容的重要程度:

namespace Microsoft.Extensions.Logging
{
    // 日誌級別從下往上遞增,所以根據級別可以過濾掉低階別的日誌資訊
    public enum LogLevel
    {
        Trace,
        Debug,
        Information,
        Warning,
        Error,
        Critical,
        None
    }
}

來一個控制檯程式例項演示:

img

執行結果:

img

咋樣,使用還是依舊簡單,這裡是控制檯程式,還需要寫配置框架和依賴注入相關的程式碼邏輯,如果在WebAPI專案,直接就可以使用日誌記錄了,如下:

img

對於WebAPI專案而言,在專案啟動流程分析的時候,就提到內部已經註冊了相關服務了,所以才能這樣如此簡單的使用;

難道日誌就這樣結束了嗎?猜想看到這的小夥伴也不甘心,是的,得進一步瞭解,不需要特別深入,但至少得知道關鍵嘛,對不對?

老規矩,程式中能看到日誌相關點,當然就從這開始,看看是如何註冊日誌啊相關服務的:

img

對應程式碼:

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綜藝圈",識別關注跟我一起學~~~

img

相關文章