在过去的几年中,结构化日志已经大受欢迎。而Serilog是 .NET 中最著名的结构化日志类库 ,咱们提供了这份的精简指南来帮助你快速了解并运用它。git
你可能以前已经在项目中使用了Serilog,或者你有一个新的项目但愿使用Serilog,又或者你只是对结构化日志记录感兴趣: 那就很是好! 你来对地方了。github
然而,更进一步来讲,你的目标多是:web
Serilog使用json格式来记录应用程序中的事件,方便咱们能够快速的查询日志,关键是能够方便地过滤和查询日志,而不用编写正则表达式。正则表达式
在本教程中,咱们将介绍最关键的几个部分,帮助咱们能够在生成环境中提供使人惊叹的诊断能力。[注:原文这句挺拗口]shell
那就让咱们开始吧!为了更好的理解,你能够先建立一个新的.Net console 项目,能够是netcore或者传统的NETFramework版本。数据库
Serilog 经过NuGet分发,项目包括一个Seirlog核心项目Seirlog和不少接收器sinks(超过100个),这些接收是经过插件的方式来实现将日志写入到各类终端,文件,邮件,数据库或日志服务器express
咱们将经过使用Serilog和Serilog.Sinks.Console这两个组件开始,在稍后讨论其余选项:json
dotnet add package Serilog dotnet add package Serilog.Sinks.Console
这是世界上最简单的Serilog配置:windows
using Serilog; class Program { public static void Main(string[] args) { using (var log = new LoggerConfiguration() .WriteTo.Console() .CreateLogger()) { log.Information("Hello, Serilog!"); log.Warning("Goodbye, Serilog."); } } }
让咱们稍微分解一下:服务器
LoggerConfiguration
类提供一个流式接口用于构建一个日志记录管道WriteTo.Console()
将控制台接收器添加到上述管道中CreateLogger()
组装并返回一个实现ILogger
接口的Logger
对象using
中调用它log.Information()
和log.Warning()
触发记录日志这个日志记录管道是一个可释放的[disposable],这可能会让你有点意外,可是请记住,记录器一般是要写入文件,数据库等等: 不少sinks 必须被彻底地关闭掉。尽管这样,也仅仅在应用程序退出前,根logger才须要被释放。而在应用程序中使用logger是不须要关心这些细节的。
你运行了这个程序嘛?这是你看到的效果吧?
Apart from just passing it around everywhere, there are two possibilities. [ 除了在各地传递外,还有两种可能性。] If you're using an IoC container, you might have components receive an ILogger through dependency injection. [ 若是您使用的是IoC容器,则可能会让组件经过依赖注入来接收ILogger。] Packages like AutofacSerilogIntegration can help with that. [ 像AutofacSerilogIntegration这样的软件包能够提供帮助。]
如今最直接的问题是:咱们在应用程序的其余类里面如何得到这个log
对象,除了处处传递以外,还有两个办法。
AutofacSerilogIntegration
的包括帮助你实现这种方式。Log.Logger = new LoggerConfiguration() .WriteTo.Console() .CreateLogger(); Log.Information("Hello again, Serilog!"); Log.CloseAndFlush();
Log
类提供全部与ILogger
接口相同的方法,这里咱们显示调用Log.CloseAndFlush()
来关闭它,而不是使用using
代码块
你可使用依赖注入的方式,也能够是静态属性的方式 - 这取决你的我的喜爱问题。为了简单起见,咱们在本教程中使用了静态Log的方式。
也许,你不是在编写一个控制台应用程序。咱们将使用Console应用做为广为人知的示例,可是你一旦完成了本教程,您应该查看目标平台的文档(例如,ASP.NET Core)。
和一些老的日志类库相比(如log4net),在使用Serilog时,你须要作的就是最大改变就是思考日志事件[log events],而不是日志消息[log message],一条事件[event]由如下几个内容组成:
您能够将日志事件格式化为控制台的可读文本,正如咱们在第一个示例中看到的那样:
11:33:01 [INF] Hello, Serilog!
或者,您能够将相同的事件格式化为JSON并将其发送到远程日志服务器:
{"@t":"2017-11-20T11:33:01.22138","@m":"Hello, Serilog!"}
在背后,应用程序中的日志语句会建立LogEvent
对象,而链接到管道的接收器[sinks]会知道如何记录它们。
### Logging levels
Serilog速度很快,但始终构建和记录详细的日志事件会浪费CPU,磁盘和网络资源。为了管理这个,Serilog事件被赋予了多种级别:Debug
, Information
, Warning
和 Error
等。对应的有一个Log.*()
方法来对应各个级别。
在开发过程当中,可能会打开调试级别的事件:
Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() // <- Set the minimum level .WriteTo.Console() .CreateLogger(); // In a tight loop... Log.Debug("Processing item {ItemNumber} of {ItemCount}", itemNumber, itemCount);
在生成环境中,一般关闭调试的日志,并将最小的日志级别设置成Information
,以便只记录重要的事件,在这篇文档中能够得到有关Serilog Logger Lever的更多信息
Tip: Serilog has special handling for Exception objects; methods like Log.Error() take the exception as the first parameter, e.g. [ 提示:Serilog对Exception对象有特殊的处理;] Log.Error(ex, "Task {TaskName} was canceled", task). [ Log.Error(例如,“任务{任务名称}被取消”,任务)。] Don't include exceptions in the log message itself. [ 不要在日志消息自己中包含异常。]
提示:Serilog对Exception对象有特殊的处理; 像方法Log.Error()
将 exception 做为第一个参数,例如Log.Error(ex, "Task {TaskName} was canceled", task)
,不要将异常的包括在message消息中
让咱们回到最后一个代码片断:
var itemNumber = 10; var itemCount = 999; Log.Debug("Processing item {ItemNumber} of {ItemCount}", itemNumber, itemCount);
您是否注意到日志消息中的{ItemNumber}
这样的占位符? 这不是一个C#的内插字符串[Interpolated string C# 6.0的新特性],Log.*()
方法接收一个消息模板,另一种.NET格式化字符串,除了支持传统的{0}
的方式,还支持{Name}
的方式。
这看起来有点奇怪,除非您意识到经过这样作,Serilog能够将这些消息的一部分做为类的属性与人性化的文本一块儿捕获:
{ "@t": "2017-11-20T11:33:01.22138", "@l": "Debug", "@m": "Processing item 10 of 999", "ItemNumber": 10, "ItemCount": 999 }
咱们为何要这样作?由于经过这种有趣的字段插入方式,并做为属性记录到事件日志中,咱们能够当即使用优雅的简单的过滤器来查找事件,就像ItemNumber > 900
,而无需经过正则表达式从消息中提取了。
进一步,咱们可使用 @
结构捕获运算符 来获取不只仅是平坦的属性值,而是完整的对象:
var user = new { Name = "Nick", Id = "nblumhardt" }; Log.Information("Logged on user {@User}", user);
在这里,user
对象被捕获,并生成的JSON中,以便咱们可使用查询来查找事件,如:User.Id = 'nblumhardt'
{ "@t": "2017-11-20T11:33:01.22138", "@m": "Logged on user {\"Name\": \"Nick\", \"Id\": \"nblumhardt\"}", "User": {"Name": "Nick", "Id": "nblumhardt"} }
生产环境的监控和调试已经很是困难和耗时,并且常常是压力山大的任务:而经过将相关的数据放在手边,Serilog除去了运维操做相关活动的最大难题之一。
Tip: 从Visual Studio Marketplace安装使人惊叹的Serilog Analyzer,能够帮助你检查你的消息模板类型( 注:这个插件还能帮你经过配置代码生成appsetting.json的内容,可是只支持生成一级配置:( )
这实际上有多大的差别取决于你如何收集Serilog的事件。通常来讲,日志事件进入文本文件并用grep
进行搜索。Serilog也能够记录文本文件,但不能在记事本中执行ItemNumber> 900
,所以您须要评估更强大的工具来执行此操做。
若是您的需求很简单,您能够将JSON写入日志文件,并使用支持JSON的工具直接查询文件。] [ Serilog的文件接收器[sink]和紧凑的JSON格式化类库[compact JSON formatter]使第一部分变得简单。 让咱们从新建一个控制台应用程序,并安装下列软件包:
dotnet add package Serilog dotnet add package Serilog.Sinks.File dotnet add package Serilog.Formatting.Compact
在Main
函数中插入:
Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .WriteTo.File(new CompactJsonFormatter(), "log.clef") .CreateLogger(); var itemCount = 99; for (var itemNumber = 0; itemNumber < itemCount; ++itemNumber) Log.Debug("Processing item {ItemNumber} of {ItemCount}", itemNumber, itemCount); Log.CloseAndFlush();
运行这个程序将产生使用Serilog的紧凑格式[compact],在文件log.clef中生成以换行符分隔的JSON
流,若是没有使用CompactJsonFormatter
,则会建立一个简单饿扁平日志文件。
若是你在文本编辑器中打开文件,你会看到JSON事件,就像咱们上面使用的例子。
CLEF-tool 是查询CLEF格式的日志文件的方便的命令行应用程序:(注:貌似只支持windows)
注意第二行的过滤器ItemNumber> 95
:绝不费力地在大型日志流中定位事件,就是结构化日志记录的好处吧。
将日志事件从多个应用程序发送到中央服务器或日志聚合服务很是方便,而不是试图在生产环境中多个日志进行浑水摸鱼[shuffle log files]
日志服务器一般经过HTTP/S或UDP在网络上接收事件,并为开发人员和操做员工程师提供用户界面,以便在出现问题时搜索和分析日志流。
Serilog接收器[sinks]支持大量的日志服务器,其中许多具备结构化数据支持。
注:这段是广告就不翻译了,读者能够根据实际需求选择本身的日志服务器。
咱们刚刚看到消息模板如何实现咱们传统上认为能够有效搜索和分析的日志“消息”。
结构化日志记录的另外一方面是经过某种因果关系或空间关联来识别相关事件集合。事件触发:[Events raised: ]
Serilog经过enrichment来处理全部这些状况(以及其余状况)。Enrichment只是为事件添加附加属性,而不是源自消息模板的属性
最简单的enrichment方法将固定属性值添加到源自日志记录管道的全部事件,能够经过Enrich.WithProperty()
方法快速实现
Log.Logger = new LoggerConfiguration() .Enrich.WithProperty("Application", "Demo") .WriteTo.Seq("http://localhost:5341") //Seq 日志服务器 .CreateLogger();
在LogEvents
上,经过enrichment添加的属性看起来与源自消息模板的属性相同
{ "@t": "2017-11-20T11:33:01.22138", "@l": "Debug", "@m": "Processing item 10 of 999", "ItemNumber": 10, "ItemCount": 999, "Application": "Demo" }
经过建立和使用上下文记录器,能够将属性添加到一个或几个相关事件中,而不是增长具备相同值的全部事件
var orderLog = Log.ForContext("OrderId", order.Id); orderLog.Information("Looking up product codes"); // ... orderLog.Information("Product lookup took {Elapsed} ms", elapsed.TotalMilliseconds);
在这里,经过orderLog
发出的两个事件都会附加一个OrderId
属性。
Enrichmen
是附加的:若是应用程序属性设置在管道级别,则上面的第二个事件将携带Elapsed
(来自消息),OrderId
(来自上下文记录器)和Application
(来自记录管道)。
记录器特定的enrichment一个特例是关于如何使用建立它们的类标记事件
在名为HomeController
的类中,使用如下命令建立类型特定的记录器:
private readonly ILogger _log = Log.ForContext<HomeController>();
经过_log
发出的事件将携带一个值为MyApp.Controllers.HomeController
的SourceContext
属性。
为了丰富工做单元中全部事件[为全部事件添加特定属性],Serilog
提供了LogContext
,首先须要使用Enrich.FromLogContext()
在LoggerConfiguration
级别启用:
Log.Logger = new LoggerConfiguraition() .Enrich.FromLogContext() // ...
LogContext能够被认为是一堆(key,value)
键值对;
using (LogContext.PushProperty("MessageId", message.Id)) { Log.Debug("Dispatching message of type {MessageType}", message.GetType()); await handler.HandleAsync(message); }
关于LogContext有趣的是没有什么须要对象须要传递。在示例代码中,HandleAsync()
以及由它调用的任何其余方法的实现能够直接使用Log
和ILogger
- MessageId属性将自动T并添加LogEvent
中。
Tip: LogContext
是一个堆栈;推送到堆栈上的属性必须经过释放从PushProperty()返回的对象, -- 上述经过手动使用using
块的方式
全部enrichment API都是使用Enricher
的实现Serilog的ILogEventEnricher
接口的对象来实现的。
NuGet中为线程细节,机器信息和用户详细信息等内容提供了一些有趣的预先构建的Enricher实现。
Serilog.Enrichers.Thread 经过 Enrich.WithThreadId() 来添加线程ID相关的扩展:
Log.Logger = new LoggerConfiguration() .Enrich.WithThreadId() // ...
这将为事件附加一个ThreadId
属性,以便交错事件能够再次分开。
咱们将在下一节中看到一个简单的例子,说明如何编写和插入本身的应用程序专用的Enricher
程序。
若是咱们已经知道如何使用Serilog调用消息模板和enrichment结构化日志的两个支柱,那么第三个支柱就是隐式事件类型的概念。
结构化日志适合有效处理大量日志数据。关于大型日志流的一个有趣的观察是,真实产生的事件比编写日志语句代码块时要多的多(注:这算什么发现)
Log.Debug("Processing item {ItemNumber} of {ItemCount}", itemNumber, itemCount);
这意味着,尽管生成了许多独特的消息字符串,如"Processing item 31 of 4159"
,但由此日志记录语句生成的每一个事件共享相同的消息模板,即"Processing item {ItemNumber} of {ItemCount}"
Serilog及其许多sinks 利用这一事实从根本上改进了查询和过滤日志事件的体验。若是消息模板与事件一块儿保存,则下面的过滤器能够当即从嘈杂的日志记录语句中排除成千上万的事件,从而更容易看到不然会被淹没的有趣事件:
@MessageTemplate <> 'Processing item {ItemNumber} of {ItemCount}'
反转也适用 - 放大事件类型能够从单个日志记录语句中查找全部事件
如何利用此功能取决于您存储和搜索日志的位置。接下来咱们会看看细节。
Tip:字符串连接,C#内插字符串,以及其余技术手段来预格式化传递给Serilog的消息内容,会取消此功能。详细请看 How (not) to parameterize Serilog events
存储,而后过滤罗嗦的消息模板并不老是理想的。 相反,一般从消息模板建立一个数字哈希值,并将其与事件一块儿存储:
日志文件和本地不支持消息模板的日志服务器仍然能够经过在Serilog管道中明确地enricher
事件来接收事件类型。
为此,自定义enricher
程序将EventType
属性附加到每一个事件
// Install-Package Murmurhash class EventTypeEnricher : ILogEventEnricher { public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { var murmur = MurmurHash.Create32(); var bytes = Encoding.UTF8.GetBytes(logEvent.MessageTemplate.Text); var hash = murmur.ComputeHash(bytes); var numericHash = BitConverter.ToUInt32(hash, 0); var eventType = propertyFactory.CreateProperty("EventType", numericHash); logEvent.AddPropertyIfAbsent(eventType); } }
当插入管道时,这会使{EventType}
属性可用于sinks
Log.Logger = new LoggerConfiguration() .Enrich.With<EventTypeEnricher>() .WriteTo.Console(outputTemplate: "{Timestamp:HH:mm:ss} [{EventType:x8} {Level:u3}] {Message:lj}{NewLine}{Exception}") .CreateLogger();
WriteTo.Console()
的参数是一个Serilog输出模板,描述了如何将日志事件的属性格式化为文本。 大多数基于文本的sinks(包括Serilog.Sinks.File
)均可以接受输出模板来指导格式化。
[ 输出以下所示:]
Serilog是一个强大的库,拥有普遍的插件和工具生态系统。咱们只涉及绝对的基础知识 - 取决于您但愿如何使用Serilog以及您使用的框架和平台,还有不少能够发现的地方。
这里有一些文章和扩展,供你参考:
appsettings.json
configuration - in this article we've only shown the C# configuration API; Serilog.Settings.Configuration adds support for logger configuration in .NET Core JSON settings<appSettings>
configuration - Serilog.Settings.AppSettings adds support for reading logger configuration from .NET Framework configuration filesSerilog有三大优秀的社区支持渠道:
Happy logging!