日志追踪革命!.NET 中关联ID的终极实现方案

作者:微信公众号:【架构师老卢】
7-20 19:5
13

场景:

假设你有一个执行关键任务的应用程序,并添加了日志记录:

internal sealed class Handler(ILogger<Handler> logger) : IHandler
{
    public async Task InvokeAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation("处理器 {HandlerName} 开始工作", nameof(Handler));
        
        // 执行异步操作
        await Task.Delay(TimeSpan.FromMicroseconds(2), cancellationToken);
        
        logger.LogInformation("处理器 {HandlerName} 完成工作", nameof(Handler));
    }
}

标准日志输出

info: LevelUpLogs.Handler[0]
      处理器 Handler 开始工作
info: LevelUpLogs.Handler[0]
      处理器 Handler 完成工作

核心问题
当需要追踪特定请求时,现有日志缺乏关联标识,无法跨条目跟踪完整链路。


✅ 解决方案 1:启用日志范围

appsettings.json 开启 IncludeScopes

{
  "Logging": {
    "Console": {
      "IncludeScopes": true // <-- 关键配置
    }
  }
}

优化后日志

info: LevelUpLogs.Handler[0]
      => SpanId:64b2903e475a475b, TraceId:cfccdb7d8f91e4a3468abd1e870cd469...
      处理器 Handler 开始工作
info: LevelUpLogs.Handler[0]
      => SpanId:64b2903e475a475b, TraceId:cfccdb7d8f91e4a3468abd1e870cd469...
      处理器 Handler 完成工作

优势:通过 SpanId/TraceId 实现请求链路追踪


🚀 解决方案 2:自定义关联ID系统

步骤 1:创建关联ID提供器

internal interface ICorrelationIdProvider
{
    string? CorrelationId { get; set; }
}

internal sealed class CorrelationIdProvider : ICorrelationIdProvider
{
    public string? CorrelationId { get; set; }
}

// 注册为作用域服务
builder.Services.AddScoped<ICorrelationIdProvider, CorrelationIdProvider>();

步骤 2:创建中间件获取ID

internal sealed class CorrelationIdMiddleware(RequestDelegate next)
{
    public async Task Invoke(HttpContext context, ICorrelationIdProvider provider)
    {
        const string HeaderName = "X-Correlation-ID";
        
        // 从请求头获取或生成新ID
        context.Request.Headers.TryGetValue(HeaderName, out var headerValue);
        provider.CorrelationId = !string.IsNullOrEmpty(headerValue) 
            ? headerValue.ToString() 
            : Guid.NewGuid().ToString();
        
        await next(context);
    }
}

步骤 3:在处理器中使用

internal sealed class Handler(
    ILogger<Handler> logger,
    ICorrelationIdProvider provider) : IHandler
{
    public async Task InvokeAsync(CancellationToken ct)
    {
        // 创建日志范围
        using (logger.BeginScope(provider.CorrelationId!)) 
        {
            logger.LogInformation("处理器 {HandlerName} 开始工作", nameof(Handler));
            await Task.Delay(TimeSpan.FromMicroseconds(2), ct);
            logger.LogInformation("处理器 {HandlerName} 完成工作", nameof(Handler));
        }
    }
}

日志输出

info: LevelUpLogs.Handler[0]
      => ... => 255fb6aa-4cae-4fc4-876a-35e0c059bb8a
      处理器 Handler 开始工作
info: LevelUpLogs.Handler[0]
      => ... => 255fb6aa-4cae-4fc4-876a-35e0c059bb8a
      处理器 Handler 完成工作

⚡ 终极方案:高性能日志优化

步骤 1:关闭范围包含

{
  "Logging": {
    "Console": {
      "IncludeScopes": false // 禁用范围输出
    }
  }
}

步骤 2:使用 LoggerMessage.Define

internal sealed class Handler(ILogger<Handler> logger, ICorrelationIdProvider provider) : IHandler
{
    private const int _eventId = 100;
    
    // 预编译日志模板
    private static readonly Action<ILogger, string, string, Exception?> _logStart = 
        LoggerMessage.Define<string, string>(LogLevel.Information,
            new EventId(_eventId, nameof(LogStart)),
            "[{CorrelationId}] 处理器 {HandlerName} 开始工作");
    
    private static readonly Action<ILogger, string, string, Exception?> _logEnd = 
        LoggerMessage.Define<string, string>(LogLevel.Information,
            new EventId(_eventId, nameof(LogEnd)),
            "[{CorrelationId}] 处理器 {HandlerName} 完成工作");
    
    public async Task InvokeAsync(CancellationToken ct)
    {
        // 高性能日志调用
        _logStart(logger, provider.CorrelationId!, nameof(Handler), null);
        await Task.Delay(TimeSpan.FromMicroseconds(2), ct);
        _logEnd(logger, provider.CorrelationId!, nameof(Handler), null);
    }
}

优化后日志

info: LevelUpLogs.Handler[100]
      [43f26b8e-e9a8-411b-9865-f88ad667d59c] 处理器 Handler 开始工作
info: LevelUpLogs.Handler[100]
      [43f26b8e-e9a8-411b-9865-f88ad667d59c] 处理器 Handler 完成工作

三大优势

  1. 关联ID直接嵌入日志消息
  2. EventId精准定位代码位置
  3. 零分配开销的高性能日志

架构价值

| 方案 | 追踪能力 | 性能 | 可读性 |
|-------------------|-------------|---------|-----------|
| 基础日志 | ❌ | ⭐⭐⭐⭐ | ⭐⭐ |
| 日志范围 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐ |
| 关联ID+日志范围 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| 关联ID+预编译 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |


💬 开发者问答

Q:何时必须使用关联ID?
A:当你的应用涉及:

  • 微服务间调用链追踪
  • 支付/订单等关键业务流水
  • 需要分钟级定位问题的生产系统

Q:两种方案如何选择?

  • 快速实现:选择日志范围方案(方案1)
  • 高性能场景:采用预编译日志+关联ID(终极方案)
相关留言评论
昵称:
邮箱:
阅读排行