logging

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Logging & Observability

日志与可观测性

Core Principles

核心原则

  1. Structured logging with Serilog — Every log entry is a structured event with named properties, not a formatted string. This enables searching, filtering, and alerting. All setup (two-stage bootstrap,
    AddSerilog()
    , sinks, enrichers) lives in the serilog skill — that skill's
    AddSerilog()
    -over-
    UseSerilog()
    guidance is canonical.
  2. OpenTelemetry for distributed tracing — Traces connect requests across services; metrics track system health over time. Full setup lives in the opentelemetry skill.
  3. Health checks for operational readiness — Every service exposes
    /health
    endpoints for load balancers and orchestrators. Liveness and readiness are separate questions and separate endpoints.
  4. Correlation IDs for request tracing — Every request gets a unique ID that flows through all log entries and downstream service calls, so one user complaint maps to one filtered log stream.
  1. 基于Serilog的结构化日志 —— 每条日志条目都是带有命名属性的结构化事件,而非格式化字符串。这支持搜索、过滤和告警功能。所有配置(两阶段启动、
    AddSerilog()
    、输出接收器、增强器)都位于serilog Skill中——该Skill中关于
    AddSerilog()
    优先于
    UseSerilog()
    的指导为标准规范。
  2. 用于分布式追踪的OpenTelemetry —— 追踪可跨服务关联请求;指标用于随时间跟踪系统健康状态。完整配置位于opentelemetry Skill中。
  3. 用于运行就绪性的健康检查 —— 每个服务都暴露
    /health
    端点供负载均衡器和编排器使用。存活检查(liveness)和就绪检查(readiness)是两个独立的概念,对应独立的端点。
  4. 用于请求追踪的关联ID —— 每个请求都会分配一个唯一ID,该ID会贯穿所有日志条目和下游服务调用,因此用户的一个反馈可对应到一条过滤后的日志流。

Patterns

模式

How the Pieces Fit Together

组件协同方式

ConcernOwnerSkill
Structured application logsSerilog (
AddSerilog()
)
serilog
Request summary logging
UseSerilogRequestLogging()
serilog
Traces + metrics + OTLP exportOpenTelemetry SDK
opentelemetry
Health endpoints, correlation IDs, log-level strategyThis skill
logging
Wire logging first (you need logs to debug the rest), then health checks, then tracing.
关注点负责方Skill
结构化应用日志Serilog(
AddSerilog()
serilog
请求摘要日志
UseSerilogRequestLogging()
serilog
追踪 + 指标 + OTLP导出OpenTelemetry SDK
opentelemetry
健康端点、关联ID、日志级别策略本Skill
logging
优先配置日志(调试其他功能需要日志支持),然后是健康检查,最后是追踪。

Correlation IDs

关联ID

csharp
// Middleware to set correlation ID
public class CorrelationIdMiddleware(RequestDelegate next)
{
    private const string CorrelationIdHeader = "X-Correlation-Id";

    public async Task InvokeAsync(HttpContext context)
    {
        var correlationId = context.Request.Headers[CorrelationIdHeader].FirstOrDefault()
            ?? Guid.NewGuid().ToString();

        context.Items["CorrelationId"] = correlationId;
        context.Response.Headers[CorrelationIdHeader] = correlationId;

        using (LogContext.PushProperty("CorrelationId", correlationId))
        {
            await next(context);
        }
    }
}

// Program.cs — register early so every downstream log carries the ID
app.UseMiddleware<CorrelationIdMiddleware>();
Why middleware: pushing the property once at the pipeline edge attaches it to every log event in the request scope — no per-call-site plumbing. Propagate the same header on outgoing
HttpClient
calls via a
DelegatingHandler
(see the httpclient-factory skill).
csharp
// 设置关联ID的中间件
public class CorrelationIdMiddleware(RequestDelegate next)
{
    private const string CorrelationIdHeader = "X-Correlation-Id";

    public async Task InvokeAsync(HttpContext context)
    {
        var correlationId = context.Request.Headers[CorrelationIdHeader].FirstOrDefault()
            ?? Guid.NewGuid().ToString();

        context.Items["CorrelationId"] = correlationId;
        context.Response.Headers[CorrelationIdHeader] = correlationId;

        using (LogContext.PushProperty("CorrelationId", correlationId))
        {
            await next(context);
        }
    }
}

// Program.cs —— 尽早注册,让所有下游日志都携带该ID
app.UseMiddleware<CorrelationIdMiddleware>();
为何使用中间件:在管道边缘一次性推送属性,即可将其附加到请求范围内的所有日志事件——无需在每个调用点编写 plumbing 代码。通过
DelegatingHandler
在 outgoing
HttpClient
调用中传播相同的头部(详见httpclient-factory Skill)。

Health Checks

健康检查

csharp
// Program.cs
builder.Services.AddHealthChecks()
    .AddNpgSql(builder.Configuration.GetConnectionString("Default")!,
        name: "database", tags: ["ready"])
    .AddRedis(builder.Configuration.GetConnectionString("Redis")!,
        name: "redis", tags: ["ready"])
    .AddRabbitMQ(builder.Configuration.GetConnectionString("RabbitMq")!,
        name: "rabbitmq", tags: ["ready"]);

// Map endpoints
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false // No dependency checks — just "am I running?"
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready")
});
Why two endpoints: liveness failing means "restart me"; readiness failing means "stop sending traffic". Conflating them makes a slow database restart your app in a loop.
csharp
// Program.cs
builder.Services.AddHealthChecks()
    .AddNpgSql(builder.Configuration.GetConnectionString("Default")!,
        name: "database", tags: ["ready"])
    .AddRedis(builder.Configuration.GetConnectionString("Redis")!,
        name: "redis", tags: ["ready"])
    .AddRabbitMQ(builder.Configuration.GetConnectionString("RabbitMq")!,
        name: "rabbitmq", tags: ["ready"]);

// 映射端点
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false // 不检查依赖项 —— 仅判断“我是否在运行?”
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready")
});
为何设置两个端点:存活检查失败意味着“重启我”;就绪检查失败意味着“停止向我发送流量”。将两者混淆会导致数据库缓慢时应用陷入重启循环。

Log-Level Strategy

日志级别策略

LevelUse forEnvironment default
DebugDiagnostic detail, payload dumps (never PII in prod)Development only
InformationBusiness events: order placed, job completedDev + staging
WarningRecoverable anomalies: retry fired, fallback usedEverywhere — production default
ErrorFailed operations that need attentionEverywhere
Fatal/CriticalApp cannot continueEverywhere
Why Warning as the production default: Information-level request noise at scale costs real money in log storage and drowns the signals. Keep Information for genuine business events via namespace overrides (see the serilog skill's
MinimumLevel.Override
pattern).
级别使用场景默认环境
Debug诊断细节、负载转储(生产环境中绝不能包含PII)仅开发环境
Information业务事件:订单已下单、任务已完成开发 + 预发布环境
Warning可恢复的异常:触发重试、使用降级方案所有环境 —— 生产环境默认
Error需要关注的失败操作所有环境
Fatal/Critical应用无法继续运行所有环境
为何生产环境默认使用Warning级别:大规模下的Information级请求日志会占用大量日志存储成本,且会掩盖关键信号。通过命名空间覆盖仅保留真正的业务事件(详见serilog Skill的
MinimumLevel.Override
模式)。

Anti-patterns

反模式

Don't Log Sensitive Data

请勿记录敏感数据

csharp
// BAD — logging credentials
logger.LogInformation("User logged in: {Email} with password {Password}", email, password);

// GOOD — log identifiers, never secrets or PII at Information level
logger.LogInformation("User {UserId} logged in", userId);
csharp
// 错误示例 —— 记录凭证
logger.LogInformation("User logged in: {Email} with password {Password}", email, password);

// 正确示例 —— 记录标识符,绝不记录机密信息或PII(Information级别)
logger.LogInformation("User {UserId} logged in", userId);

Don't Skip Health Check Tags

请勿省略健康检查标签

csharp
// BAD — all checks run for liveness AND readiness
app.MapHealthChecks("/health");

// GOOD — separate liveness (am I running?) from readiness (can I serve traffic?)
app.MapHealthChecks("/health/live", new() { Predicate = _ => false });
app.MapHealthChecks("/health/ready", new() { Predicate = c => c.Tags.Contains("ready") });
csharp
// 错误示例 —— 所有检查同时用于存活检查和就绪检查
app.MapHealthChecks("/health");

// 正确示例 —— 将存活检查(我是否在运行?)与就绪检查(我能否处理流量?)分开
app.MapHealthChecks("/health/live", new() { Predicate = _ => false });
app.MapHealthChecks("/health/ready", new() { Predicate = c => c.Tags.Contains("ready") });

Don't Re-Implement What the Owning Skill Provides

请勿重复实现已有Skill提供的功能

csharp
// BAD — hand-rolling Serilog bootstrap here from memory
builder.Host.UseSerilog(...);  // legacy API — the serilog skill forbids this

// GOOD — load the serilog skill and use its two-stage AddSerilog() bootstrap
builder.Services.AddSerilog((services, lc) => lc.ReadFrom.Configuration(builder.Configuration)...);
csharp
// 错误示例 —— 凭记忆手动实现Serilog启动逻辑
builder.Host.UseSerilog(...);  // 旧版API —— serilog Skill禁止使用该方式

// 正确示例 —— 加载serilog Skill并使用其两阶段AddSerilog()启动逻辑
builder.Services.AddSerilog((services, lc) => lc.ReadFrom.Configuration(builder.Configuration)...);

Decision Guide

决策指南

ScenarioRecommendation
Application logging setupLoad
serilog
AddSerilog()
two-stage bootstrap
Distributed tracing / metricsLoad
opentelemetry
— OTLP exporter
Custom business metrics
IMeterFactory
+ counters/histograms (
opentelemetry
skill)
Request tracingCorrelation ID middleware (this skill)
Container health
/health/live
and
/health/ready
endpoints (this skill)
Log storageSeq (development), Elastic/Grafana/OTLP backend (production)
Log levelsDebug in dev, Information in staging, Warning default in production
场景推荐方案
应用日志配置加载
serilog
Skill —— 使用
AddSerilog()
两阶段启动
分布式追踪 / 指标加载
opentelemetry
Skill —— 使用OTLP导出器
自定义业务指标
IMeterFactory
+ 计数器/直方图(
opentelemetry
Skill)
请求追踪关联ID中间件(本Skill)
容器健康检查
/health/live
/health/ready
端点(本Skill)
日志存储Seq(开发环境)、Elastic/Grafana/OTLP后端(生产环境)
日志级别开发环境用Debug,预发布环境用Information,生产环境默认用Warning