在.NET生態圈中,最早被廣泛使用的日志庫可能是派生自Java世界里的Apache log4net。而其后來者,莫過於NLog。Nlog與log4net相比,有一項較顯著的優勢,它支持結構化日志。
結構化日志,也被稱為語義化日志。其作用有二,利於查詢與方便分析。
當系統上線被廣泛使用或者時間久遠之后,日志的大量出現不可避免。且日志本身作為一種數據,也有其重要的價值。因此,如何有效地對其進行查詢以及最大價值化地分析處理便成了一個重要的問題。
非結構化日志
對於日志的處理,需要權衡對開發者的友好性與對程序解析的方便性。傳統的非結構化日志更傾向於前者。當開發者想要記錄一段日志時,他可以很簡單地加上一行代碼,即可達成其目的。
logger.Info("Logon by user:{0} from ip_address:{1}", "Kenny", "127.0.0.1");
然后這行代碼的執行結果可能被存於文本文件或者數據庫中。
2018-12-22 16:29:29.2793|Info|Logon by user:Kenny from ip_address:127.0.0.1
這樣的日志以開發者的角度來看,清晰易懂,十分友好。但如果要使用程序去查找海量的上述例子里的某段時間內的特定登陸用戶,則很難高效地完成這一要求,因為需要對每個日志進行字符串解析。
消息模板
消息模板規范是結構化日志的通用語法,其是一個與開發語言無關的規范,能以特定格式捕獲及呈現結構化日志,同時提供對開發者與程序解析的友好支持。
以上圖片中的日志記錄方式乍看起來與非結構化日志差不多,但它們之間具有本質的區別,在結構化日志里,是以對象而非字符串處理日志內容的。
如果將非結構化日志例子里的代碼改成結構化日志的寫法。
logger.Info("Logon by {user} from {ip_address}", "Kenny", "127.0.0.1");
執行后兩者的結果是這樣的:
2018-12-22 16:29:29.2793|Info|Logon by user:Kenny from ip_address:127.0.0.1
2018-12-22 16:29:29.2976|Info|Logon by "Kenny" from "127.0.0.1"
似乎差別並不大,再將輸出類型改成JSON風格看看:
{ "time": "2018-12-22 16:30:15.1314", "level": "INFO", "message": "Logon by user:Kenny from ip_address:127.0.0.1" }
{ "time": "2018-12-22 16:30:15.1569", "level": "INFO", "message": "Logon by \"Kenny\" from \"127.0.0.1\"", "user": "Kenny", "ip_address": "127.0.0.1" }
顯而易見,對於后者,因為user
被作為對象的屬性獨立分離出來,在做程序處理時,可以很方便地以其為條件進行篩選。這對於查詢或者分析日志是極為重要的。
NLog
NLog中對於結構化日志的支持是在4.5版本才開始的。這一改動並不會破壞原有的代碼,而如果想要使用新的特性,則只要用符合消息模板的語法編寫所需的代碼即可。
Object o = null;
logger.Info("Test {value1}", o); // null case. Result: Test NULL
logger.Info("Test {value1}", new DateTime(2018,03, 25)); // datetime case. Result: Test 25-3-2018 00:00:00 (locale TString)
logger.Info("Test {value1}", new List<string> { "a", "b" }); // list of strings. Result: Test "a", "b"
logger.Info("Test {value1}", new[] { "a", "b" }); // array. Result: Test "a", "b"
logger.Info("Test {value1}", new Dictionary<string, int> { { "key1", 1 }, { "key2", 2 } }); // dict. Result: Test "key1"=1, "key2"=2
var order = new Order
{
OrderId = 2,
Status = OrderStatus.Processing
};
logger.Info("Test {value1}", order); // object Result: Test MyProgram.Program+Order
logger.Info("Test {@value1}", order); // object Result: Test {"OrderId":2, "Status":"Processing"}
logger.Info("Test {value1}", new { OrderId = 2, Status = "Processing"}); // anomynous object. Result: Test { OrderId = 2, Status = Processing }
logger.Info("Test {@value1}", new { OrderId = 2, Status = "Processing"}); // anomynous object. Result:Test {"OrderId":2, "Status":"Processing"}
代碼的格式化結果依據數據的類型而定。
- 字符串類型將被雙引號包圍
- 數值類型沒有引號
- null顯示為NULL
- 列表或數組類型,以逗號分隔,例如:"item1", "item2"
- 字典類型,健與值之間用等號相聯,例如:"key1"="value1", "key2"="value2"
- 對象類型,調用ToString方法顯示結果
此外,還可以有@與$符號:
- @ 以JSON格式格式化數據
- $ 強制調用ToString方法
而將日志輸出格式改成JSON的方法,是在NLog.config配置文件里將布局切換成JsonLayout
類型,同時設置includeAllProperties
為true
,以顯示所有對象屬性。
<target name="console" xsi:type="Console">
<layout xsi:type="JsonLayout" includeAllProperties="true">
<attribute name="time" layout="${longdate}" />
<attribute name="level" layout="${level:upperCase=true}"/>
<attribute name="message" layout="${message}" />
</layout>
</target>
Serilog
能夠實現結構化日志的類庫除了NLog之外,其它較常用的當屬Serilog。
與NLog相比,Serilog省去了配置文件,直接使用代碼,實現方式更加簡潔。
var position = new { Latitude = 25, Longitude = 134 };
var elapsedMs = 34;
var log = new LoggerConfiguration()
.WriteTo.Console(new JsonFormatter())
.CreateLogger();
log.Information("Processed {@Position} in {Elapsed:000} ms.", position, elapsedMs);
執行結果:
{"Timestamp":"2018-12-22T17:15:23.6389082+08:00","Level":"Information","MessageTemplate":"Processed {@Position} in {Elapsed:000} ms.","Properties":{"Position":{"Latitude":25,"Longitude":134},"Elapsed":34},"Renderings":{"Elapsed":[{"Format":"000","Rendering":"034"}]}}