在.NET生態圈中,最先被普遍使用的日誌庫多是派生自Java世界裏的Apache log4net。而其後來者,莫過於NLog。Nlog與log4net相比,有一項較顯著的優點,它支持結構化日誌。html
結構化日誌,也被稱爲語義化日誌。其做用有二,利於查詢與方便分析。git
當系統上線被普遍使用或者時間久遠以後,日誌的大量出現不可避免。且日誌自己做爲一種數據,也有其重要的價值。所以,如何有效地對其進行查詢以及最大價值化地分析處理便成了一個重要的問題。數據庫
對於日誌的處理,須要權衡對開發者的友好性與對程序解析的方便性。傳統的非結構化日誌更傾向於前者。當開發者想要記錄一段日誌時,他能夠很簡單地加上一行代碼,便可達成其目的。數組
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
這樣的日誌以開發者的角度來看,清晰易懂,十分友好。但若是要使用程序去查找海量的上述例子裏的某段時間內的特定登錄用戶,則很難高效地完成這一要求,由於須要對每一個日誌進行字符串解析。spa
消息模板規範是結構化日誌的通用語法,其是一個與開發語言無關的規範,能以特定格式捕獲及呈現結構化日誌,同時提供對開發者與程序解析的友好支持。日誌
以上圖片中的日誌記錄方式乍看起來與非結構化日誌差很少,但它們之間具備本質的區別,在結構化日誌裏,是以對象而非字符串處理日誌內容的。code
若是將非結構化日誌例子裏的代碼改爲結構化日誌的寫法。orm
logger.Info("Logon by {user} from {ip_address}", "Kenny", "127.0.0.1");
執行後二者的結果是這樣的:xml
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中對於結構化日誌的支持是在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"}
代碼的格式化結果依據數據的類型而定。
此外,還能夠有@與$符號:
而將日誌輸出格式改爲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>
可以實現結構化日誌的類庫除了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"}]}}