用到的單詞
Sink 接收器模塊、輸出方式、接收模塊庫、輸出模塊庫
Diagnostic 診斷
Enricher 擴展器
embedded 嵌入式的
compact 緊湊的、簡潔的
concept 概念
usage 用法
restrict 限制、約束
raise 提升
necessary 必要的
digging 挖掘
一、基礎用法
1.1 添加 Nuget 引用
Serilog.AspNetCore
日志包主體
Serilog.AspNetCore.RollingFile
將日志寫入文件
1.2 注冊服務
1.2.1 在 appsettings.json
中添加 Serilog
節點。
簡單說明下配置文件的意思:
- 將日志寫入RollingFile(文件)和Console(控制台)。
RollingFile
的具體配置:記錄文件到根目錄/logs/{日期}.txt
文件內,每天記錄一個文件,並且只記錄Warning
及其以上的日志;- 默認日志級別記錄
Debug
及其以上的日志。 - 如果日志包含
Microsoft
System
,只記錄級別為Information
及以上的日志。
{
"Serilog": {
"WriteTo": [
{
"Name": "RollingFile",
"Args": {
"pathFormat": "logs\\{Date}.txt",
"RestrictedToMinimumLevel": "Warning"
}
},
{
"Name": "Console"
}
],
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Information",
"System": "Information"
}
}
},
}
1.2.2 修改 program.cs
,注冊 Serilog
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
}).UseSerilog((context, configure) =>
{
configure.ReadFrom.Configuration(context.Configuration);
});
1.2.3 簡單配置完成,現在可以在項目中方便的使用 Serilog
了。
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
public IActionResult Index()
{
_logger.LogError("Error 測試");
return View();
}
二、Serilog 的重要組成部分
2.1 日志接收器 (Sink)
接收器用來配置日志記錄到哪種介質。比如說 Console(輸出到控制台),File(日志文件)等。就是我們上面配置的 WriteTo
節點。
接收器一般以擴展庫的方式提供的,為了將日志寫入文件,我們在上面引入了 Serilog.AspNetCore.Sinks.RollingFile
包。
除了在配置文件中使用 WriteTo
指定,也可以通過代碼配置,比如在 Console
控制台程序中使用:
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateLogger();
Log.Information("Ah, there you are!");
可以同時使用多個接收器,用法很簡單,支持鏈式調用:
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("log-.txt", rollingInterval: RollingInterval.Day)
.CreateLogger();
基於文本的接收器使用 output templates
來控制格式。
Log.Logger = new LoggerConfiguration()
.WriteTo.File("log.txt",
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
格式這部分涉及到擴展器,會在后面具體說明,這里只是簡單提一下如何使用。
可以使用每個接收器指定不同的日志級別
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.File("log.txt")
.WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information)
.CreateLogger();
上面代碼中指定了默認日志級別為 Debug
,但為 Console Sink
的日志重寫級別為 Information
==========
在聲明Logger時可以通過 MinimumLevel.Debug()
指定最小級別,而在 WriteTo
中指定接收器Sink時,也可以通過 restrictetToMinimumLevel:LogEventLevel.Information
指定最小級別,那兩者之間的關系是怎么樣的呢?
注意:接收器Sink的級別必須高於Logger的級別。 假設Logger的默認級別為 Information
,即便 Sink
重寫日志級別為 LogEventLevel.Debug
,也只能看到 Information
及以上級別的日志。這是因為默認級別的配置控制哪些事件級別的日志記錄語句可以被創建,而 Sink
級別僅僅是對這些事件進行過濾。
2.2 擴展器 (Enricher)
顧名思義,擴展器可以添加,刪除或修改附加到日志事件的屬性。 例如,下面的代碼可以達到將線程ID附加到每個事件的目的。
class ThreadIdEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
"ThreadId", Thread.CurrentThread.ManagedThreadId));
}
}
使用 Enricher
將擴展添加到配置對象
Log.Logger = new LoggerConfiguration()
.Enrich.With(new ThreadIdEnricher())
.WriteTo.Console(
outputTemplate: "{Timestamp:HH:mm} [{Level}] ({ThreadId}) {Message}{NewLine}{Exception}")
.CreateLogger();
注意模板中的 {ThreadId}
,在日志中打印的便是當前線程的ID了
如果擴展的屬性值在整個應用程序運行期間都是恆定的,則可以使用快捷方式WithProperty方法簡化配置。
Log.Logger = new LoggerConfiguration()
.Enrich.WithProperty("Version", "1.0.0")
.WriteTo.Console()
.CreateLogger();
可用的擴展器包
- Serilog.Enrichers.Environment - WithMachineName() and WithEnvironmentUserName()
- Serilog.Enrichers.Process - WithProcessId()
- Serilog.Enrichers.Thread - WithThreadId()
其他有意思的擴展器包
- Serilog.Web.Classic - WithHttpRequestId() and many other enrichers useful in classic ASP.NET applications
- Serilog.Exceptions - WithExceptionDetails() adds additional structured properties from + exceptions
- Serilog.Enrichers.Demystify - WithDemystifiedStackTraces()
2.3 過濾器 (Filter)
可以通過過濾器有選擇的記錄事件。過濾器只是 LogEvent
的謂詞,其中一些常見的情況由 Matching
類處理。
只要在調試過程中打個斷點,就可以看到 LogEvent
的詳細屬性,這里就不贅述,懶~~~
var log = new LoggerConfiguration()
.WriteTo.Console(
outputTemplate:
"{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {Count} {Message:j} {NewLine}")
.Filter.ByExcluding(Matching.WithProperty<int>("Count", p => p < 10))
.CreateLogger();
log.Information("測試 {Count}", 20);
log.Information("測試 {Count}", 3);
log.Information("測試 {Count}", 11);
Console.Read();
// 輸出:
// 2020-05-05 00:04:46 INF 20 測試 20
// 2020-05-05 00:04:47 INF 11 測試 11
代碼說明:忽略參數 Count
小於 10 的日志,所以最終只輸出了 2 條。
三、說說輸出格式吧
一個基本的OutTemplate模板大概長這樣:
{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} {NewLine} {Exception}
從頭到尾分別是:日期、日志級別、日志內容、換行、異常信息
模板可用參數
Serilog的日志是由LogEvent構成的,其可用的參數對應了LogEvent的public屬性,分別是下面幾個:
- 時間戳【Timestamp】- 日志產生時的時間戳,可定義格式
- 日志級別【Level】- 日志的級別,可選定義格式
- 日志內容【Message】- 日志的內容,一般由字符串模板和參數構成,類似 string.Format(temp, param1, param2, ...),但比Format強大的多
- 命名屬性【Properties】- 包涵 擴展器定義的屬性 以及 日志內容中提供的參數,后面細說
- 異常【Exception】- 如果是輸出的異常,會完整異常信息和棧跟蹤信息。如果沒有異常則為空
- 換行【NewLine】- 就是調用 System.Environment.NewLine
個人理解的模板可以分兩種
- 【日志】的輸出模板,由 OutTemplate 定義
- 【日志內容】的輸出模板,就是上面整體的模板中【Message】的部分,在寫日志時由調用者提供,如:
log.Error("測試 {Count}", 10)
格式化輸出
serilog 的輸出很有意思,並不僅僅能輸出字符串,還可以輸出對象或枚舉。
這里主要以【Message】為說明對象,事實上也有【Properties】用法也差不多,但輸出樣式稍微有些區別
請看下面的代碼:
var user = new User { Id = 1, Name = "張三" };
log.Debug("當前用戶: {User},當前時間:{CurrentTime}", user, DateTime.Now)
- 在模板字符串內,包含在 {} 中的內容為“標量”名稱,這里輸出的內容由后面實參決定
- 如果要原樣輸出大括號,只要 {{內容}} 就可以了
- 標量和參數的對應關系是從左到右一一對應
- 標准 string.Format 也能用,比如 log.Debug("{0} {1}!", "Hello", "world"),但建議不要混用
默認的輸出行為
1. 簡單標量類型:
- 布爾類型 - bool
- 數值類型 - byte,short,ushort,int,unit,long,ulong,float,double,decimal
- 字符串 - string,byte[]
- 日期 - DateTime,DateTimeOffset,TimeSpan
- 其他 - Guid,Uri
- 可空類型 - 上面所有類型的可空版本
上面這些類型,表達的內容基本上不會有歧義,所以一般處理方式就是直接調用 .ToString()
方法
var count = 100;
log.Debug("檢索到{Count}條記錄", count); // 檢索到100條記錄
其他類型都差不多,大家可以自己測試。
2. 集合 Collection
如果屬性值傳遞的IEnumerable
,Serilog會將其視為一個集合
var fruits = new[] {"apple", "pear", "orange"};
log.Information("我有這些水果:{fruit}", fruits); // 我有這些水果:["apple", "pear", "orange"]
var fruits2 = new Dictionary<string,int> {{ "Apple", 1}, { "Pear", 5 }};
log.Information("In my bowl I have {Fruit}", fruits2); // In my bowl I have {"Apple": 1, "Pear": 5}
可以看出,Serilog很智能,根據不同的集合類型輸出了不同格式,而該格式正好可以表達數據的內容
3. 對象 object
var user = new User() {Id = 1, Name = "張三"};
log.Information("{User}", user); // "SerilogSample.User"
log.Information("{@User}", user); // {"Id": 1, "Name": "張三", "$type": "User"}
log.Information("{$User}", user); // "SerilogSample.User"
- 默認情況下,如果Serilog沒能識別數據類型,直接調用
ToString()
,對應實例 1 - 上面那種情況,我們一般希望保留對象的結構,對此,Serilog提供了
@
符號,叫做 析構運算符。添加該符號后,默認輸出對象的JSON格式,對應實例 2 - 強制字符串化,通過
$
符號,可以強制調用對應參數的ToString()
方法,對應實例3 - 補充下上面一條,如果
log.Information("{$User}", "abc")
輸出啥呢?很簡單,"abc".ToString()
所以輸出內容就是 abc 嘛 - 自定義數據,注意代碼的第二行,Serilog提供了很多不同的析構策略,大家自己試吧。注意:ByTransforming 必須返回與輸入參數不同的類型,不然會被遞歸調用
var log = new LoggerConfiguration()
.Destructure.ByTransforming<User>(u => new { UserName = u.Name })
.WriteTo.Console(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {Message:j} {NewLine}")
.CreateLogger();
var user = new User() {Id = 1, Name = "張三"};
log.Information("{@User:l}", user); // {"UserName": "張三"}
格式化輸出
var log = new LoggerConfiguration()
.WriteTo.Console(
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} {User} {EventType} {Level:u3} {Message} {Exception} {NewLine}")
.Enrich.WithProperty("EventType", "事件類型")
.Enrich.WithProperty("User", new User { Id = 1, Name = "測試" })
.CreateLogger();
var exception = new Exception("人工異常");
log.Error(exception, "啥也不是");
格式說明:
- Timestamp:時間戳,指定格式:
{Timestamp: yyyy-MM-dd HH:mm:ss.fff zzz}
- Level:日志級別,默認為完整的級別名稱,如:
Information
。可選格式 :u3 (日志級別縮寫,三個字母,大寫)和 :w3 (日志級別縮寫,三個字母,小寫),效果:{Level:u3}=INF;{Level:w3}=inf - Properties:命名屬性,除了上面提到過的5個參數(Timestamp,Exception,Message,NewLine,Level)外的所有其他參數。這些參數來源於:
- 上面提到過的擴展器(Enricher)
- Message 中提供的參數,如果兩者同時指定了同名參數,Message的參數會覆蓋擴展器的配置
var log = new LoggerConfiguration()
.WriteTo.Console(
outputTemplate:
"{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {User:lj} {Message:j} {NewLine}")
.Filter.ByExcluding(Matching.WithProperty<int>("Count", p => p < 10))
.Enrich.WithProperty("User", "Enricher")
.CreateLogger();
log.Information("{User}", "Message");
// 輸出:2020-05-05 00:19:39 INF Message "Message"
雖然在Enricher中指定了屬性User,但最終輸出的是Message模板指定的實參。修改一下代碼:
var log = new LoggerConfiguration()
.WriteTo.Console(
outputTemplate:
"{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {User:lj} {Message:j} {NewLine}")
.Filter.ByExcluding(Matching.WithProperty<int>("Count", p => p < 10))
.Enrich.WithProperty("User", "Enricher")
.CreateLogger();
log.Information("{User1}", "Message");
// 輸出:2020-05-05 00:19:39 INF Enricher "Message"
- Message: 消息內容;如果要輸出對象的JSON,一般會指定格式參數
{Message:lj}
;
參數l
的作用:
var log = new LoggerConfiguration()
.WriteTo.Console(
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {Message:l}")
.CreateLogger();
log.Information("{a} {b}", 23, "abc");
// 輸出為:2020-05-04 23:41:12 INF 23 abc
var log = new LoggerConfiguration()
.WriteTo.Console(
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {Message}")
.CreateLogger();
log.Information("{a} {b}", 23, "abc");
// 輸出為:2020-05-04 23:44:21 INF 23 "abc"
注意輸出的 abc
字符串,添加了參數 :l
后,若格式的實參是字符串類型,會自動刪除雙引號。
如果要輸出一個對象,應該怎么做呢?看下面的三種做法:
var log = new LoggerConfiguration()
.WriteTo.Console(
outputTemplate:
"{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {Message}")
.CreateLogger();
var user = new User {Id = 1, Name = "張三"};
log.Information("{u}", user);
// 輸出為:2020-05-04 23:47:08 INF "SerilogSample.User"
可以看到,輸出的是對象類型的名稱,修改代碼如下:
var log = new LoggerConfiguration()
.WriteTo.Console(
outputTemplate:
"{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {Message}")
.CreateLogger();
var user = new User {Id = 1, Name = "張三"};
log.Information("{@u}", user);
// 輸出為:2020-05-04 23:49:59 INF User {Id=1, Name="張三"}
添加參數 :j
,修改代碼如下:
var log = new LoggerConfiguration()
.WriteTo.Console(
outputTemplate:
"{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {Message:j}")
.CreateLogger();
var user = new User {Id = 1, Name = "張三"};
log.Information("{@u}", user);
// 輸出為:2020-05-04 23:51:42 INF {"Id": 1, "Name": "張三", "$type": "User"}
由上面的測試可以看出,
- 如果要輸出一個對象,輸出模板形參前要添加@符號
- 如果要將對象輸出成標准json格式,需要在日志模板中添加格式符號
{Message:j}
- 一般情況下,只要固定寫法
{Message:lj}
就夠了
四、最后嘗試使用代碼的方式實現一個跟簡單示例差不多功能的配置練練手吧
修改下 Program.cs
文件
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureAppConfiguration(configure => configure.AddJsonFile("Serilog.json"));
webBuilder.UseStartup<Startup>();
})
.UseSerilog((context, configure) =>
{
configure.MinimumLevel.Information()
.WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Debug)
.Filter.With<MicrosoftFilter>()
.WriteTo.RollingFile("logs\\{Date}.txt", LogEventLevel.Warning);
});
自定義一個 Filter
public class MicrosoftFilter : ILogEventFilter
{
public bool IsEnabled(LogEvent logEvent)
{
if (!logEvent.Properties.TryGetValue("SourceContext", out var source))
{
return logEvent.Level >= LogEventLevel.Debug;
}
if (source.ToString().StartsWith("\"Microsoft"))
{
return logEvent.Level >= LogEventLevel.Warning;
}
return logEvent.Level >= LogEventLevel.Debug;
}
}
差不多就這樣吧~~告辭!!!