[AspNetCore3.1] 使用Serilog记录日志
阅读原文时间:2023年07月10日阅读:1

用到的单词

Sink 接收器模块、输出方式、接收模块库、输出模块库 Diagnostic 诊断 Enricher 扩展器 embedded 嵌入式的 compact 紧凑的、简洁的 concept 概念 usage 用法 restrict 限制、约束 raise 提升 necessary 必要的 digging 挖掘

1.1 添加 Nuget 引用

Serilog.AspNetCore 日志包主体

Serilog.AspNetCore.RollingFile 将日志写入文件

1.2 注册服务

1.2.1 在 appsettings.json 中添加 Serilog 节点。

简单说明下配置文件的意思:

  • 将日志写入RollingFile(文件)和Console(控制台)。

  • RollingFile 的具体配置:记录文件到 根目录/logs/{日期}.txt 文件内,每天记录一个文件,并且只记录 Warning 及其以上的日志;

  • 默认日志级别记录 Debug 及其以上的日志。

  • 如果日志包含 Microsoft System ,只记录级别为 Information 及以上的日志。

    {
    "Serilog": {
    "WriteTo": [
    {
    "Name": "RollingFile",
    "Args": {
    "pathFormat": "logs\{Date}.txt",
    "RestrictedToMinimumLevel": "Warning"
    }
    },
    {
    "Name": "Console"
    }
    ],
    "MinimumLevel": {
    "Default": "Debug",
    "Override": {
    "Microsoft": "Information",
    "System": "Information"
    }
    }
    },
    }

1.2.2 修改 program.cs,注册 Serilog

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        }).UseSerilog((context, configure) =>
        {
            configure.ReadFrom.Configuration(context.Configuration);
        });

1.2.3 简单配置完成,现在可以在项目中方便的使用 Serilog 了。

private readonly ILogger<HomeController> _logger;

public HomeController(ILogger<HomeController> logger)
{
    _logger = logger;
}

public IActionResult Index()
{
    _logger.LogError("Error 测试");
    return View();
}

2.1 日志接收器 (Sink)

接收器用来配置日志记录到哪种介质。比如说 Console(输出到控制台),File(日志文件)等。就是我们上面配置的 WriteTo 节点。

接收器一般以扩展库的方式提供的,为了将日志写入文件,我们在上面引入了 Serilog.AspNetCore.Sinks.RollingFile 包。

除了在配置文件中使用 WriteTo 指定,也可以通过代码配置,比如在 Console 控制台程序中使用:

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .CreateLogger();

Log.Information("Ah, there you are!");

可以同时使用多个接收器,用法很简单,支持链式调用:

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .WriteTo.File("log-.txt", rollingInterval: RollingInterval.Day)
    .CreateLogger();

基于文本的接收器使用 output templates 来控制格式。

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

格式这部分涉及到扩展器,会在后面具体说明,这里只是简单提一下如何使用。

可以使用每个接收器指定不同的日志级别

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .WriteTo.File("log.txt")
    .WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information)
    .CreateLogger();

上面代码中指定了默认日志级别为 Debug,但为 Console Sink 的日志重写级别为 Information

==========

在声明Logger时可以通过 MinimumLevel.Debug() 指定最小级别,而在 WriteTo 中指定接收器Sink时,也可以通过 restrictetToMinimumLevel:LogEventLevel.Information 指定最小级别,那两者之间的关系是怎么样的呢?

注意:接收器Sink的级别必须高于Logger的级别。 假设Logger的默认级别为 Information,即便 Sink 重写日志级别为 LogEventLevel.Debug,也只能看到 Information 及以上级别的日志。这是因为默认级别的配置控制哪些事件级别的日志记录语句可以被创建,而 Sink 级别仅仅是对这些事件进行过滤。

2.2 扩展器 (Enricher)

顾名思义,扩展器可以添加,删除或修改附加到日志事件的属性。 例如,下面的代码可以达到将线程ID附加到每个事件的目的。

class ThreadIdEnricher : ILogEventEnricher
{
    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
    {
        logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
                "ThreadId", Thread.CurrentThread.ManagedThreadId));
    }
}

使用 Enricher 将扩展添加到配置对象

Log.Logger = new LoggerConfiguration()
    .Enrich.With(new ThreadIdEnricher())
    .WriteTo.Console(
        outputTemplate: "{Timestamp:HH:mm} [{Level}] ({ThreadId}) {Message}{NewLine}{Exception}")
    .CreateLogger();

注意模板中的 {ThreadId},在日志中打印的便是当前线程的ID了

如果扩展的属性值在整个应用程序运行期间都是恒定的,则可以使用快捷方式WithProperty方法简化配置。

Log.Logger = new LoggerConfiguration()
    .Enrich.WithProperty("Version", "1.0.0")
    .WriteTo.Console()
    .CreateLogger();
可用的扩展器包
  • Serilog.Enrichers.Environment - WithMachineName() and WithEnvironmentUserName()
  • Serilog.Enrichers.Process - WithProcessId()
  • Serilog.Enrichers.Thread - WithThreadId()
其他有意思的扩展器包
  • Serilog.Web.Classic - WithHttpRequestId() and many other enrichers useful in classic ASP.NET applications
  • Serilog.Exceptions - WithExceptionDetails() adds additional structured properties from + exceptions
  • Serilog.Enrichers.Demystify - WithDemystifiedStackTraces()

2.3 过滤器 (Filter)

可以通过过滤器有选择的记录事件。过滤器只是 LogEvent 的谓词,其中一些常见的情况由 Matching 类处理。

只要在调试过程中打个断点,就可以看到 LogEvent 的详细属性,这里就不赘述,懒~~~

var log = new LoggerConfiguration()
    .WriteTo.Console(
        outputTemplate:
        "{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {Count} {Message:j} {NewLine}")
    .Filter.ByExcluding(Matching.WithProperty<int>("Count", p => p < 10))
    .CreateLogger();

log.Information("测试 {Count}", 20);
log.Information("测试 {Count}", 3);
log.Information("测试 {Count}", 11);

Console.Read();

// 输出:
// 2020-05-05 00:04:46 INF 20 测试 20
// 2020-05-05 00:04:47 INF 11 测试 11

代码说明:忽略参数 Count 小于 10 的日志,所以最终只输出了 2 条。

一个基本的OutTemplate模板大概长这样:

{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} {NewLine} {Exception}
从头到尾分别是:日期、日志级别、日志内容、换行、异常信息

模板可用参数

Serilog的日志是由LogEvent构成的,其可用的参数对应了LogEvent的public属性,分别是下面几个:

  • 时间戳【Timestamp】- 日志产生时的时间戳,可定义格式
  • 日志级别【Level】- 日志的级别,可选定义格式
  • 日志内容【Message】- 日志的内容,一般由字符串模板和参数构成,类似 string.Format(temp, param1, param2, …),但比Format强大的多
  • 命名属性【Properties】- 包涵 扩展器定义的属性 以及 日志内容中提供的参数,后面细说
  • 异常【Exception】- 如果是输出的异常,会完整异常信息和栈跟踪信息。如果没有异常则为空
  • 换行【NewLine】- 就是调用 System.Environment.NewLine

个人理解的模板可以分两种

  • 【日志】的输出模板,由 OutTemplate 定义
  • 【日志内容】的输出模板,就是上面整体的模板中【Message】的部分,在写日志时由调用者提供,如:
    log.Error("测试 {Count}", 10)

格式化输出

serilog 的输出很有意思,并不仅仅能输出字符串,还可以输出对象或枚举。

这里主要以【Message】为说明对象,事实上也有【Properties】用法也差不多,但输出样式稍微有些区别

请看下面的代码:

var user = new User { Id = 1, Name = "张三" };
log.Debug("当前用户: {User},当前时间:{CurrentTime}", user, DateTime.Now)
  1. 在模板字符串内,包含在 {} 中的内容为“标量”名称,这里输出的内容由后面实参决定
  2. 如果要原样输出大括号,只要 {{内容}} 就可以了
  3. 标量和参数的对应关系是从左到右一一对应
  4. 标准 string.Format 也能用,比如 log.Debug("{0} {1}!", "Hello", "world"),但建议不要混用

默认的输出行为

1. 简单标量类型:
  • 布尔类型 - bool
  • 数值类型 - byte,short,ushort,int,unit,long,ulong,float,double,decimal
  • 字符串 - string,byte[]
  • 日期 - DateTime,DateTimeOffset,TimeSpan
  • 其他 - Guid,Uri
  • 可空类型 - 上面所有类型的可空版本

上面这些类型,表达的内容基本上不会有歧义,所以一般处理方式就是直接调用 .ToString() 方法

var count = 100;
log.Debug("检索到{Count}条记录", count);    // 检索到100条记录

其他类型都差不多,大家可以自己测试。

2. 集合 Collection

如果属性值传递的IEnumerable,Serilog会将其视为一个集合

var fruits = new[] {"apple", "pear", "orange"};
log.Information("我有这些水果:{fruit}", fruits);  // 我有这些水果:["apple", "pear", "orange"]

var fruits2 = new Dictionary<string,int> {{ "Apple", 1}, { "Pear", 5 }};
log.Information("In my bowl I have {Fruit}", fruits2); // In my bowl I have {"Apple": 1, "Pear": 5}

可以看出,Serilog很智能,根据不同的集合类型输出了不同格式,而该格式正好可以表达数据的内容

3. 对象 object
var user = new User() {Id = 1, Name = "张三"};
log.Information("{User}", user);        // "SerilogSample.User"
log.Information("{@User}", user);       // {"Id": 1, "Name": "张三", "$type": "User"}
log.Information("{$User}", user);       // "SerilogSample.User"
  • 默认情况下,如果Serilog没能识别数据类型,直接调用 ToString(),对应实例 1

  • 上面那种情况,我们一般希望保留对象的结构,对此,Serilog提供了 @ 符号,叫做 析构运算符。添加该符号后,默认输出对象的JSON格式,对应实例 2

  • 强制字符串化,通过 $ 符号,可以强制调用对应参数的 ToString() 方法,对应实例3

  • 补充下上面一条,如果 log.Information("{$User}", "abc") 输出啥呢?很简单,"abc".ToString() 所以输出内容就是 abc 嘛

  • 自定义数据,注意代码的第二行,Serilog提供了很多不同的析构策略,大家自己试吧。注意:ByTransforming 必须返回与输入参数不同的类型,不然会被递归调用

    var log = new LoggerConfiguration()
    .Destructure.ByTransforming(u => new { UserName = u.Name })
    .WriteTo.Console(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {Message:j} {NewLine}")
    .CreateLogger();

    var user = new User() {Id = 1, Name = "张三"};
    log.Information("{@User:l}", user); // {"UserName": "张三"}

格式化输出

var log = new LoggerConfiguration()
    .WriteTo.Console(
        outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} {User} {EventType} {Level:u3} {Message} {Exception} {NewLine}")
    .Enrich.WithProperty("EventType", "事件类型")
    .Enrich.WithProperty("User", new User { Id = 1, Name = "测试" })
    .CreateLogger();

var exception = new Exception("人工异常");
log.Error(exception, "啥也不是");

格式说明:

  • Timestamp:时间戳,指定格式:{Timestamp: yyyy-MM-dd HH:mm:ss.fff zzz}

  • Level:日志级别,默认为完整的级别名称,如:Information。可选格式 :u3 (日志级别缩写,三个字母,大写)和 :w3 (日志级别缩写,三个字母,小写),效果:{Level:u3}=INF;{Level:w3}=inf

  • Properties:命名属性,除了上面提到过的5个参数(Timestamp,Exception,Message,NewLine,Level)外的所有其他参数。这些参数来源于:

    var log = new LoggerConfiguration()
    .WriteTo.Console(
    outputTemplate:
    "{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {User:lj} {Message:j} {NewLine}")
    .Filter.ByExcluding(Matching.WithProperty("Count", p => p < 10))
    .Enrich.WithProperty("User", "Enricher")
    .CreateLogger();

    log.Information("{User}", "Message");

    // 输出:2020-05-05 00:19:39 INF Message "Message"

虽然在Enricher中指定了属性User,但最终输出的是Message模板指定的实参。修改一下代码:

var log = new LoggerConfiguration()
    .WriteTo.Console(
        outputTemplate:
        "{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {User:lj} {Message:j} {NewLine}")
    .Filter.ByExcluding(Matching.WithProperty<int>("Count", p => p < 10))
    .Enrich.WithProperty("User", "Enricher")
    .CreateLogger();

log.Information("{User1}", "Message");

// 输出:2020-05-05 00:19:39 INF Enricher "Message"
  • Message: 消息内容;如果要输出对象的JSON,一般会指定格式参数 {Message:lj}

    参数 l 的作用:

    var log = new LoggerConfiguration()
    .WriteTo.Console(
    outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {Message:l}")
    .CreateLogger();

    log.Information("{a} {b}", 23, "abc");

    // 输出为:2020-05-04 23:41:12 INF 23 abc

    var log = new LoggerConfiguration()
    .WriteTo.Console(
    outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {Message}")
    .CreateLogger();

    log.Information("{a} {b}", 23, "abc");

    // 输出为:2020-05-04 23:44:21 INF 23 "abc"

注意输出的 abc 字符串,添加了参数 :l后,若格式的实参是字符串类型,会自动删除双引号。

如果要输出一个对象,应该怎么做呢?看下面的三种做法:

var log = new LoggerConfiguration()
    .WriteTo.Console(
        outputTemplate:
        "{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {Message}")
    .CreateLogger();

var user = new User {Id = 1, Name = "张三"};

log.Information("{u}", user);

// 输出为:2020-05-04 23:47:08 INF "SerilogSample.User"

可以看到,输出的是对象类型的名称,修改代码如下:

var log = new LoggerConfiguration()
    .WriteTo.Console(
        outputTemplate:
        "{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {Message}")
    .CreateLogger();

var user = new User {Id = 1, Name = "张三"};

log.Information("{@u}", user);

// 输出为:2020-05-04 23:49:59 INF User {Id=1, Name="张三"}

添加参数 :j,修改代码如下:

var log = new LoggerConfiguration()
    .WriteTo.Console(
        outputTemplate:
        "{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {Message:j}")
    .CreateLogger();

var user = new User {Id = 1, Name = "张三"};

log.Information("{@u}", user);

// 输出为:2020-05-04 23:51:42 INF {"Id": 1, "Name": "张三", "$type": "User"}

由上面的测试可以看出,

  • 如果要输出一个对象,输出模板形参前要添加@符号
  • 如果要将对象输出成标准json格式,需要在日志模板中添加格式符号 {Message:j}
  • 一般情况下,只要固定写法 {Message:lj} 就够了

修改下 Program.cs 文件

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureAppConfiguration(configure => configure.AddJsonFile("Serilog.json"));
            webBuilder.UseStartup<Startup>();
        })
        .UseSerilog((context, configure) =>
        {
            configure.MinimumLevel.Information()
                .WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Debug)
                .Filter.With<MicrosoftFilter>()
                .WriteTo.RollingFile("logs\\{Date}.txt", LogEventLevel.Warning);
        });

自定义一个 Filter

public class MicrosoftFilter : ILogEventFilter
{
    public bool IsEnabled(LogEvent logEvent)
    {
        if (!logEvent.Properties.TryGetValue("SourceContext", out var source))
        {
            return logEvent.Level >= LogEventLevel.Debug;
        }

        if (source.ToString().StartsWith("\"Microsoft"))
        {
            return logEvent.Level >= LogEventLevel.Warning;
        }

        return logEvent.Level >= LogEventLevel.Debug;
    }
}

差不多就这样吧~~告辞!!!

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器