关于 dotnet-structured-logging
dotnet-structured-logging 面向设计和运维分布式日志管道的 .NET 团队,侧重聚合、结构化查询、日志量控制、PII 清理和跨服务关联,而非日志发射配置。
主要功能
- 01ELK、Seq、Loki 和 Azure Monitor 模式
- 02结构化查询示例
- 03采样与日志量控制
- 04PII 遮盖与移除建议
- 05跨服务关联实践
适用场景
- 01选择日志聚合架构
- 02在结构化日志中查找服务错误
- 03降低高吞吐系统的日志摄入量
- 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 / Filebeat | Elasticsearch | Kibana 中的 KQL | 大规模场景、灵活的架构、全文搜索 |
| Seq | HTTP API / Serilog 接收器 | 内置 | Seq 信号表达式 | 原生支持 .NET、方便开发者使用、结构化查询 |
| Grafana Loki | Promtail / OTel Collector | Loki(基于标签索引) | LogQL | 成本效益高、Grafana 生态系统、基于标签的查询 |
| Azure Monitor | OTel Collector / Application Insights SDK | Log 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.*" />
日志量管理清单
- 按索引/数据流设置保留策略(例如,Info 保留 30 天,Error 保留 90 天)
- 使用日志级别筛选,在源头抑制嘈杂的框架类别
- 从请求日志中排除运行状况检查端点
- 应用索引生命周期管理(Elasticsearch 中的 ILM,Loki 中的保留策略)
- 监控摄取速率,并针对存储成本设置预算提醒
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 清理清单
- 识别 PII 字段 -- 电子邮件、电话、IP、SSN、信用卡、身份验证令牌、Cookie
- 尽早应用处理 -- 使用 enricher 或 OTel 处理器,而不是在查询时处理
- 审核日志模板 -- 确保结构化日志模板不会将 PII 记录为命名属性
- 与合规团队共同测试 -- 根据法规要求验证清理规则
- 对确实需要 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 注意事项
- 不要混淆日志生成与日志管道——此技能涵盖管道、查询和运维。有关 Serilog/MEL 配置、增强器、接收器注册和源生成的 LoggerMessage,请参阅 [skill:dotnet-observability]。
- 不要在生产日志中存储个人身份信息(PII)——在管道层应用脱敏增强器或 OTel 处理器规则。存储后再脱敏不足以满足合规要求。
- 高吞吐量服务不要跳过日志采样——每秒处理数千个请求的服务若不对 Debug/Info 日志进行采样,会导致存储过载并降低查询性能。使用基于尾部的采样,保留所有错误和慢请求。
- 不要在应用程序代码中硬编码聚合平台端点——使用环境变量(
OTEL_EXPORTER_OTLP_ENDPOINT)或配置,以便同一映像能在不同环境中使用。 - 不要仅依赖 TraceId 进行业务关联——跟踪上下文会在异步边界(消息队列、计划任务)处重置。对于跨越这些边界的工作流,请添加显式的业务关联 ID。
- 不要忘记保留策略——没有保留策略的日志会无限累积,增加成本并减慢查询速度。按严重性设置保留期限(例如,Info 保留 30 天,Error 保留 90 天)。