You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何统一在Serilog推送上下文添加自定义数据适配不同日志源?

问题描述

我在为Web API配置日志时遇到一个问题:需要在日志中记录自定义的ChannelId字段,但当前不同的SourceContext必须用不同的方式推送数据:

  • 当SourceContext为Serilog.AspNetCore.RequestLoggingMiddleware时,需要用_diagnosticContext.Set("ChannelId", request.Channel)
  • 通用日志或HttpClient日志场景,需要用LogContext.PushProperty("ChannelId", request.Channel)
  • 当SourceContext为Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware时,得先把值存到HttpContext.Items,再通过拦截器读取

现有日志配置如下:

Program.cs 初始化代码

Log.Logger = new Serilog.LoggerConfiguration()
            .CreateBootstrapLogger();

服务注册阶段

services.AddHttpContextAccessor();

services.AddHttpLogging(o =>
{
    o.CombineLogs = true;
    o.LoggingFields = HttpLoggingFields.All;   
    o.ResponseHeaders.Remove("Content-Type");
    o.ResponseHeaders.Remove("Accept");
    o.RequestHeaders.Remove("Content-Type");
    o.RequestHeaders.Remove("Accept");
    o.RequestBodyLogLimit = 8096;
    o.ResponseBodyLogLimit = 8096;
});

services.AddHttpLoggingInterceptor<SampleHttpLoggingInterceptor>();

var columnOptions = new ColumnOptions
{
    AdditionalDataColumns = new List<DataColumn>
    {
        new DataColumn("CorrelationId", typeof(string)),
        new DataColumn("TraceId", typeof(string)),
        new DataColumn("SpanId", typeof(string)),
        new DataColumn("ChannelId", typeof(string)),
        new DataColumn("RequestPath", typeof(string)),
        new DataColumn("Duration", typeof(decimal)),
        new DataColumn("SourceContext", typeof(string)),
    }
};

columnOptions.Store.Remove(StandardColumn.Properties);

services.AddSerilog((services, lc) => lc
    .ReadFrom.Configuration(config)
    .ReadFrom.Services(services)

    .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Warning)
    .MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning)
    .MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning)
    .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning)
    .MinimumLevel.Override("System.Net.Http.HttpClient.X", LogEventLevel.Warning)
    .MinimumLevel.Override("Polly", LogEventLevel.Warning)

    .Enrich.FromLogContext()
    .Enrich.WithProperty("ChannelId", string.Empty)
    .Enrich.WithProperty("Duration", string.Empty)
    .Enrich.WithClientIp()
    .Enrich.WithCorrelationId("CorrelationIdHeader", addValueIfHeaderAbsence: true)                
    .Enrich.WithSpan()

    .WriteTo.Conditional(
        _ => isDeveloment,
            wt => wt.Console(theme: AnsiConsoleTheme.Code          
        )
    )
    
    .WriteTo.Oracle(cfg =>
        cfg.WithSettings(
                "User Id=...", 
                tableSpaceAndTableName: "LogTable",
                tableSpaceAndFunctionName: string.Empty, columnOptions: columnOptions                            
            )
            .UseBurstBatch()                        
            .CreateSink(), 
            restrictedToMinimumLevel: LogEventLevel.Information
        )
     );

中间件配置

app.UseHttpLogging();

app.UseSerilogRequestLogging(options =>
{
       options.IncludeQueryInRequestPath = true;
});

当前为了填充ChannelId,我需要在控制器动作中写重复代码,还得用拦截器处理微软的HttpLoggingMiddleware,想找一种统一的方式来推送自定义数据,最好能去掉拦截器。

统一解决方案

方案1:全局请求中间件 + LogContext(推荐)

通过自定义中间件,在请求开始时就将ChannelId推入LogContext,这样所有基于Serilog的日志(包括RequestLogging、普通日志、HttpClient日志)都会自动带上这个属性;同时针对微软的HttpLoggingMiddleware,通过自定义enricher从HttpContext读取该值,避免使用拦截器。

步骤1:创建全局中间件

public class ChannelIdLoggingMiddleware
{
    private readonly RequestDelegate _next;

    public ChannelIdLoggingMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 从请求中获取ChannelId,示例从Header读取,可替换为你的业务逻辑
        var channelId = context.Request.Headers.TryGetValue("X-Channel-Id", out var value) 
            ? value.ToString() 
            : string.Empty;

        // 推入LogContext,作用域覆盖整个请求生命周期
        using (LogContext.PushProperty("ChannelId", channelId))
        {
            // 将值存入HttpContext,供微软HttpLoggingMiddleware读取
            context.Items["ChannelId"] = channelId;
            await _next(context);
        }
    }
}

步骤2:注册中间件(放在UseRouting之前)

app.UseMiddleware<ChannelIdLoggingMiddleware>();

app.UseRouting();
// 其他中间件...
app.UseHttpLogging();
app.UseSerilogRequestLogging(options =>
{
    options.IncludeQueryInRequestPath = true;
});

步骤3:自定义Enricher处理微软HttpLogging日志

创建enricher从HttpContext读取ChannelId并注入日志事件:

public class ChannelIdEnricher : ILogEventEnricher
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ChannelIdEnricher(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
    {
        var httpContext = _httpContextAccessor.HttpContext;
        if (httpContext?.Items.TryGetValue("ChannelId", out var channelId) == true)
        {
            logEvent.AddOrUpdateProperty(propertyFactory.CreateProperty("ChannelId", channelId.ToString()));
        }
    }
}

步骤4:在Serilog配置中添加该Enricher

修改Serilog注册代码,加入自定义enricher:

services.AddSerilog((services, lc) => lc
    // ... 其他原有配置
    .Enrich.FromLogContext()
    .Enrich.With<ChannelIdEnricher>() // 添加此行
    .Enrich.WithProperty("ChannelId", string.Empty)
    // ... 其他原有配置
);

此方案下,所有日志场景都会自动带上ChannelId,无需在控制器中写重复代码,也不需要拦截器。


方案2:完全替换微软HttpLoggingMiddleware,只用Serilog RequestLogging

如果不需要微软HttpLoggingMiddleware的特定功能,可直接移除它,统一用Serilog的RequestLogging处理请求日志,这样所有请求日志都归Serilog管理,仅需维护一套属性推送逻辑。

步骤1:移除微软HttpLogging相关配置

删除以下代码:

services.AddHttpLogging(o => { ... });
services.AddHttpLoggingInterceptor<SampleHttpLoggingInterceptor>();
app.UseHttpLogging();

步骤2:增强Serilog RequestLogging

在Serilog RequestLogging中直接注入ChannelId:

app.UseSerilogRequestLogging(options =>
{
    options.IncludeQueryInRequestPath = true;
    options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
    {
        var channelId = httpContext.Request.Headers["X-Channel-Id"].ToString();
        diagnosticContext.Set("ChannelId", channelId);
    };
});

这种方式更简洁,减少日志源数量,所有请求日志通过Serilog统一处理。


方案3:Serilog动态属性注入(适合简单场景)

无需编写中间件,直接在Serilog配置中通过工厂方法从HttpContextAccessor动态获取ChannelId:

services.AddSerilog((services, lc) => lc
    // ... 其他原有配置
    .Enrich.FromLogContext()
    .Enrich.WithProperty("ChannelId", () => 
    {
        var httpContextAccessor = services.GetRequiredService<IHttpContextAccessor>();
        var httpContext = httpContextAccessor.HttpContext;
        if (httpContext == null) return string.Empty;
        return httpContext.Request.Headers.TryGetValue("X-Channel-Id", out var value) 
            ? value.ToString() 
            : string.Empty;
    })
    // ... 其他原有配置
);

注意:非请求场景下HttpContext可能为null,需做空值处理。


内容的提问来源于stack exchange,提问作者mehmetilker

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 19:52:32