Serilog 是一種序列化器。在許多情況下,它具有良好的預設行為,能夠滿足其目的,但有時也需要指示 Serilog 如何儲存附加到日誌事件上的屬性。
Serilog 使用一些不尋常的術語來指代 .NET 物件如何對映到其內部(與接收器無關的)屬性表示。這些術語的詳細解釋如下,所以如果你打算閱讀整頁內容,可以跳過這一部分。
-
字串化(Stringification) 是指獲取一個提供.NET 屬性,並呼叫其 ToString() 方法,以便到達接收器的表示是一個簡單的字串。
-
解構(Destructuring) 是指將複雜的 .NET 物件轉換為結構的過程,這些結構可能會被表示為 JSON 物件或 XML 塊。
-
標量(Scalars) 是指可以表示為原子值的 .NET 型別;大多數值型別如 int 都符合這個描述,但一些引用型別如 Uri 和 string 也符合。
01、為什麼需要控制表示方式?
記錄物件到日誌的方式可能有很多種。大多數型別可以很好地表示為字串或簡單值,但有些則更適合記錄為集合,還有些則適合記錄為具有命名屬性的結構體。
日誌事件屬性的儲存表示方式對日誌的大小以及獲取這些資料所需的記憶體和處理開銷有很大影響。
考慮到這一點,我們來看看如何在簡單情況下配置 Serilog。
02、預設行為
當在日誌事件中指定屬性時,Serilog 會盡力確定正確的表示方式。
簡單的標量值
var count = 456;
Log.Information("Retrieved {Count} records", count);
在這種情況下,Count 屬性的儲存方式幾乎沒有歧義。作為一個簡單的整數值,Serilog 會選擇這種表示方式。
{ "Count": 456 }
這些示例使用 JSON,但相同的原則也適用於其他格式。
開箱即用,Serilog 識別以下列表作為基本標量型別,無論是否應用了其他策略:
-
布林值 - bool
-
數值 - byte, short, ushort, int, uint, long, ulong, float, double, decimal
-
字串 - string, byte[]
-
時間 - DateTime, DateTimeOffset, TimeSpan
-
其他 - Guid, Uri
-
可空型別 - 上述型別的可空版本
集合
如果作為屬性傳遞的物件是IEnumerable,Serilog會將該屬性視為集合。
var fruit = new[] { "Apple", "Pear", "Orange" };
Log.Information("In my bowl I have {Fruit}", fruit);
```對應的JSON包括一個陣列。
```csharp
{ "Fruit": ["Apple", "Pear", "Orange"] }
Serilog 之所以這樣選擇,是因為大多數可列舉型別關注的是其元素,而作為結構或字串表示不佳。
Serilog還識別Dictionary<TKey,TValue>,只要鍵型別是上面列出的標量型別之一。
var fruit = new Dictionary<string,int> {{ "Apple", 1}, { "Pear", 5 }};
Log.Information("In my bowl I have {Fruit}", fruit);
支援字典的格式器可以將屬性記錄為字典。
{ "Fruit": { "Apple": 1, "Pear": 5 }}
IDictionary<TKey,TValue> - 實現字典介面的物件不會被序列化為字典。首先,因為在.NET中檢查泛型介面相容性效率較低,其次,一個物件可能實現多個泛型字典介面,從而產生歧義。
物件
除了上述特殊處理的型別之外,Serilog 很難智慧地選擇資料的渲染和持久化方式。未明確用於序列化的物件往往序列化效果很差。
SqlConnection conn = ...;
Log.Information("Connected to {Connection}", conn);
(哎呀!如何序列化一個 SqlConnection 物件?)
當 Serilog 無法識別該型別且未指定運算子(見下文)時,物件將使用 ToString() 方法進行渲染。
03、保留物件結構
在許多情況下,如果可能的話,將日誌事件屬性序列化為結構化物件是有意義的。資料傳輸物件(DTOs)、訊息、事件和模型通常最好透過將其分解為具有值的屬性來進行日誌記錄。
為此,Serilog 提供了 @ 解構運算子。
var sensorInput = new { Latitude = 25, Longitude = 134 };
Log.Information("Processing {@SensorInput}", sensorInput);
(“解構”一詞是從各種程式語言中借用的;它是一種用於從結構化資料中提取值的模式匹配風格。目前,Serilog 中的用法僅與該術語在概念上相關,但未來對該運算子的擴充套件可能會更準確地匹配其更廣泛的定義。)
自定義儲存的資料
通常,對複雜物件的只有部分屬性是感興趣的。要自定義 Serilog 如何持久化解構的複雜型別,可以在 LoggerConfiguration 上使用 Destructure 配置物件:
Log.Logger = new LoggerConfiguration()
.Destructure.ByTransforming<HttpRequest>(
r => new { RawUrl = r.RawUrl, Method = r.Method })
.WriteTo...
這個示例將HttpRequest型別的物件轉換為僅保留RawUrl和Method屬性的新物件。可以使用多種不同的解構策略,也可以透過實現 IDestructuringPolicy 建立自定義策略。
注意:提供給 Destructure.ByTransforming() 的函式必須返回與傳入型別不同的型別,否則會遞迴呼叫。可以使用自定義的 IDestructuringPolicy 來實現條件轉換。
運算子與格式
雖然運算子和格式都影響屬性的表示方式,但它們的作用是不同的。運算子在捕獲屬性時被應用,用於以某種方式保留或結構化資料。而格式則僅在將屬性顯示為文字時使用,不會影響序列化的表示。
格式化集合和結構
當為複雜屬性指定格式字串時,它們通常會被忽略。只有可列舉型別會考慮格式字串,並在顯示時將其傳遞給元素。
04、強制字串化
有時,日誌記錄的物件型別可能不完全確定,或者可能以不希望在日誌事件中保留的方式變化。在這些情況下,$ 字串化運算子將把屬性值轉換為字串,然後再進行其他處理,無論其型別或實現的介面是什麼。
var unknown = new[] { 1, 2, 3 }
Log.Information("Received {$Data}", unknown);
儘管 unknown 是一個列舉型別,但它被捕獲並以字串形式呈現。
Received "System.Int32[]"
注:相關原始碼都已經上傳至程式碼庫,有興趣的可以看看。https://gitee.com/hugogoos/Planner