ASP.NET Core 所使用的 Logging

  • 1831
  • 0

誠如前面文章所談的內容,在網站建置的過程中有些基礎工程,這篇文章接著談論下一個基礎工程 - Logging.

很快的時間來到了四月底,從上一次 RC1 的釋出後到現在已經約半年的時間了,而 RC2 什麼時候釋出呢,我也不知道,不管它何時釋出,這段時間其實還是可以看到陸陸續續做了一些小改變,位於工具鏈中尾端的我也很倒霉,因為前面改變若影響到我,我也得馬上配合跟著改變自己的程式.這對寫介紹文章來說也會造成一些麻煩,因為程式改了之後,文章常常會忘了更改.所以若你看之前的文章發現有些東西已經不一樣時,再麻煩你通知我進行修改.因為這樣的情況,所以我也在思考未來的文章就盡量以大方向與高角度的視角來介紹,因為談的太細節時,說不定下一個釋出又會被動了.

這一篇文章的內容將討論 Logging.它和 Configuration 一樣都是定義在 Microsoft.Extensions namespace 之下,並沒有把它歸類在 ASPNetCore 之中,這表示你可以把它用在 ASP.NET Core 的範圍之外的程式,比如你可以拿到你其他的 .Net 專案上使用,並不用局限在 ASP.NET.我相信很多做專案有數年經驗的團隊與公司一定都會有自己慣用的 Logging 元件或系統, 其實你可以沿用你慣用的方式來執行 Logging,Microsoft.Extenstions.Logging 並不是必要的.若你要使用你慣用的 Log 元件,你只要確定你的 Log 元件在 .net core 之下也能正常執行即可,假設你需要跨平台的話.

設計

在討論 Log 系統時,我們需要知道要記錄什麼資料,然後記錄到什麼地方.記錄什麼資料這件事就是你自己程式裡面所寫的,比如說,當 Exception 發生時,需要把這個錯誤記下來,或是發生某一特別事件時,需要把它記下來.至於記錄到什麼地方去這件事,它通常會由你的 Log 元件來支援,比如說要把資料寫到檔案,寫到資料庫,或是顯示在 Console 上等等.然後透過簡單的設定就可以將你要記錄的資料寫入到目標地.Microsoft.Extenstions.Logging 基本上的想法也是如何,所以它定義了你的資料該怎麼記,然後也定義了該怎麼處理資料.它裡面有三個重要的 interfaces: ILoggerFactory, ILooger, ILoggerProvider.

簡單的說,ILoggerProvider  是用來產生 ILogger,而 ILogger 是用來執行記錄資料.從底下的程式碼你可以看到這個關係.

    public interface ILoggerProvider : IDisposable
    {
        ILogger CreateLogger(string categoryName);
    }
   public interface ILogger
    {
        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);
    }

ILoggerFactory: 這裡面簡單定義了兩個動作,一個是 CreateLogger(),另一個是 AddProvider().

    public interface ILoggerFactory : IDisposable
    {
        ILogger CreateLogger(string categoryName);
        void AddProvider(ILoggerProvider provider);
    }

如果你看多了 ASP.NET source code 後,你會常發現很多地方都有 Factory 的存在,在 Logging 也可以看的到.這是一個工廠的概念,它可以讓你把機器 (ILogger) 放進來,也可以讓你把產生機器的東西 (ILoggerProvider) 也放進來.從這概念上可以了解到未來要實做這介面的人, 一定要能儲存多個 ILogger 以及 ILoggerProvider.透過工廠這樣的容器可以讓你定義多個不同的記錄方式,比如在某些程式碼中需要把資料記錄到檔案,而在其他地方需要把資料記錄到事件檢視器.工廠的概念就是可以來實現這樣的需求.

因此,實做 ILogger 介面的人就必須明確定寫出記錄的動作是什麼.比如,Microsoft.Extensions.Logging.Console namespace 裡面的 ConsoleLogger 

public class ConsoleLogger : ILogger
{
        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }

            if (formatter == null)
            {
                throw new ArgumentNullException(nameof(formatter));
            }

            var message = formatter(state, exception);

            if (!string.IsNullOrEmpty(message))
            {
                WriteMessage(logLevel, Name, eventId.Id, message);
            }

            if (exception != null)
            {
                WriteException(logLevel, Name, eventId.Id, exception);
            }
        }
}

你可以預見 WriteMessage() , WriteException() 的內容一定會有 Console.Write 之類的動作,把資料顯示在 Console 上.接下來,你會馬上想到,那這個 Console 是怎麼來的呢 ? 就是透過實做 ILoggerProvider 的 ConsoleProvider 來的

public class ConsoleLoggerProvider : ILoggerProvider
{
        public ILogger CreateLogger(string name)
        {
            return _loggers.GetOrAdd(name, CreateLoggerImplementation);
        }

        private ConsoleLogger CreateLoggerImplementation(string name)
        {
            return new ConsoleLogger(name, GetFilter(name, _settings), _settings.IncludeScopes);
        }

        public ConsoleLogger(string name, Func<string, LogLevel, bool> filter, bool includeScopes)
        {
            if (name == null)
            {
                throw new ArgumentNullException(nameof(name));
            }

            Name = name;
            Filter = filter ?? ((category, logLevel) => true);
            IncludeScopes = includeScopes;

            if (PlatformServices.Default.Runtime.OperatingSystem.Equals("Windows", StringComparison.OrdinalIgnoreCase))
            {
                Console = new WindowsLogConsole();
            }
            else
            {
                Console = new AnsiLogConsole(new AnsiSystemConsole());
            }
        }
}

從上面的程式碼你可以看到 Console 就是透過 WindowsLogConsole() (Windows用的Console) 或 AnsiLogConsole() (非Windows用的Console) 來產生的.

你可能會想為何這麼麻煩,為何不把 ConsoleLogger 和 ConsoleProvider 整合在一起就好了.要這樣做也是可以的,只是這並不是 Microsoft.Extensions.Logging 的設計想法.它的設計想法是當工廠裡面有多個機器時,當某一個資料要進行記錄時,工廠裡面的每台機器都要執行記錄的工作.工廠提供的 CreateLogger() 方式會依照你給的資料類別名稱來建立或取得 Logger,如果是建立 Logger 的話,它會提供工廠裡面所有的 provider,然後依照每個 provider 的內容來建立機器,最後全部結構包起來回傳給你 ILogger.當你執行 ILogger 裡面定義的 Log() 時,它就會把所有的機器的 Log() 動作都執行了一次.舉例來說,工廠裡面加了一個 ConsoleProvider,然後也加了 DatabaseProvider,你今天建立一個資料類別名稱叫 Test,所以當你執行 Factory.CreateLogger("Test") 時,它會回傳一個 ILogger 給你,而其實它裡面包含了 ConsoleLogger 和 DatabaseLogger (透過各自的 provider 建立起來的),所以當你執行這個 ILogger 的 Log() 動作時,其實背後就是呼叫了 ConsoleLogger 和 DatabaseLogger 的 Log() 動作.而接下來你可能建立一個新的資料類別叫 TestAgain,然後加入新的 Provider,所以這個新的 ILogger 裡面就會有三個 provider,而前面那一個 Test ILogger 仍保持原來的兩個 provider.所以使用新的 TestAgain ILogger 時就會記錄到三個地方.這樣可以讓 ILogger 在產生時才建立出 Logger 的內容,而不是事先建立好才傳給 ILogger.讓程式運作時能多些彈性空間.總之,這只是一個設計的想法,沒絕對好也沒絕對壞,找出自己適用而且也能維護的想法就好.

使用的程式碼都是從原始程式碼上節錄下來,原始程式碼請參考 https://github.com/aspnet/Logging/tree/release/src

在 ASP.NET Core 中使用 Logging

在 ASP.NET Core 使用 Logging 是很簡單的一件事.基本上它和其他資料結構一樣都是透過 Dependency Injection 方式在 runtime 程式裡面傳遞 (未來文章再來介紹這個方式).下面的例子用 ConsoleLogger 做為範例.

首先到 Startup.cs 裡面的 Configure(),把 ConsoleProvider 加到工廠裡

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole();

AddConsole() 是在 Microsoft.Extensions.Logging.Console 裡面提供的一個 extension method,它會幫你把 ConsoleProvider 加入到工廠裡面.如果你還需要加入其他的 provider,也可以繼續地加進去.

當你在 MVC 裡面的 Controller 要執行記錄的動作時,可參考下列程式碼

    public class HomeController : Controller
    {
        private ILogger<HomeController> _logger;

        public HomeController(ILogger<HomeController> logger)
        {
            _logger = logger;
        }

        public IActionResult Index()
        {
            _logger.LogInformation("Hit Home Index()");
            return View();
        }

上述程式碼是透過 ILogger<HomeController> 來使用 Logging,其實它的動作就是會到工廠裡執行 CreateLogger(),而資料類別名稱就叫 HomeController,因此這一個傳進來的 ILogger 裡面就包含了 ConsoleProvider,所以當 LogInformation() 執行時,就是 ConsoleLogger 的 LogInformation() 在執行了,因此當你執行這網站時,瀏覽器執行到 HomeController Index()時,你會在 Console 上看到 Hit Home Index() 的字樣.

整合原有的 Log 元件

如果你團隊已有自製的 Log 元件想整到 Microsoft.Extensions.Logging 裡,好讓你的 Log 元件可以搭配 Logging 工廠一同運作,這是可行的.NLog 團隊已經執行了這一個項目,請參考 https://github.com/NLog/NLog.Extensions.Logging/tree/master/src/NLog.Extensions.Logging

基本上需要知道的內容大部份都在前面有提到.你需要了解如何製作你的 logging provider 以及定義好你的 ILogger 延伸出來物件的 Log() 動作細節.

 

最後

Logging 裡面還有一些資料設定的屬性,如 LogLevel, Scope 之類的東西,在這文章就先跳過它了,未來有機會再寫.希望這文章內容對你有幫助.若有寫錯,請再留言.