mcpField
S
serilog

serilog

github.com/codewithmukesh开发7913.0k 次安装12 周前更新
代码仓库

关于 serilog

面向 .NET 开发的 Serilog 使用指南,介绍日志配置与实践模式,帮助开发者构建结构化日志、记录启动和请求事件,并规避常见问题。

主要功能

  1. 01两阶段初始化日志记录器
  2. 02使用结构化消息模板
  3. 03通过 appsettings.json 配置
  4. 04请求日志中间件
  5. 05LogContext 作用域属性
  6. 06导出事件到 OTLP

适用场景

  1. 01捕获应用启动期间的错误
  2. 02按命名属性查询日志
  3. 03汇总 ASP.NET Core 请求
  4. 04将事件导出到 OTLP 后端

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill serilog

安装后在 Claude Code 中输入/serilog确认已加载。

详细介绍

Serilog

核心原则

  1. 两阶段初始化 — 为启动阶段创建引导日志记录器,然后在 DI 就绪后将其替换为完整的日志记录器。这样可以捕获否则会丢失的启动错误。
  2. 优先使用 AddSerilog(),而非 UseSerilog() — 使用 builder.Services.AddSerilog()(现代 API),而不是 builder.Host.UseSerilog()。它通过 ReadFrom.Services(services) 集成 DI 服务。
  3. 使用消息模板,而非字符串插值 — {PropertyName} 语法会创建可供查询的结构化数据。字符串插值($"...")会破坏结构,并且即使日志级别已禁用也会分配内存。
  4. 通过 appsettings.json 配置 — 将日志级别、接收器和覆盖项保存在配置中,以便按环境更改,而无需重新部署。

模式

两阶段引导设置
using Serilog;

// Stage 1: Bootstrap logger — captures startup errors before DI
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
    .Enrich.FromLogContext()
    .WriteTo.Console()
    .CreateBootstrapLogger();

try
{
    Log.Information("Starting application");

    var builder = WebApplication.CreateBuilder(args);

    // Stage 2: Full logger with DI and configuration
    builder.Services.AddSerilog((services, lc) => lc
        .ReadFrom.Configuration(builder.Configuration)
        .ReadFrom.Services(services)
        .Enrich.FromLogContext()
        .Enrich.WithMachineName()
        .Enrich.WithEnvironmentName()
        .Enrich.WithProperty("Application", "MyApp.Api"));

    var app = builder.Build();

    app.UseSerilogRequestLogging(options =>
    {
        options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
        {
            diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
            diagnosticContext.Set("UserAgent",
                httpContext.Request.Headers.UserAgent.ToString());
        };
    });

    app.Run();
}
catch (Exception ex)
{
    Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
    await Log.CloseAndFlushAsync();
}
appsettings.json 配置
{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "Microsoft.AspNetCore": "Warning",
        "Microsoft.EntityFrameworkCore": "Warning",
        "Microsoft.Hosting.Lifetime": "Information",
        "System": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
        }
      },
      {
        "Name": "File",
        "Args": {
          "path": "logs/app-.log",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 30,
          "fileSizeLimitBytes": 104857600
        }
      },
      {
        "Name": "Seq",
        "Args": { "serverUrl": "http://localhost:5341" }
      }
    ],
    "Enrich": ["FromLogContext", "WithMachineName", "WithEnvironmentName"],
    "Destructure": [
      { "Name": "ToMaximumDepth", "Args": { "maximumDestructuringDepth": 4 } },
      { "Name": "ToMaximumStringLength", "Args": { "maximumStringLength": 1024 } },
      { "Name": "ToMaximumCollectionCount", "Args": { "maximumCollectionCount": 10 } }
    ]
  }
}

覆盖部分使用与 SourceContext 匹配的命名空间前缀。更具体的前缀优先级更高。

请求日志中间件

将 ASP.NET Core 为每个请求生成的多条日志事件替换为一条摘要事件。

app.UseSerilogRequestLogging(options =>
{
    options.MessageTemplate =
        "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms";

    options.GetLevel = (httpContext, elapsed, ex) => ex is not null
        ? LogEventLevel.Error
        : httpContext.Response.StatusCode >= 500
            ? LogEventLevel.Error
            : LogEventLevel.Information;

    options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
    {
        diagnosticContext.Set("UserId",
            httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anonymous");
    };
});
结构化日志记录与解构
// Named properties — creates queryable structured data
logger.LogInformation("Order {OrderId} placed by {CustomerId} for {Total:C}",
    orderId, customerId, total);

// @ operator preserves object structure as properties
logger.LogInformation("Processing {@SensorInput}", sensorInput);
// Output: Processing {"Latitude": 25, "Longitude": 134}

// $ operator forces ToString()
logger.LogInformation("Received {$Data}", new[] { 1, 2, 3 });
// Output: Received "System.Int32[]"
使用 LogContext 设置作用域属性
using (LogContext.PushProperty("CorrelationId", correlationId))
using (LogContext.PushProperty("TenantId", tenantId))
{
    logger.LogInformation("Processing order {OrderId}", orderId);
    // CorrelationId and TenantId attached to ALL log events in this scope
}

需要在日志记录器配置中添加 .Enrich.FromLogContext()。

OpenTelemetry 接收器(OTLP 导出)

无需 OpenTelemetry SDK,即可将 Serilog 事件直接导出到任意 OTLP 后端:

.WriteTo.OpenTelemetry(options =>
{
    options.Endpoint = "http://localhost:4317";
    options.Protocol = OtlpProtocol.Grpc;
    options.ResourceAttributes = new Dictionary<string, object>
    {
        ["service.name"] = "MyApp.Api",
        ["deployment.environment"] = "production"
    };
})
使用 Serilog.Expressions 进行筛选

需要 Serilog.Expressions 包。

// Exclude health check noise
.Filter.ByExcluding("RequestPath like '/health%'")

// Route errors to a separate file
.WriteTo.Conditional("@l = 'Error'",
    wt => wt.File("logs/errors-.log", rollingInterval: RollingInterval.Day))
使用 [LoggerMessage] 源生成器处理热点路径

内置于 Microsoft.Extensions.Logging.Abstractions 中——编译时生成,禁用相应级别时零内存分配。

public static partial class OrderLogs
{
    [LoggerMessage(Level = LogLevel.Information,
        Message = "Order {OrderId} created for {CustomerId}")]
    public static partial void OrderCreated(this ILogger logger, Guid orderId, Guid customerId);
}

// Usage
logger.OrderCreated(order.Id, order.CustomerId);

反模式

不要使用字符串插值
// BAD — breaks structured logging, allocates even when level is disabled
logger.LogInformation($"Order {orderId} created for {customerId}");

// GOOD — message template with named parameters
logger.LogInformation("Order {OrderId} created for {CustomerId}", orderId, customerId);
不要跳过 CloseAndFlush
// BAD — async sinks (Seq, OTLP, Elasticsearch) lose buffered events
app.Run();

// GOOD — wrap in try/finally
try { app.Run(); }
catch (Exception ex) { Log.Fatal(ex, "Unhandled exception"); }
finally { await Log.CloseAndFlushAsync(); }
不要记录敏感数据
// BAD — passwords and tokens in logs
logger.LogInformation("Login: {Email} with password {Password}", email, password);

// GOOD — never log secrets, passwords, tokens, or PII
logger.LogInformation("Login: {Email}", email);
不要不加限制地解构
// BAD — large object graphs cause memory issues and massive log entries
logger.LogInformation("Request: {@Request}", httpContext.Request);

// GOOD — configure destructuring limits
.Destructure.ToMaximumDepth(4)
.Destructure.ToMaximumStringLength(1024)
.Destructure.ToMaximumCollectionCount(10)

// BETTER — destructure to specific properties
.Destructure.ByTransforming<HttpRequest>(r => new { r.Method, r.Path })
不要使用已弃用的 Elasticsearch 接收器
// BAD — the Serilog.Sinks.Elasticsearch PACKAGE is deprecated
// <PackageReference Include="Serilog.Sinks.Elasticsearch" />
.WriteTo.Elasticsearch("http://localhost:9200")

// GOOD — same method name, but from the official Elastic.Serilog.Sinks
// package, which writes ECS-formatted documents to data streams
// <PackageReference Include="Elastic.Serilog.Sinks" />
.WriteTo.Elasticsearch([new Uri("https://elastic.example.com:9200")], opts =>
    opts.DataStream = new DataStreamName("logs", "myapp"))

决策指南

场景建议
应用程序日志记录使用 AddSerilog() 和 appsettings.json 配置 Serilog
日志存储(开发环境)Seq(免费单用户版)或 Aspire Dashboard
日志存储(生产环境)Seq、Elasticsearch(Elastic 接收器)或 OTLP 后端
请求日志记录UseSerilogRequestLogging()(替代每个请求产生的冗余日志)
作用域属性在中间件中使用 LogContext.PushProperty()
日志筛选使用 Serilog.Expressions 进行基于表达式的筛选
高性能路径[LoggerMessage] 源生成器
审计跟踪AuditTo(同步执行,异常会向上传播)
按环境设置日志级别在 appsettings 中按命名空间使用 MinimumLevel.Override
OpenTelemetry 集成Serilog.Sinks.OpenTelemetry(无 SDK 依赖)

评论

支持 Markdown