本文基于发稿时的最新版本,Serilog: 2.10
这里以控制台应用为例,首先安装以下三个nuget包:
Serilog
Serilog.Sinks.Console
Serilog.Sinks.File
(可以用命令行方式安装或通过VS安装,随意)
第二个nuget包,用来将日志输出到控制台。第三个用来将日志输出到文件。
然后,修改代码如下:
static void Main()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()//最小日志等级
//日志打印到控制台
.WriteTo.Console()
//日志打印到文件上
.WriteTo.File("logs/myapp.txt", rollingInterval: RollingInterval.Day)
//按日期生成日志路径,需要安装nuget: Serilog.Sinks.Map
.WriteTo.Map(
le => le.Timestamp.Date,
(d, lc) => { lc.File($"logs/{d:yyyyMMdd}/log.txt"); })
.CreateLogger();
Log.Information("Hello, world!");
int a = 10, b = 0;
try
{
Log.Debug("Dividing {A} by {B}", a, b);
Console.WriteLine(a / b);
}
catch (Exception ex)
{
Log.Error(ex, "Something went wrong");
}
finally
{
Log.CloseAndFlush();
}
}
首先安装Serilog.AspNetCor
包
Program.cs
代码 public static int Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Console()
.CreateLogger();
try
{
Log.Information("Starting web host");
CreateHostBuilder(args).Build().Run();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly");
return 1;
}
finally
{
Log.CloseAndFlush();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog() // 添加Serilog
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
appsettings.json
里的Logging
节点删除之后,即可使用
在上面的例子中我们在程序启动时就初始化了Serilog,这样做好处是可以捕获到Host配置的异常,但没法使用appsettings.json
和依赖注入。
所以为了能够使用配置文件和依赖注入,Serilog支持第二次初始化(两步初始化)。通过在UseSerilog
里配置回调实现:
首先,将CreateLogger
改为CreateBootstrapLogger
:
public static int Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Console()
.CreateBootstrapLogger(); // 修改此项
然后,在UserSerilog
里配置回调:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog((context, services, configuration) => configuration
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext()
.WriteTo.Console())
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
上面的实例代码的WriteTo.Console()
和WriteTo.File()
,就是不同的sinker。用来控制把日志写入到哪里。除了控制台和文件系统,你还可以通过安装不同的nuget包把日志写入各种存储系统,如数据库、消息队列、AWS、Azure、邮箱、HTTP等很多地方。
.WriteTo.File("log.txt",
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
默认的输出模板就是上面的样子,Timestamp
和Level
都是内置属性。{Message:lj}
表示将消息序列化成json字符串,string类型除外(j
表示json,l
表示except for string literals)。
{Level:u3}
表示将日志等级的输出显示为3个大写字符,如DBG
、INF
、ERR
等。{Level:w3}
表示三个字符的小写。
同理,可以增加一个{Properties:j}
用来显示额外的上下文信息。
同大多数的日志框架一样,分为Verbose
、Debug
、Information
、Warning
、Error
和Fatal
六个等级。在之前的例子中我们可以看到,使用.MinimumLevel.Debug()
配置了最低等级,小于此等级的日志不会被打印出来。
默认日志等级:如果没有配置MinimumLevel
的话,默认等级为Information
。
可以通过Log.IsEnabled(LogEventLevel.Debug)
来判断某个等级是否启用。
var levelSwitch = new LoggingLevelSwitch();
levelSwitch.MinimumLevel = LogEventLevel.Warning;
var log = new LoggerConfiguration()
.MinimumLevel.ControlledBy(levelSwitch)
.WriteTo.ColoredConsole()
.CreateLogger();
当需要切换日志等级时,直接修改:
levelSwitch.MinimumLevel = LogEventLevel.Verbose;
log.Verbose("This will now be logged");
我们可以通过配置不同sinker的restrictedToMinimumLevel
的属性,来让不同级别的日志落到不同的sinker上。
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.File("log.txt")
.WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information)
.CreateLogger();
以上代码会将所有的level的日志输出到log.txt
上,但只有Information
及以上级别的日志会输出到Console
上。
看到这里,也许你会问:MinimumLevel.Debug()
和sinker里的restrictedToMinimumLevel
有什么区别?
其实MinimumLevel.Debug()
只是来负责控制哪些级别的日志可以触发WriteTo
操作,而restrictedToMinimumLevel
只是用来根据级别过滤这些日志。如果MinimumLevel
设置为Information
,即使restrictedToMinimumLevel
设置为Debug
,最终也不会看到Debug级别的日志。
输出模板里我们介绍过{Timestamp:yyyy}
、{Level}
都属于Enricher,只不过这些都是框架内置的。我们也可以定义自己的Enricher来打印自定义的内容,如下就是一个需要打印出线程Id的Enricher:
class ThreadIdEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
"ThreadId", Thread.CurrentThread.ManagedThreadId));
}
}
使用,通过{ThreadId}
:
Log.Logger = new LoggerConfiguration()
.Enrich.With(new ThreadIdEnricher())
.WriteTo.Console(
outputTemplate: "{Timestamp:HH:mm} [{Level}] ({ThreadId}) {Message}{NewLine}{Exception}")
.CreateLogger();
如果你想打印的ThreadId
是固定的,就不用定义ThreadIdEnricher
类,可以直接这么写:
Log.Logger = new LoggerConfiguration()
.Enrich.WithProperty("ThreadId","123")
.WriteTo.Console(
outputTemplate: "{Timestamp:HH:mm} [{Level}] ({ThreadId}) {Message}{NewLine}{Exception}")
.CreateLogger();
框架支持的Enricher有以下几种:
名称 | nuget包 |
---|---|
WithMachineName() 和WithEnvironmentUserName() | Serilog.Enrichers.Environment |
WithProcessId() | Serilog.Enrichers.Process |
WithThreadId() | Serilog.Enrichers.Thread |
WithHttpRequestId() | Serilog.Web.Classic |
WithExceptionDetails() | Serilog.Exceptions |
WithDemystifiedStackTraces() | Serilog.Enrichers.Demystify |
WithCorrelationId() | Serilog.Enrichers.CorrelationId |
WithClientIp() 、WithClientAgent() | Serilog.Enrichers.ClientInfo |
WithXllPath() | Serilog.Enrichers.ExcelDna |
WithSensitiveDataMasking() | Serilog.Enrichers.Sensitive |
FromGlobalLogContext() | Serilog.Enrichers.GlobalLogContext |
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.Filter.ByExcluding(Matching.WithProperty<int>("Count", p => p < 10))
.CreateLogger();
当Count
的值小于10时,不会打印日志。
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.Logger(lc => lc
.Filter.ByIncludingOnly(...)
.WriteTo.File("log.txt"))
.CreateLogger();
个人感觉是sinker加上filter。
可以通过 Serilog.Settings.AppSettings
或 Serilog.Settings.Configuration
两个nuget包实现。
对于int
、bool
、string
、Guid
、Uri
等简单的数据类型,Serilog可以转为字符串输出。
对于List
、Dictionary
等集合类型,会自动序列化为json字符串。
对于复杂对象,可以使用@
操作符将对象序列化为json字符串,否则会直接调用ToString()
方法。如有以下代码:
var person = new Person { Name = "aa", FirstName = "bb", Id = 5 };
Log.Information("Processing {Person}", person);
Log.Information("Processing {@Person}", person);
输出为:
[16:22:33 INF] Processing CalcStringDuplicated.Person
[16:22:33 INF] Processing {"Name": "aa", "FirstName": "bb", "Id": 5, "$type": "Person"}
如何自定义数据的输出结构?
可以使用ByTransforming
,比如下述代码我们只需要把Person
对象的Name
和Id
属性输出:
Log.Logger = new LoggerConfiguration()
.Destructure.ByTransforming<Person>(
r => new { Name = r.Name, Id = r.Id })
.WriteTo...
输出为:
[16:27:39 INF] Processing CalcStringDuplicated.Person
[16:27:39 INF] Processing {"Name": "aa", "Id": 5}
强制将object转为string
上面说到对于List
等集合类型,Serilog会自动序列化为json字符串,如果不想这么做,可以使用$
操作符:
var c =new List<int>{10};
Log.Information("value = {$c}", c);
输出为:
[16:35:13 INF] value = System.Collections.Generic.List`1[System.Int32]
Serilog建议使用消息模板将日志展示出来,而不是直接展示消息。
// 不推荐
Log.Information("The time is " + DateTime.Now);
// 推荐做法
Log.Information("The time is {Now}", DateTime.Now);
这个模板的语法类似于string.format()
. 大括号里的属性命名规则同普通属性一致,建议使用Pascal命名,但你也可以随便写。
属性和后面参数对象是根据先后位置对应的,而不是根据名称。
以下代码:
var person = new Person { Name = "aa", FirstName = "bb", Id = 5 };
var person2 = new Person { Name = "aa2", FirstName = "22", Id = 5 };
Log.Information("Processing {@Person2}---{@Person}",person, person2);
Log.Information("Processing {@person}---{@person2}", person, person2);
Log.Information("Processing {@person2}---{@person}", person, person2);
Log.Information("Processing {@a}---{@b}", person, person2);
输出结果都一样:
[17:21:58 INF] Processing {"Name": "aa", "Id": 5}---{"Name": "aa2", "Id": 5}
[17:21:58 INF] Processing {"Name": "aa", "Id": 5}---{"Name": "aa2", "Id": 5}
[17:21:58 INF] Processing {"Name": "aa", "Id": 5}---{"Name": "aa2", "Id": 5}
[17:21:58 INF] Processing {"Name": "aa", "Id": 5}---{"Name": "aa2", "Id": 5}
消息模板也支持类似string.format
的{0}{1}
。如:
Log.Information("Processing {@1}---{@0}", person, person2);
输出为:
[17:24:07 INF] Processing {"Name": "aa2", "Id": 5}---{"Name": "aa", "Id": 5}
Serilog自带了三个json序列化程序:
Serilog.Formatting.Json.JsonFormatter
- 这是Serilog软件包中附带的历史默认值。它生成日志事件的完整呈现,并支持一些配置选项。Serilog.Formatting.Compact.CompactJsonFormatter
- 存在于Serilog.Formatting.Compact
nuget包,提供了更节省空间的 JSON 格式化程序。Serilog.Formatting.Compact.RenderedCompactJsonFormatter
- 同样附带在Serilog.Formatting.Compact
包中,这个格式化程序将消息模板预渲染成文本。自定义formater:
class User
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime Created { get; set; }
}
class CustomDateFormatter : IFormatProvider
{
readonly IFormatProvider basedOn;
readonly string shortDatePattern;
public CustomDateFormatter(string shortDatePattern, IFormatProvider basedOn)
{
this.shortDatePattern = shortDatePattern;
this.basedOn = basedOn;
}
public object GetFormat(Type formatType)
{
if (formatType == typeof(DateTimeFormatInfo))
{
var basedOnFormatInfo = (DateTimeFormatInfo)basedOn.GetFormat(formatType);
var dateFormatInfo = (DateTimeFormatInfo)basedOnFormatInfo.Clone();
dateFormatInfo.ShortDatePattern = this.shortDatePattern;
return dateFormatInfo;
}
return this.basedOn.GetFormat(formatType);
}
}
public class Program
{
public static void Main(string[] args)
{
var formatter = new CustomDateFormatter("dd-MMM-yyyy", new CultureInfo("en-AU"));
Log.Logger = new LoggerConfiguration()
.WriteTo.Console(formatProvider: new CultureInfo("en-AU")) // Console 1
.WriteTo.Console(formatProvider: formatter) // Console 2
.CreateLogger();
var exampleUser = new User { Id = 1, Name = "Adam", Created = DateTime.Now };
Log.Information("Created {@User} on {Created}", exampleUser, DateTime.Now);
Log.CloseAndFlush();
}
}
输出:
[13:57:12 INF] Created {"Id": 1, "Name": "Adam", "Created": "2020-09-01T13:56:59.7803740-05:00", "$type": "User"} on 1/09/2020 1:57:12 PM
[13:57:12 INF] Created {"Id": 1, "Name": "Adam", "Created": "2020-09-01T13:56:59.7803740-05:00", "$type": "User"} on 01-Sep-2020 1:57:12 PM
参考:
https://github.com/serilog/serilog/wiki/Getting-Started