在.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中对于结构化日志的支持是在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"}]}}