mcpField
D
dotnet-structured-logging

dotnet-structured-logging

github.com/wshaddix云与 DevOps8692 次安装35 周前更新
代码仓库

关于 dotnet-structured-logging

dotnet-structured-logging 面向设计和运维分布式日志管道的 .NET 团队,侧重聚合、结构化查询、日志量控制、PII 清理和跨服务关联,而非日志发射配置。

主要功能

  1. 01ELK、Seq、Loki 和 Azure Monitor 模式
  2. 02结构化查询示例
  3. 03采样与日志量控制
  4. 04PII 遮盖与移除建议
  5. 05跨服务关联实践

适用场景

  1. 01选择日志聚合架构
  2. 02在结构化日志中查找服务错误
  3. 03降低高吞吐系统的日志摄入量
  4. 04关联跨服务与队列的事件

安装

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

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-structured-logging

安装后在 Claude Code 中输入/dotnet-structured-logging确认已加载。

详细介绍

dotnet-structured-logging

面向 .NET 分布式系统的日志管道设计与运维。涵盖日志聚合架构(ELK、Seq、Grafana Loki)、各平台的结构化查询模式、日志采样与日志量管理策略、PII 清理与解构策略,以及超越单个服务日志作用域的跨服务关联。本技能关注日志发出_之后_的事项——管道、查询与运维层。

不在范围内: 日志发出机制(Serilog/NLog/MEL 配置、源生成的 LoggerMessage、丰富器、单服务日志作用域、接收器注册、OTel 日志导出)——请参阅 [skill:dotnet-observability]。应用程序配置与选项模式——请参阅 [skill:dotnet-csharp-configuration]。分布式追踪设置与追踪上下文传播——请参阅 [skill:dotnet-observability]。

交叉引用:[skill:dotnet-observability]:日志发出、Serilog/MEL 配置和 OpenTelemetry 日志导出;[skill:dotnet-csharp-configuration]:日志管道设置中使用的 appsettings.json 配置模式。


日志聚合架构

架构选项
平台摄取存储查询最适用于
ELK(Elasticsearch、Logstash、Kibana)Logstash / FilebeatElasticsearchKibana 中的 KQL大规模场景、灵活的架构、全文搜索
SeqHTTP API / Serilog 接收器内置Seq 信号表达式原生支持 .NET、方便开发者使用、结构化查询
Grafana LokiPromtail / OTel CollectorLoki(基于标签索引)LogQL成本效益高、Grafana 生态系统、基于标签的查询
Azure MonitorOTel Collector / Application Insights SDKLog Analytics 工作区KQL(Kusto)原生支持 Azure、集成告警、成本管理
推荐的管道模式

模式 1:将 OTel Collector 用作中央路由器

App (OTLP) --> OTel Collector --> Elasticsearch / Loki / Azure Monitor
                  |
                  +--> Sampling / filtering / PII scrub

OpenTelemetry Collector 充当与供应商无关的日志路由器。应用程序通过 OTLP 发出日志;collector 负责过滤、采样、丰富日志,并将其路由到一个或多个后端。这样可让应用程序与后端选择解耦。

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: "0.0.0.0:4317"
      http:
        endpoint: "0.0.0.0:4318"

processors:
  batch:
    timeout: 5s
    send_batch_size: 1024
  filter:
    logs:
      exclude:
        match_type: strict
        bodies:
          - "Health check endpoint hit"

exporters:
  elasticsearch:
    endpoints: ["https://es-cluster:9200"]
    logs_index: "app-logs"
  loki:
    endpoint: "http://loki:3100/loki/api/v1/push"

service:
  pipelines:
    logs:
      receivers: [otlp]
      processors: [batch, filter]
      exporters: [elasticsearch, loki]

模式 2:直接接收器(较小规模的部署)

App (Serilog) --> Seq / Elasticsearch sink

对于较小型系统或开发环境,Serilog 接收器会将日志直接写入聚合平台。这样无需使用 OTel Collector,但会使应用程序与后端耦合。

.NET 应用程序 OTLP 配置

有关 .NET 应用程序侧的 OTLP 日志导出配置(builder.Logging.AddOpenTelemetry()),请参阅 [skill:dotnet-observability]。OTLP 端点通过环境变量(OTEL_EXPORTER_OTLP_ENDPOINT)配置,使应用程序代码不依赖特定后端。


结构化查询模式

结构化日志将每个属性存储为可查询字段。各平台的查询语法有所不同,但概念一致:按属性名称、值、严重性和时间范围筛选。

Kibana KQL(Elasticsearch / ELK)
# Find errors for a specific order
level: "Error" AND OrderId: "abc-123"

# Find slow operations (custom Duration property)
Duration > 5000 AND ServiceName: "order-api"

# Wildcard on message template
message: "Failed to process*" AND NOT level: "Debug"

# Time-scoped with correlation
TraceId: "0af7651916cd43dd8448eb211c80319c" AND @timestamp >= "2025-01-15T10:00:00"
Seq 信号表达式
# Find errors for a specific order
@Level = 'Error' and OrderId = 'abc-123'

# Find slow operations
Duration > 5000 and Application = 'order-api'

# Free-text search combined with structured filter
@Message like '%timeout%' and @Level in ['Warning', 'Error']

# Correlation across services
TraceId = '0af7651916cd43dd8448eb211c80319c'

Seq 信号是可触发告警的已保存查询。为重复出现的模式定义信号(例如,“每分钟支付失败次数 > 10”),并关联通知渠道。

Grafana LogQL(Loki)
# Filter by labels then regex on log line
{service_name="order-api"} |= "Error" | json | OrderId="abc-123"

# Structured field extraction and filtering
{service_name="order-api"} | json | Duration > 5000

# Count errors per service over time (for dashboards)
sum(rate({service_name=~".+"} |= "Error" [5m])) by (service_name)
Azure Monitor KQL(Kusto)
// Find errors for a specific order
traces
| where severityLevel >= 3
| where customDimensions.OrderId == "abc-123"
| order by timestamp desc

// Slow operations
traces
| where toint(customDimensions.Duration) > 5000
| where cloud_RoleName == "order-api"

// Cross-service correlation
union traces, exceptions
| where operation_Id == "0af7651916cd43dd8448eb211c80319c"
| order by timestamp asc

日志采样与日志量管理

高吞吐量系统每分钟可能生成数百万条日志事件。如果不进行采样,存储成本会迅速增加,查询性能也会迅速下降。

采样策略
策略工作方式适用场景
基于头部在处理之前决定是否采样每个请求保持一致;实现简单
基于尾部在处理之后决定是否采样保留所有错误和慢请求,丢弃常规日志
基于级别按严重性采样始终保留 Warning 及以上级别,对 Debug/Info 采样
动态根据日志量调整采样率无需更改配置即可应对流量激增
OTel Collector 日志筛选

OTel Collector 中的 filter 处理器会在管道级别丢弃日志记录,使其不会到达导出器。使用它排除嘈杂的低严重性日志并减少存储量。

注意:tail_sampling 处理器处理的是跟踪(跨度),而不是日志。要管理日志量,请改用 filter 和 transform 处理器。

processors:
  filter:
    logs:
      exclude:
        match_type: regexp
        # Drop Debug and Trace logs at the collector level
        severity_texts: ["DEBUG", "TRACE"]
      exclude:
        match_type: strict
        # Exclude health check noise
        bodies:
          - "Health check endpoint hit"
  transform:
    log_statements:
      - context: log
        conditions:
          # Keep all Warning+ logs unconditionally
          - severity_number >= SEVERITY_NUMBER_WARN
        statements: []
使用 Serilog 在应用程序级别采样
// Serilog.Expressions package for conditional log filtering
builder.Host.UseSerilog((context, loggerConfiguration) =>
{
    loggerConfiguration
        .ReadFrom.Configuration(context.Configuration)
        // Drop health check logs entirely
        .Filter.ByExcluding("RequestPath = '/health/ready'")
        // Sample Debug logs at 10%
        .Filter.ByExcluding(
            "@Level = 'Debug' and Hash(@i) % 10 != 0");
});

关键软件包:

<PackageReference Include="Serilog.Expressions" Version="5.*" />
日志量管理清单
  1. 按索引/数据流设置保留策略(例如,Info 保留 30 天,Error 保留 90 天)
  2. 使用日志级别筛选,在源头抑制嘈杂的框架类别
  3. 从请求日志中排除运行状况检查端点
  4. 应用索引生命周期管理(Elasticsearch 中的 ILM,Loki 中的保留策略)
  5. 监控摄取速率,并针对存储成本设置预算提醒

PII 清理与解构策略

生产环境中的日志不得包含个人身份信息(PII)。GDPR、HIPAA 和 SOC 2 要求对敏感数据进行屏蔽,或将其排除在日志存储之外。

使用 Enricher 按属性屏蔽
// Enricher that masks known-sensitive properties on every log event
public sealed class PiiMaskingEnricher : ILogEventEnricher
{
    private static readonly HashSet<string> s_sensitiveKeys = new(
        StringComparer.OrdinalIgnoreCase)
    {
        "Email", "PhoneNumber", "IpAddress",
        "CreditCard", "SSN", "Password"
    };

    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory factory)
    {
        var propertiesToMask = logEvent.Properties
            .Where(p => s_sensitiveKeys.Contains(p.Key))
            .Select(p => p.Key)
            .ToList();

        foreach (var key in propertiesToMask)
        {
            logEvent.AddOrUpdateProperty(
                factory.CreateProperty(key, "***REDACTED***"));
        }
    }
}

// Registration
loggerConfiguration.Enrich.With<PiiMaskingEnricher>();
OTel Collector 属性处理
processors:
  attributes:
    actions:
      # Mask email addresses using regex
      - key: user.email
        action: update
        value: "***@redacted.com"
      # Remove sensitive attributes entirely
      - key: http.request.header.authorization
        action: delete
      - key: user.password
        action: delete
PII 清理清单
  1. 识别 PII 字段 -- 电子邮件、电话、IP、SSN、信用卡、身份验证令牌、Cookie
  2. 尽早应用处理 -- 使用 enricher 或 OTel 处理器,而不是在查询时处理
  3. 审核日志模板 -- 确保结构化日志模板不会将 PII 记录为命名属性
  4. 与合规团队共同测试 -- 根据法规要求验证清理规则
  5. 对确实需要 PII 的审计日志使用单独的保留策略(并启用静态加密)

跨服务关联

在分布式系统中,单个用户请求可能会经过多个服务。关联可用于跨所有服务跟踪请求,并还原完整的事件时间线。

W3C Trace Context 关联

主要的关联机制是 W3C traceparent 标头。配置 OpenTelemetry 检测后,它会通过 HttpClient 自动传播(参见 [skill:dotnet-observability])。跟踪请求中发出的所有日志事件都包含 TraceId 和 SpanId 属性。

// Query all logs for a distributed operation across services
// In Seq:
TraceId = '0af7651916cd43dd8448eb211c80319c'

// In Kibana:
TraceId: "0af7651916cd43dd8448eb211c80319c"

// In Azure Monitor:
traces | where operation_Id == "0af7651916cd43dd8448eb211c80319c"
自定义关联 ID

当跟踪上下文不足以满足需求时(例如,跨越消息队列的异步工作流、批处理作业或外部系统回调),请添加自定义关联 ID:

// Propagate a business correlation ID through Serilog LogContext
public sealed class CorrelationIdMiddleware(RequestDelegate next)
{
    private const string CorrelationHeader = "X-Correlation-Id";

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

        context.Response.Headers[CorrelationHeader] = correlationId;

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

// Registration
app.UseMiddleware<CorrelationIdMiddleware>();
消息队列关联

对于异步消息传递(Azure Service Bus、RabbitMQ),通过消息属性传播关联信息:

// Producer -- attach correlation to message
var message = new ServiceBusMessage(payload)
{
    CorrelationId = Activity.Current?.TraceId.ToString()
        ?? Guid.NewGuid().ToString("N"),
    ApplicationProperties =
    {
        ["BusinessCorrelationId"] = orderId.ToString()
    }
};

// Consumer -- restore correlation in log scope
processor.ProcessMessageAsync += async args =>
{
    using var scope = logger.BeginScope(new Dictionary<string, object>
    {
        ["CorrelationId"] = args.Message.CorrelationId,
        ["BusinessCorrelationId"] =
            args.Message.ApplicationProperties["BusinessCorrelationId"]
    });

    logger.LogInformation("Processing message {MessageId}", args.Message.MessageId);
    await ProcessAsync(args.Message, args.CancellationToken);
};
关联最佳实践
实践理由
始终在日志输出中包含 TraceId支持在可观测性平台中关联日志与跟踪
在业务流程中使用 CorrelationId在跟踪上下文重置的异步间隔中保持关联
将关联 ID 存储在消息标头中支持通过队列进行端到端跟踪
在错误响应中包含关联信息支持团队可据此查找完整跟踪
使用 Serilog LogContext.PushProperty 或 MEL BeginScope自动附加到作用域内的所有日志事件

Agent 注意事项

  1. 不要混淆日志生成与日志管道——此技能涵盖管道、查询和运维。有关 Serilog/MEL 配置、增强器、接收器注册和源生成的 LoggerMessage,请参阅 [skill:dotnet-observability]。
  2. 不要在生产日志中存储个人身份信息(PII)——在管道层应用脱敏增强器或 OTel 处理器规则。存储后再脱敏不足以满足合规要求。
  3. 高吞吐量服务不要跳过日志采样——每秒处理数千个请求的服务若不对 Debug/Info 日志进行采样,会导致存储过载并降低查询性能。使用基于尾部的采样,保留所有错误和慢请求。
  4. 不要在应用程序代码中硬编码聚合平台端点——使用环境变量(OTEL_EXPORTER_OTLP_ENDPOINT)或配置,以便同一映像能在不同环境中使用。
  5. 不要仅依赖 TraceId 进行业务关联——跟踪上下文会在异步边界(消息队列、计划任务)处重置。对于跨越这些边界的工作流,请添加显式的业务关联 ID。
  6. 不要忘记保留策略——没有保留策略的日志会无限累积,增加成本并减慢查询速度。按严重性设置保留期限(例如,Info 保留 30 天,Error 保留 90 天)。

参考资料

评论

支持 Markdown