ASP.NET Core 中文文檔 第三章 原理(8)日志


原文:Logging
作者:Steve Smith
翻譯:劉怡(AlexLEWIS)
校對:何鎮汐許登洋(Seay)

ASP.NET Core 內建支持日志,也允許開發人員輕松切換為他們想用的其他日志框架。盡量用最少的代碼來實現應用程序日志,只要做到這點,就能想在哪里加就能在那里加日志記錄。

章節:

訪問或下載樣例代碼

在應用程序中實現日志

通過 dependency-injection 請求 ILoggerFactoryILogger<T> 可為應用程序增加日志功能。如果請求了 ILoggerFactory,日志記錄器就必須使用它的 CreateLogger 方法,在下例中將展示如何做到這一點:

var logger = loggerFactory.CreateLogger("Catchall Endpoint");
logger.LogInformation("No endpoint found for request {path}", context.Request.Path);

當日志記錄器創建時,需要提供類別名稱。類別名稱指定了日志記錄事件的根源。根據約定,這一字符串通過點符號(.)來分割以體現其層次性。一些日志記錄提供程序還提供了過濾功能,這使得輸出的日志更易被檢索。在本文的示例應用程序中,日志被配置為使用內建的 ConsoleLogger (查閱下文的 在應用程序中配置日志_ 一節)。使用 dotnet run 命令運行應用程序,並請求已配置的 URL(localhost:5000),查看運行中的控制台記錄器。你將看到如下輸出:

你可能會發現每次通過瀏覽器發出一個 Web 請求都會產生超過一條日志記錄,這是因為大多數瀏覽器嘗試加載一個頁面的時候會發出多個請求(既請求網站的圖標文件)。注意一點,在控制台記錄器顯示的日志級別(比如上圖中的 info),然后是類別([Catchall Endpoint]),最后是日志消息。

日志方法被調用時,可以利用命名占位符(如 {path} )來實現格式化。按照方法調用時傳入參數的順序一一填充出現在其中的占位符。一些日志提供程序會用一個字典來保存名稱和映射值以供以后使用。在下例中,請求路徑通過命名占位符傳入:

logger.LogInformation("No endpoint found for request {path}", context.Request.Path);

在實際的應用程序中,你會希望基於應用程序級別來添加日志,而不是基於框架級別或事件。例如,你創建了一個 Web API 應用程序來管理 To-Do 條目(參見 用 Visual Studio 和 ASP.NET Core MVC 創建首個 Web API ),你可能會為這些條目的各種操作添加日志記錄。

對於 API 的邏輯部分被包含在 TodoController 中,在構造函數中通過依賴注入( dependency-injection )的方式來請求需要的服務。理想情況下,類應當像這個例子一樣使用構造函數來 顯式定義它們的依賴項 並使其作為參數傳入,而不是請求 ILoggerFactory 並顯式創建 ILogger 實例。 TodoController 展示了另一種應用程序使用日志記錄器的方法——通過請求 ILogger<T> (其中 T 是所請求的記錄器的類)。

[Route("api/[controller]")]
public class TodoController : Controller
{
    private readonly ITodoRepository _todoRepository;
    private readonly ILogger<TodoController> _logger;

    public TodoController(ITodoRepository todoRepository, 
        ILogger<TodoController> logger)
    {
        _todoRepository = todoRepository;
        _logger = logger;
    }

    [HttpGet]
    public IEnumerable<TodoItem> GetAll()
    {
        _logger.LogInformation(LoggingEvents.LIST_ITEMS, "Listing all items");
        EnsureItems();
        return _todoRepository.GetAll();
    }

在每個控制器 Action 內,通過本地字段 _logger (上例第 17 行所示)來記錄日志。這種技術並不僅限於控制器內,通過 dependency-injection 它能被用於應用程序內所有的服務中。

使用 ILogger

如您所見,應用程序可通過構造函數請求到 ILogger<T> 實例,其中 T 是執行日志記錄的類型。TodoController 就是用了這種方法。當使用這種技術時,日志記錄器會自動使用該類型的名稱作為其日志類別的名稱。通過請求 ILogger<T> 實例,類自己不必通過 ILoggerFactory 來實例化日志記錄器。這種方法可以用在所有的地方——而不必使用 ILoggerFactory

日志記錄級別

當應用程序添加一條日志記錄時,必須指定 日志級別 。日志級別允許你控制應用程序輸出日志的詳細程度,以及把不同類型的日志傳送給不同的日志記錄器。比方說,你可能會希望調試消息在一個本地文件,而把錯誤消息記錄到計算機的事件日志或數據庫中。

ASP.NET Core 詳盡地定義了六個日志級別,通過增加重要性或嚴重程度排序:

Trace
用於記錄最詳細的日志消息,通常僅用於開發階段調試問題。這些消息可能包含敏感的應用程序數據,因此不應該用於生產環境。默認應禁用。舉例:Credentials: {"User":"someuser", "Password":"P@ssword"}

Debug
這種消息在開發階段短期內比較有用。它們包含一些可能會對調試有所助益、但沒有長期價值的信息。默認情況下這是最詳細的日志。舉例: Entering method Configure with flag set to true

Information
這種消息被用於跟蹤應用程序的一般流程。與 Verbose 級別的消息相反,這些日志應該有一定的長期價值。舉例: Request received for path /foo

Warning
當應用程序出現錯誤或其它不會導致程序停止的流程異常或意外事件時使用警告級別,以供日后調查。在一個通用的地方處理警告級別的異常。舉例: Login failed for IP 127.0.0.1FileNotFoundException for file foo.txt

Error
當應用程序由於某些故障停止工作則需要記錄錯誤日志。這些消息應該指明當前活動或操作(比如當前的 HTTP 請求),而不是應用程序范圍的故障。舉例: Cannot insert record due to duplicate key violation

Critical
當應用程序或系統崩潰、遇到災難性故障,需要立即被關注時,應當記錄關鍵級別的日志。舉例:數據丟失、磁盤空間不夠等。

Logging 包為每個 LogLevel 值提供 helper 擴展方法 ,允許你調用,例如, LogInformation ,而不是更多詳盡的 Log(LogLevel.Information, ...) 方法。每個 LogLevel - 特定擴展方法有多個重載,允許你傳遞下面的一些或者是所有的參數:

string data
記錄消息。

EventId eventId
使用數字類型的 id 來標記日志,這樣可以將一系列的事件彼此相互關聯。被記錄的事件 ID 應該是靜態的、特定於指定類型時間的。比如,你可能會把添加商品到購物車的事件 ID 標記為 1000,然后把結單的事件 ID 標記為 1001,以便能智能過濾並處理這些日志記錄。

string format
日志消息的格式字符串。

object[] args
用於格式化的一組對象。

Exception error
用於記錄的異常實例。

注解
EventId 類型可以隱式轉換為 int ,所以,你可以傳遞一個 int 參數。

注解
像本文中所使用的 ConsoleLogger 這類內建的日志記錄器會忽略 eventId 參數。如果你需要顯示它,你可以把它包含在消息文本內。在下例中你可以輕松發現 eventId 被關聯到每一條消息,但實際上你通常不會將它包含在日志信息中。

TodoController 這個例子中,事件 id 常數為每一個事件定義,根據操作是否成功配置日志語句的詳細級別。在這種情況下,成功操作記錄為 Information,數據未發現則記錄為 Warning (不顯示錯誤處理)。

[HttpGet]
public IEnumerable<TodoItem> GetAll()
{
    _logger.LogInformation(LoggingEvents.LIST_ITEMS, "Listing all items");
    EnsureItems();
    return _todoRepository.GetAll();
}

[HttpGet("{id}", Name = "GetTodo")]
public IActionResult GetById(string id)
{
    _logger.LogInformation(LoggingEvents.GET_ITEM, "Getting item {0}", id);
    var item = _todoRepository.Find(id);
    if (item == null)
    {
        _logger.LogWarning(LoggingEvents.GET_ITEM_NOTFOUND, "GetById({0}) NOT FOUND", id);
        return NotFound();
    }
    return new ObjectResult(item);
}

注解
建議在應用程序及其 API 上執行應用程序日志記錄,而不是在框架級別上記錄。框架已經有了一個內建的能夠簡單通過設置啟用相應日志級別的日志記錄器了。

要查看框架級別的詳細日志,可以為日志提供程序調整為指定的日志級別,這樣就能得到更為詳細的日志記錄(如 DebugTrace)。比如,如果你在 Configure 方法中修改 AddConsole 調用的日志級別,改為使用 LogLevel.Trace 並運行應用程序的話,框架級別的每個請求詳細日志就會像下圖這般顯示:

控制台記錄器輸出時使用前綴「dbug: 」,默認的框架沒有追蹤的記錄器,每一個日志級別都有使用對應的四個字符的前綴,使得日志信息始終一致。

Log Level Prefix
Critical crit
Error fail
Warning warn
Information info
Debug dbug
Trace trce

作用域

在應用程序記錄日志信息的過程中,你可以將一組邏輯操作用 作用域 打包為一組。作用域也是一種 IDisposable 類型,通過調用 ILogger.BeginScope<TState> 方法來返回,它自創建起持續到釋放為止。內建的 TraceSource 日志記錄器會返回一個作用域實例用來響應啟動與停止跟蹤操作。任何諸如事務 ID 這樣的日志狀態從剛創建便關聯到作用域了。

作用域不是必須的,而且需要謹慎使用。它們適合用於具有比較明顯的開始和結束的操作,比如在一個事務中調用多個資源。

在應用程序中配置日志

為在 ASP.NET 應用程序中配置日志,你須在 StartupConfigure 方法中解析 ILoggerFactory。ASP.NET 會基於 dependency-injection 以參數的形式自動為 Configure 方法提供 ILoggerFactory 實例。當你把 ILoggerFactory 添作參數時,在 Configure 中,通過在日志記錄器工廠上調用方法(或擴展方法)來配置日志記錄器。我們已在本文開頭處看到,通過簡單地調用 loggerFactory.AddConsole 來加入控制台日志記錄。除了添加日志記錄器,你還可以通過設置日志記錄器工廠的 MinimumLevel 屬性來控制應用程序日志的詳細程度。默認的詳細程度是 Verbose

    public void Configure(IApplicationBuilder app,
        IHostingEnvironment env,
        ILoggerFactory loggerFactory)

一旦你以參數的形式添加了 ILoggerFactory,就配置了一個帶有日志記錄器工廠方法(或擴展方法)的日志記錄器。我們已經看到在這篇文章開頭的配置例子里,我們通過調用 loggerFactory.AddConsole 添加控制台日志記錄。

注解
你可以選擇配置日志記錄,當設置 hosting 時,而不是在啟動時。

每個記錄器為 ILoggerFactory 提供了自己的一套擴展方法。控制台,調試和事件日志記錄器允許你指定那些應該寫的日志消息的最低日志記錄級別。控制台和調試記錄器根據自己的日志記錄級別和/或類別提供的擴展方法接受一個函數來過濾日志消息(例如: logLevel => logLevel >= LogLevel.Warning 或者 (category, loglevel) => category.Contains("MyController") && loglevel >= LogLevel.Trace )。事件日志記錄器提供了類似重載接受一個 EventLogSettings 實例作為參數,其 Filter 屬性可包含過濾方法。該 TraceSource 記錄器不提供任何的重載,因為它的記錄級別和其他參數基於他使用的 SourceSwitchTraceListener

一個 LoggerFactory 實例可以選擇性地使用自定義 FilterLoggerSettings 配置。下面的示例配置自定義日志級別不同的范圍,限制系統和微軟內置的日志記錄警告,同時允許應用程序在默認情況下記錄調試級別的。 WithFilter 方法返回一個新的 ILoggerFactory ,將過濾傳遞的所有注冊的記錄器日志信息。它不會影響其它的任何 ILoggerFactory 實例,包括原始的 ILoggerFactory 實例。

loggerFactory
    .WithFilter(new FilterLoggerSettings
    {
        { "Microsoft", LogLevel.Warning },
        { "System", LogLevel.Warning },
        { "ToDoApi", LogLevel.Debug }
    })
    .AddConsole();

配置 TraceSource 日志

當運行在完整的 .NET 框架之上時,你可以使用現有的 System.Diagnostics.TraceSource 類庫和提供程序來配置日志,包括輕松訪問到 Windows 事件日志。TraceSource 允許你將消息路由到不同的監聽器上,而這已經被很多組織所使用。

首先,確保項目已添加了 Microsoft.Extensions.Logging.TraceSource 包(在 project.json 中),與將使用的任何指定的追蹤源代碼包(這個例子中: TextWriterTraceListener ):

  "Microsoft.AspNetCore.Mvc": "1.0.0",
  "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
  "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
  "Microsoft.AspNetCore.StaticFiles": "1.0.0",
  "Microsoft.Extensions.Logging": "1.0.0",
  "Microsoft.Extensions.Logging.Console": "1.0.0",
  "Microsoft.Extensions.Logging.Filter": "1.0.0",
  "Microsoft.Extensions.Logging.TraceSource": "1.0.0"
},
"tools": {
  "Microsoft.AspNetCore.Server.IISIntegration.Tools": {

在下例中演示了如何在一個應用程序中配置一個的 TraceSourceLogger 實例,日志都只記錄 Warning 或者是更高級別的消息。每次調用 AddTraceSource 都需要一個 TraceListener 。調用配置了一個 TextWriterTraceListener,用於將日志寫到控制台窗體。這一日志輸出將附加於已在本例中添加了的控制台日志,但它們的行為略有不同。

// add Trace Source logging
var testSwitch = new SourceSwitch("sourceSwitch", "Logging Sample");
testSwitch.Level = SourceLevels.Warning;
loggerFactory.AddTraceSource(testSwitch,
    new TextWriterTraceListener(writer: Console.Out));

sourceSwitch 是使用 SourceLevels.Warning 配置的,因此,僅 Warning (或更高) 日志信息被 TraceListener 實例提取。
當指定的 id 沒有找到時,下面 API 行為會記錄一個警告信息:

using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using TodoApi.Core;
using TodoApi.Core.Interfaces;
using TodoApi.Core.Model;

namespace TodoApi.Controllers
{
    [Route("api/[controller]")]
    public class TodoController : Controller
    {
        private readonly ITodoRepository _todoRepository;
        private readonly ILogger<TodoController> _logger;

        public TodoController(ITodoRepository todoRepository, 
            ILogger<TodoController> logger)
        {
            _todoRepository = todoRepository;
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<TodoItem> GetAll()
        {
            _logger.LogInformation(LoggingEvents.LIST_ITEMS, "Listing all items");
            EnsureItems();
            return _todoRepository.GetAll();
        }

        [HttpGet("{id}", Name = "GetTodo")]
        public IActionResult GetById(string id)
        {
            _logger.LogInformation(LoggingEvents.GET_ITEM, "Getting item {0}", id);
            var item = _todoRepository.Find(id);
            if (item == null)
            {
                _logger.LogWarning(LoggingEvents.GET_ITEM_NOTFOUND, "GetById({0}) NOT FOUND", id);
                return NotFound();
            }
            return new ObjectResult(item);
        }

        [HttpPost]
        public IActionResult Create([FromBody] TodoItem item)
        {
            if (item == null)
            {
                return BadRequest();
            }
            _todoRepository.Add(item);
            _logger.LogInformation(LoggingEvents.INSERT_ITEM, "Item {0} Created", item.Key);
            return CreatedAtRoute("GetTodo", new { controller = "Todo", id = item.Key }, item);
        }

        [HttpPut("{id}")]
        public IActionResult Update(string id, [FromBody] TodoItem item)
        {
            if (item == null || item.Key != id)
            {
                return BadRequest();
            }

            var todo = _todoRepository.Find(id);
            if (todo == null)
            {
                _logger.LogWarning(LoggingEvents.GET_ITEM_NOTFOUND, "Update({0}) NOT FOUND", id);
                return NotFound();
            }

            _todoRepository.Update(item);
            _logger.LogInformation(LoggingEvents.UPDATE_ITEM, "Item {0} Updated", item.Key);
            return new NoContentResult();
        }

        [HttpDelete("{id}")]
        public void Delete(string id)
        {
            _todoRepository.Remove(id);
            _logger.LogInformation(LoggingEvents.DELETE_ITEM, "Item {0} Deleted", id);
        }

        private void EnsureItems()
        {
            if (!_todoRepository.GetAll().Any())
            {
                _logger.LogInformation(LoggingEvents.GENERATE_ITEMS, "Generating sample items.");
                for (int i = 1; i < 11; i++)
                {
                    _todoRepository.Add(new TodoItem() { Name = "Item " + i });
                }
            }
        }
    }
}

為了測試這個代碼,你通過運行控制台應用程序並導航到 http://localhost:5000/api/Todo/0 可以觸發記錄警告。你應該看到類似以下的輸出:

以"warn: "為前綴的黃線,隨着后面的行,是由 ConsoleLogger 輸出。以 “TodoApi.Controllers.TodoController” 開始的下一行,是由 TraceSource logger 輸出。還有其他可用的 TraceSource,並且 TextWriterTraceListener 可以通過 TextWriter 實例配置,這對於記錄是一個非常靈活的選擇。

配置其它提供程序

除內置日志記錄器外,你可以配置其它提供商提供的日志。將相應的包添加到 project.json 文件中,並以上文同樣的方法配置它們。通常情況下,這些包應該會包含 ILoggerFactory 的擴展方法以便能方便地添加它們。

你也可以創建自己定制的提供程序來支持其他的日志框架或自己內部的日志需求。

日志記錄建議

當你在 ASP.NET Core 應用程序中實現日志時可以參考以下有用建議:

  1. 使用正確的 LogLevel ,這將使不同重要級別的日志消息使用和路由到相關的輸出目標。

  2. 記錄的日志信息要能立即識別問題所在,剔除不必要的冗余信息。

  3. 保證日志內容簡單明了,直指重要信息。

  4. 盡管日志記錄器被禁用后將不記錄日志,但也請在日志方法的周圍增加控制代碼,以防止多余的方法調用和日志設置的開銷,特別是在循環和對性能要求比較高的方法中。

  5. 使用獨有的前綴命名日志記錄器以確保能快速過濾或禁用。謹記 Create<T> 擴展方法將創建的日志記錄器使用該類的完全限定名作為日志記錄器的類別名。

  6. 使用作用域時保持謹慎,明晰動作的開始和結束的界限(比如框架提供的 MVC Action 的范圍),避免相互嵌套。

  7. 應用程序日志代碼應關注應用程序 的業務。提高日志的詳細程度級別來記錄框架相關的問題,而不是日志記錄器自己。

總結

ASP.NET Core 提供了內建支持的日志,能方便地通過 Startup 類來配置,並在應用程序中使用。日志記錄的詳細程序可以在全局配置,也可以為每個日志提供程序單獨配置,以確保可操作信息能恰當地被記錄下來。框架內建了控制台和跟蹤源的日志提供程序;另外其他的日志框架也可以被方便配置。

返回目錄


免責聲明!

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



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