当前位置: 首页 > 工具软件 > Serilog > 使用案例 >

Serilog 2.10 中文文档

乜华翰
2023-12-01

本文基于发稿时的最新版本,Serilog: 2.10

1. 快速开始

1.1 控制台应用

这里以控制台应用为例,首先安装以下三个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();
    }
}

1.2 在ASP.NET Core应用中使用Serilog

1.2.1 安装nuget包

首先安装Serilog.AspNetCor

1.2.2 修改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>();
            });

1.2.3 删除appsettings.json里的Logging节点

删除之后,即可使用

1.2.4 两步初始化

在上面的例子中我们在程序启动时就初始化了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>();
        });

2. 基本配置

2.1 Sink

上面的实例代码的WriteTo.Console()WriteTo.File(),就是不同的sinker。用来控制把日志写入到哪里。除了控制台和文件系统,你还可以通过安装不同的nuget包把日志写入各种存储系统,如数据库、消息队列、AWS、Azure、邮箱、HTTP等很多地方。

详见Provided-Sinks

2.2 输出模板

.WriteTo.File("log.txt",
        outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")

默认的输出模板就是上面的样子,TimestampLevel都是内置属性。{Message:lj}表示将消息序列化成json字符串,string类型除外(j表示json,l表示except for string literals)。

{Level:u3}表示将日志等级的输出显示为3个大写字符,如DBGINFERR等。{Level:w3}表示三个字符的小写。

同理,可以增加一个{Properties:j}用来显示额外的上下文信息。

2.3 日志等级

同大多数的日志框架一样,分为VerboseDebugInformationWarningErrorFatal六个等级。在之前的例子中我们可以看到,使用.MinimumLevel.Debug()配置了最低等级,小于此等级的日志不会被打印出来。

默认日志等级:如果没有配置MinimumLevel的话,默认等级为Information

2.3.1 日志等级判断

可以通过Log.IsEnabled(LogEventLevel.Debug)来判断某个等级是否启用。

2.3.2 动态日志等级

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");

2.4 不同级别的日志输出到不同的地方

我们可以通过配置不同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级别的日志。

2.5 Enrichers

输出模板里我们介绍过{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

详见

2.6 根据Enricher的值进行过滤

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .Filter.ByExcluding(Matching.WithProperty<int>("Count", p => p < 10))
    .CreateLogger();

Count的值小于10时,不会打印日志。

2.7 Sub-logger

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .WriteTo.Logger(lc => lc
        .Filter.ByIncludingOnly(...)
        .WriteTo.File("log.txt"))
    .CreateLogger();

个人感觉是sinker加上filter。

2.8 从配置文件读取配置

可以通过 Serilog.Settings.AppSettingsSerilog.Settings.Configuration 两个nuget包实现。

3. 结构化数据

  1. 对于intboolstringGuidUri等简单的数据类型,Serilog可以转为字符串输出。

  2. 对于ListDictionary等集合类型,会自动序列化为json字符串。

  3. 对于复杂对象,可以使用@操作符将对象序列化为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对象的NameId属性输出:

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]

4. 消息模板

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}

消息模板详见

5. 自定义序列化json

Serilog自带了三个json序列化程序:

  • Serilog.Formatting.Json.JsonFormatter- 这是Serilog软件包中附带的历史默认值。它生成日志事件的完整呈现,并支持一些配置选项。
  • Serilog.Formatting.Compact.CompactJsonFormatter- 存在于Serilog.Formatting.Compactnuget包,提供了更节省空间的 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

 类似资料: