serilog

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Serilog

Serilog

Core Principles

核心原则

  1. Two-stage initialization — Create a bootstrap logger for startup, then replace it with the full logger after DI is ready. This captures startup errors that would otherwise be lost.
  2. AddSerilog()
    over
    UseSerilog()
    — Use
    builder.Services.AddSerilog()
    (the modern API) instead of
    builder.Host.UseSerilog()
    . It integrates with DI services via
    ReadFrom.Services(services)
    .
  3. Message templates, not interpolation
    {PropertyName}
    syntax creates structured data that can be queried. String interpolation (
    $"..."
    ) breaks structure and allocates even when the log level is disabled.
  4. Configure via appsettings.json — Keep log levels, sinks, and overrides in configuration so they can change per environment without redeployment.
  1. 两阶段初始化 — 创建引导日志记录器用于启动阶段,待DI就绪后替换为完整日志记录器。这样可以捕获原本会丢失的启动错误。
  2. 优先使用
    AddSerilog()
    而非
    UseSerilog()
    — 使用
    builder.Services.AddSerilog()
    (现代API)替代
    builder.Host.UseSerilog()
    。它通过
    ReadFrom.Services(services)
    与DI服务集成。
  3. 使用消息模板而非字符串插值
    {PropertyName}
    语法可生成可查询的结构化数据。字符串插值(
    $"..."
    )会破坏结构,且即使日志级别禁用也会产生内存分配。
  4. 通过appsettings.json配置 — 将日志级别、接收器和覆盖规则保存在配置中,以便无需重新部署即可根据环境调整。

Patterns

实践模式

Two-Stage Bootstrap Setup

两阶段引导设置

csharp
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();
}
csharp
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 Configuration

appsettings.json配置

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 } }
    ]
  }
}
Override section uses namespace prefixes matched against
SourceContext
. More specific prefixes take precedence.
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
匹配,更具体的前缀优先级更高。

Request Logging Middleware

请求日志中间件

Replaces the multiple per-request log events from ASP.NET Core with a single summary event.
csharp
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");
    };
});
将ASP.NET Core中多个单请求日志事件替换为单个汇总事件。
csharp
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");
    };
});

Structured Logging and Destructuring

结构化日志与对象解构

csharp
// 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[]"
csharp
// 命名属性 — 生成可查询的结构化数据
logger.LogInformation("Order {OrderId} placed by {CustomerId} for {Total:C}",
    orderId, customerId, total);

// @ 运算符保留对象结构作为属性
logger.LogInformation("Processing {@SensorInput}", sensorInput);
// 输出: Processing {"Latitude": 25, "Longitude": 134}

// $ 运算符强制调用ToString()
logger.LogInformation("Received {$Data}", new[] { 1, 2, 3 });
// 输出: Received "System.Int32[]"

Scoped Properties with LogContext

基于LogContext的作用域属性

csharp
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
}
Requires
.Enrich.FromLogContext()
on the logger configuration.
csharp
using (LogContext.PushProperty("CorrelationId", correlationId))
using (LogContext.PushProperty("TenantId", tenantId))
{
    logger.LogInformation("Processing order {OrderId}", orderId);
    // CorrelationId和TenantId会附加到此作用域内的所有日志事件
}
需要在日志记录器配置中添加
.Enrich.FromLogContext()

OpenTelemetry Sink (OTLP Export)

OpenTelemetry接收器(OTLP导出)

Export Serilog events directly to any OTLP backend without the OpenTelemetry SDK:
csharp
.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"
    };
})
无需OpenTelemetry SDK即可直接将Serilog事件导出到任意OTLP后端:
csharp
.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 for Filtering

Serilog.Expressions用于过滤

Requires the
Serilog.Expressions
package.
csharp
// 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))
需要安装
Serilog.Expressions
包。
csharp
// 排除健康检查日志噪音
.Filter.ByExcluding("RequestPath like '/health%'")

// 将错误路由到单独文件
.WriteTo.Conditional("@l = 'Error'",
    wt => wt.File("logs/errors-.log", rollingInterval: RollingInterval.Day))

[LoggerMessage] Source Generator for Hot Paths

针对热点路径的[LoggerMessage]源代码生成器

Built into
Microsoft.Extensions.Logging.Abstractions
— compile-time generated, zero allocations when the level is disabled.
csharp
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);
内置在
Microsoft.Extensions.Logging.Abstractions
中——编译时生成代码,当日志级别禁用时零内存分配。
csharp
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);
}

// 使用方式
logger.OrderCreated(order.Id, order.CustomerId);

Anti-patterns

反模式

Don't Use String Interpolation

不要使用字符串插值

csharp
// 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);
csharp
// 错误示例 — 破坏结构化日志,即使级别禁用也会产生内存分配
logger.LogInformation($"Order {orderId} created for {customerId}");

// 正确示例 — 使用带命名参数的消息模板
logger.LogInformation("Order {OrderId} created for {CustomerId}", orderId, customerId);

Don't Skip CloseAndFlush

不要跳过CloseAndFlush

csharp
// 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(); }
csharp
// 错误示例 — 异步接收器(Seq、OTLP、Elasticsearch)会丢失缓冲事件
app.Run();

// 正确示例 — 用try/finally包裹
try { app.Run(); }
catch (Exception ex) { Log.Fatal(ex, "Unhandled exception"); }
finally { await Log.CloseAndFlushAsync(); }

Don't Log Sensitive Data

不要记录敏感数据

csharp
// 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);
csharp
// 错误示例 — 日志中包含密码和令牌
logger.LogInformation("Login: {Email} with password {Password}", email, password);

// 正确示例 — 绝不记录密钥、密码、令牌或个人身份信息(PII)
logger.LogInformation("Login: {Email}", email);

Don't Destructure Without Limits

不要无限制地解构对象

csharp
// 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 })
csharp
// 错误示例 — 大型对象图会导致内存问题和超大日志条目
logger.LogInformation("Request: {@Request}", httpContext.Request);

// 正确示例 — 配置解构限制
.Destructure.ToMaximumDepth(4)
.Destructure.ToMaximumStringLength(1024)
.Destructure.ToMaximumCollectionCount(10)

// 更优示例 — 仅解构特定属性
.Destructure.ByTransforming<HttpRequest>(r => new { r.Method, r.Path })

Don't Use the Deprecated Elasticsearch Sink

不要使用已弃用的Elasticsearch接收器

csharp
// 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"))
csharp
// 错误示例 — Serilog.Sinks.Elasticsearch包已弃用
// <PackageReference Include="Serilog.Sinks.Elasticsearch" />
.WriteTo.Elasticsearch("http://localhost:9200")

// 正确示例 — 方法名相同,但来自官方Elastic.Serilog.Sinks
// 包,可将ECS格式文档写入数据流
// <PackageReference Include="Elastic.Serilog.Sinks" />
.WriteTo.Elasticsearch([new Uri("https://elastic.example.com:9200")], opts =>
    opts.DataStream = new DataStreamName("logs", "myapp"))

Decision Guide

决策指南

ScenarioRecommendation
Application loggingSerilog with
AddSerilog()
and appsettings.json
Log storage (development)Seq (free single-user) or Aspire Dashboard
Log storage (production)Seq, Elasticsearch (Elastic sink), or OTLP backend
Request logging
UseSerilogRequestLogging()
(replaces per-request noise)
Scoped properties
LogContext.PushProperty()
in middleware
Log filtering
Serilog.Expressions
for expression-based filtering
High-performance paths
[LoggerMessage]
source generator
Audit trails
AuditTo
(synchronous, exceptions propagate)
Log levels by environment
MinimumLevel.Override
per namespace in appsettings
OpenTelemetry integration
Serilog.Sinks.OpenTelemetry
(no SDK dependency)
场景推荐方案
应用日志使用Serilog +
AddSerilog()
+ appsettings.json
日志存储(开发环境)Seq(免费单用户版)或Aspire Dashboard
日志存储(生产环境)Seq、Elasticsearch(Elastic接收器)或OTLP后端
请求日志
UseSerilogRequestLogging()
(替代单请求冗余日志)
作用域属性在中间件中使用
LogContext.PushProperty()
日志过滤使用
Serilog.Expressions
实现基于表达式的过滤
高性能路径使用
[LoggerMessage]
源代码生成器
审计追踪使用
AuditTo
(同步执行,异常会向上传播)
按环境设置日志级别在appsettings中按命名空间配置
MinimumLevel.Override
OpenTelemetry集成使用
Serilog.Sinks.OpenTelemetry
(无需依赖SDK)