通過標準化的 Microsoft.Extensions.Logging 實現日誌紀錄

  • 1934
  • 0
  • Log
  • 2020-10-29

Log 是系統不可或缺的角色,有利於我們開發(偵錯)、維運,.NET Core 發展出了標準化的 Log 抽象 Microsoft.Extensions.Logging.Abstractions,未來可使用這個標準來實現 Log

開發環境

  • VS 2019
  • .NET Framework 4.8
  • ASP.NET 3.1
  • Microsoft.Extensions.Logging 3.1.9

三個介面

ILogger:紀錄日誌
ILoggerProvider:產生 ILogger 實例
ILoggerFactory:使用那些 ILoggerProvider

由 Microsoft.Extensions.Logging.Abstractions 和 Microsoft.Extensions.Logging 所提供
Install-Package Microsoft.Extensions.Logging
Install-Package Microsoft.Extensions.Logging.Abstractions

實例化範例

開始之前先來看個範例,以下的範例,
要先安裝Install-Package Microsoft.Extensions.Logging.Console

.NET Framework

  • 在 .NET Framework 沒有像 .NET Core 內建 DI Container,可以呼叫 LoggerFactory.Create 建立實例。
  • builder.AddConsole():使用 Console LoggerProvider。
  • factory.CreateLogger():Logger 實例。
  • logger.LogInformation:寫入 Log,等級為 Information
var factory = LoggerFactory.Create(builder =>
								   {
									   builder.AddConsole();
								   });
var logger = factory.CreateLogger<Form1>();
logger.LogInformation("Example log message");

 

ASP.NET Core 3.1

  • 可以使用 DI Container 的方式注入
  • builder.AddConsole():使用 Console LoggerProvider
public class Program
{
	public static IHostBuilder CreateHostBuilder(string[] args)
	{
		return Host.CreateDefaultBuilder(args)
				   .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
				   .ConfigureLogging((context, builder) =>
									 {
										 builder.AddConsole();
									 });
	}

	public static void Main(string[] args)
	{
		CreateHostBuilder(args).Build().Run();
	}
}
  • 在 Controller 就可以拿到 ILogger 實例
  • logger.LogInformation:寫入 Log,等級為 Information
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
	
	private readonly ILogger<WeatherForecastController> _logger;

	public WeatherForecastController(ILogger<WeatherForecastController> logger)
	{
		this._logger = logger;
	}

	[HttpGet]
	public IActionResult Get()
	{
	 	...
		this._logger.LogInformation(LogEvent.GenerateItem,"開始,訪問 WeatherForecast api");
	}
}

 

6個層級

LogLevel 定義以及建議的使用方式如下:

LogLevel 擴充方法 描述
Trace 0 LogTrace 追蹤:包含最詳細的訊息。 這些訊息可能包含敏感性應用程式資料。 這些訊息預設為停用,且不應在生產環境中啟用。
Debug 1 LogDebug 偵錯:用於偵錯工具和開發。 請在生產環境中小心使用,因為有大量的數量。
Information 2 LogInformation 資訊:追蹤應用程式的一般流程。 可能具有長期值。
Warning 3 LogWarning 警告:針對異常或非預期的事件。 通常會包含不會導致應用程式失敗的錯誤或狀況。
Error 4 LogError 錯誤:發生無法處理的錯誤和例外狀況。 這些訊息表示目前的作業或要求失敗,而不是整個應用程式的失敗。
Critical 5 LogCritical 嚴重:發生需要立即注意的失敗。 範例:資料遺失情況、磁碟空間不足。
None 6   指定記錄類別不應寫入任何訊息。
  • 如果未設定預設記錄層級,則預設的記錄層級值為 Information 。

ASP.NET Core 範例如下:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

 

參考
https://docs.microsoft.com/zh-tw/aspnet/core/fundamentals/logging/?view=aspnetcore-3.1#log-level

ILoggerFactory

以下的範例,要先安裝Install-Package Microsoft.Extensions.Logging.Console

  • 在 .NET Framework 沒有像 ASP .NET Core 內建 DI Container,可以呼叫 LoggerFactory.Create 建立實例
var factory = LoggerFactory.Create(builder =>
								   {
									   builder.AddConsole()
										   ;
								   });
var logger = factory.CreateLogger<Form1>();
logger.LogInformation("Example log message");

 

  • ASP.NET Core 3.1 可以使用 DI Container 的方式注入

Host.CreateDefaultBuilder 會注入下列 LoggerProvider
主控台
偵錯
EventSource
EventLog:僅限 Windows

為了演示,我仍然注入了 Console LoggerProvider

public class Program
{
	public static IHostBuilder CreateHostBuilder(string[] args)
	{
		return Host.CreateDefaultBuilder(args)
				   .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
				   .ConfigureLogging((context, builder) =>
									 {
										 builder.AddConsole()
											 ;
									 });
	}

	public static void Main(string[] args)
	{
		CreateHostBuilder(args).Build().Run();
	}
}

 

在 Controller 即可取出 ILogger 實例

private readonly ILogger<WeatherForecastController> _logger;

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

[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
    this._logger.LogInformation("訪問 /WeatherForecast");
    ///....
}

 

除了,預設 Controller 注入,我們也可以針對其他物件注入 ILogger

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddRazorPages();

    services.AddSingleton<IMyService>((container) =>
    {
        var logger = container.GetRequiredService<ILogger<MyService>>();
        return new MyService() { Logger = logger };
    });
}

 

參考
https://docs.microsoft.com/zh-tw/aspnet/core/fundamentals/logging/?view=aspnetcore-3.1#logging-providers

ILoggerProvider

 

參考
https://www.tutorialsteacher.com/core/aspnet-core-logging

ILogger

只需要一行程式,即可依照 LoggerProvider,寫入不同的目標

如下範例,注入了多個 LoggerProvider

public static IHostBuilder CreateHostBuilder(
   string[] args) => Host.CreateDefaultBuilder(args)  
      .ConfigureWebHostDefaults(webBuilder =>
      {
         webBuilder.UseStartup<Startup>();
      })
      .ConfigureLogging(logging =>  
      {  
         // clear default logging providers
         logging.ClearProviders();  

         // add built-in providers manually, as needed 
         logging.AddConsole();   
         logging.AddDebug();  
         logging.AddEventLog();
         logging.AddEventSourceLogger();
         logging.AddTraceSource(sourceSwitchName); 
      });

 

Log Message

  1. Log() 方法要傳入 Level 參數
  2. 擴充方法,方法名稱已經帶有 Level
// Log() 方法要帶有 Level
ILogger.Log(LogLevel.Information, "some text");

// 擴充方法,方法名稱已經帶有
ILogger.LogInformation("some text");

 

Log 分類

ILogger 建立物件時,需要指定分類,分類的字串是任意的,慣例是使用類別名稱。

LoggerFactory.Create 使用範例如下:

var factory = LoggerFactory.Create(builder =>
								   {
									   builder.AddFilter("Microsoft", LogLevel.Warning)
											  .AddFilter("System",           LogLevel.Warning)
											  .AddFilter("WindowsFormsApp1", LogLevel.Debug)
											  .AddConsole()
										   ;
								   });
var logger = factory.CreateLogger<Form1>();
logger.LogInformation("Example log message");

 

例如:ASP.NET Core 可以使用注入,最後,在建構函數改變自訂的分類名稱

public class ContactModel : PageModel
{
    private readonly ILogger _logger;

    public ContactModel(ILoggerFactory logger)
    {
        _logger = logger.CreateLogger("MyCategory");
    }

    public void OnGet()
    {
        _logger.LogInformation("GET Pages.ContactModel called.");
    }
}

 

參考
https://docs.microsoft.com/zh-tw/aspnet/core/fundamentals/logging/?view=aspnetcore-3.1#log-category

Log 範本

  • 訊息範本可以包含預留變數,變數使用名稱而非數字
  • 傳遞參數,可讓 LoggerProvider 執行語意結構化紀錄
_logger.LogInformation("Getting item {Id} at {RequestTime}", id, DateTime.Now);

 

參考
https://docs.microsoft.com/zh-tw/aspnet/core/fundamentals/logging/?view=aspnetcore-3.1#log-message-template

Scope

在非同的框架下,同一個流程,日誌內容會被散落在四處,這時後利用 BeginScope 可以讓我們的日誌比較好

[HttpGet]
public IActionResult Get()
{
	using (this._logger.BeginScope($"Scope Id:{Guid.NewGuid()}"))
	//using (this._logger.BeginScope("Scope Id:{id}", Guid.NewGuid().ToString())
	{
		this._logger.LogInformation("開始,訪問 WeatherForecast api");
		this._logger.LogInformation("執行流程");

		var random = new Random();
		var result = Enumerable.Range(1, 5).Select(index => new WeatherForecast
							   {
								   Date         = DateTime.Now.AddDays(index),
								   TemperatureC = random.Next(-20, 55),
								   Summary      = Summaries[random.Next(Summaries.Length)]
							   })
							   .ToArray();

		this._logger.LogInformation("結束流程");

		return this.Ok(result);
	}
}

 

最後再設定檔的 Console LoggerProvider 加入 “IncludeScopes”: true

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    },
    "Console": {
      "IncludeScopes": true,// <= 這裡
      "LogLevel": {
        "Default": "Trace",
        "Microsoft": "Warning"
      }
    }
  },
  "AllowedHosts": "*"
}

 

執行結果:

參考
https://docs.microsoft.com/zh-tw/aspnet/core/fundamentals/logging/?view=aspnetcore-3.1#log-scopes

事件識別碼

識別碼用來建立日誌的關聯性,有些 LoggerProvider 有提供識別碼篩選

先建立識別碼。如下:

public class LogEvent
{
	public const int GenerateItem = 1000;
	public const int ListItem     = 1001;
	public const int GetItem      = 1002;
	public const int InsertItem   = 1003;
	public const int UpdateItem   = 1004;
	public const int DeleteItem   = 1005;
	public const int TestItem           = 3000;
	public const int GetItemNotFound    = 4000;
	public const int UpdateItemNotFound = 4001;
}

 

[HttpGet]
public IActionResult Get()
{
	//using (this._logger.BeginScope($"Scope Id:{Guid.NewGuid()}"))
	using (this._logger.BeginScope("Scope Id:{id}", Guid.NewGuid()))
	{
		this._logger.LogInformation(LogEvent.GenerateItem,"開始,訪問 WeatherForecast api");
		this._logger.LogInformation(LogEvent.TestItem,"執行流程");

		var random = new Random();
		var result = Enumerable.Range(1, 5).Select(index => new WeatherForecast
							   {
								   Date         = DateTime.Now.AddDays(index),
								   TemperatureC = random.Next(-20, 55),
								   Summary      = Summaries[random.Next(Summaries.Length)]
							   })
							   .ToArray();

		this._logger.LogInformation(LogEvent.GenerateItem,"結束流程");

		return this.Ok(result);
	}
}

 

執行結果:
Console LoggerProvider 就會在類別後面加上 [識別碼] WebApiNetCore31.Controllers.WeatherForecastController[1000]

參考
https://docs.microsoft.com/zh-tw/aspnet/core/fundamentals/logging/?view=aspnetcore-3.1#log-event-id

非同步

官方 文件寫著沒有 Logger.Log 提供非同步的方法,原因是紀錄日誌應該是很快,如果很慢的話應該先將記錄檔寫在快速儲存區(記憶體),再把它們移到慢速儲存區(I/O),比如 SQL Server,幸好 LoggerProvider 有實作非同步寫入日誌,比如:Consloe、NLog

 

Config

ASP.NET Core 的預設專案範本一建立就會產生 appsettings.json、appsettings.Development.json 內容如下:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

 

上述設定,LoggerProvider 會依照 Namsespace 和 LogLevel 過濾出需要記錄的日誌,當指定某一 Level 時,代表該等級以上的 Log 都會被記錄下來

  • Logging:所有的 LoggerProvider,套用此設定
  • Default:預設,套用 Information Level
  • Microsoft:“Microsoft” 開頭的命名空間,套用 Warning Level
  • Microsoft.Hosting.Lifetime:比類別更明確 “Microsoft”,所以 “Microsoft.Hosting.Lifetime” 的等級為 Information
  • 特定分類覆蓋 Default 分類
  • Level 高的覆蓋低的
  • 若要隱藏所有的日誌則指定 None

ASP.NET Core 範例如下:

public class Program
{
	public static IHostBuilder CreateHostBuilder(string[] args)
	{
		return Host.CreateDefaultBuilder(args)
				   .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
				   .ConfigureLogging((context, builder) =>
									 {
										 builder.AddFilter("Microsoft",        LogLevel.Warning)
												.AddFilter("System",           LogLevel.Warning)
												.AddFilter("WindowsFormsApp1", LogLevel.Debug)
												.AddConfiguration(context.Configuration.GetSection("Logging"))
												.AddConsole()
											 ;
									 });
	}

	public static void Main(string[] args)
	{
		CreateHostBuilder(args).Build().Run();
	}
}

 

Filer 函數

當 Config 之外,透過 Filter 做更多的設定

ASP.NET Core 範例如下:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureLogging(logging =>
        {
            logging.AddFilter((provider, category, logLevel) =>
            {
                if (provider.Contains("ConsoleLoggerProvider")
                    && category.Contains("Controller")
                    && logLevel >= LogLevel.Information)
                {
                    return true;
                }
                else if (provider.Contains("ConsoleLoggerProvider")
                    && category.Contains("Microsoft")
                    && logLevel >= LogLevel.Information)
                {
                    return true;
                }
                else
                {
                    return false;
                }
            });
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

 

一般來說,記錄層級應指定于設定中,而不是程式碼。

 

專案位置

https://github.com/yaochangyu/sample.dotblog/tree/master/Log/Lab.MsLogging

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2019 .NET

Image result for microsoft+mvp+logo