.Net Core中的日志Logging使用以及源碼解析


在.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方法:

  1. 如果傳進來的Rule應用於某一個具體的Provider,並且和當前Log的Provider不一致,則直接返回False,因為不匹配
  2. 如果傳進來的的Rule應用於某一個具體的Category(默認是類的全名),並且和當前Log的Category不匹配,則返回False,因為不匹配
  3. 如果當前的Rule應用於某一個具體的Provider,並且傳進來的Rule不是應用於某一個具體的Provider,則返回False,因為當前的優先級更高
  4. 如果當前的Rule不是應用於某一個具體的Provider,並且傳進來的Rule是應用於某一個具體的Provider(由於第一點的存在,傳進來的Rule的Provider一定是和當前Log一致的,由於第二點的存在,如果傳進來的Rule包含Category,則一定和當前的Log一致),則返回True,因為傳進來的優先級更高
  5. 如果當前的Rule是應用於某一個具體的Category,並且傳遞進來的Rule不是應用於某一個具體的Category,則返回False,因為當前優先級更高
  6. 如果當前的Rule是應用於某一個具體的Category,並且當前傳遞進來的Rule也是應用於某一個具體的Category,但是當前的匹配度更高(字符串長度越長),則返回False,因為當前的優先級更高
  7. 其余都返回True

那么最終Rule的匹配度,由低到高應該是:

  1. ProviderName: null,CategoryName:null
  2. ProviderName: null,CategoryName:Matched(匹配的長度越長,說明越匹配)
  3. ProviderName: Matched,CategoryName:null
  4. 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")]

最終這個配置會被解釋成:

  1. 非Console的日志記錄時,只記錄日志級別高於Debug
  2. Console的日志,如果Category不以CoreLogging.Controllers開頭,則只記錄級別高於Warning的
  3. Console的日志,如果Category以CoreLogging.Controllers開頭但不以CoreLogging.Controllers.HomeController開頭,則只記錄級別高於Error的
  4. Console的日志,如果Category以CoreLogging.Controllers.HomeController開頭,則只記錄級別高於Info的

 

文中提到的源碼都在https://github.com/zhurongbo111/AspNetCoreDemo/tree/master/03-Logging


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM