如何统一在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

