如何在.NET Serilog中将接口执行耗时与服务响应合并到同一条日志?
如何将Serilog的接口响应与执行耗时合并到同一条JSON日志中
当然可以实现,下面提供几种实用方案,基于Serilog生态和ASP.NET Core原生机制:
方案一:利用IDiagnosticContext传递数据(推荐)
Serilog.AspNetCore的默认请求日志通过IDiagnosticContext收集上下文信息,我们可以直接把服务响应数据存入这个上下文,让请求日志自动包含该字段。
修改你的PageModel代码:
public class PrivacyModel : PageModel { private readonly ILogger<PrivacyModel> _logger; private readonly MyService _service; private readonly IDiagnosticContext _diagnosticContext; public PrivacyModel(ILogger<PrivacyModel> logger, MyService service, IDiagnosticContext diagnosticContext) { _logger = logger; _service = service; _diagnosticContext = diagnosticContext; } public void OnGet(string question) { var answer = _service.Execute(question); // 将响应数据存入诊断上下文,会被后续的请求日志自动包含 _diagnosticContext.Set("ServiceResponse", answer); // 若不需要单独的响应日志行,可以删除下面这行 // _logger.LogInformation("{@a}", answer); } }
配置后,原来的请求日志行(比如HTTP GET / responded 200 in 95.0581 ms)会自动扩展为包含ServiceResponse字段的JSON日志,同时保留耗时信息,完美满足需求。
方案二:自定义请求日志中间件
如果需要更灵活的控制(比如捕获原始响应体、自定义日志格式),可以自己实现中间件,在请求结束时统一记录合并后的日志。
1. 编写中间件代码
public class CustomRequestLoggingMiddleware { private readonly RequestDelegate _next; private readonly ILogger<CustomRequestLoggingMiddleware> _logger; public CustomRequestLoggingMiddleware(RequestDelegate next, ILogger<CustomRequestLoggingMiddleware> logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { var startTime = DateTimeOffset.UtcNow; var originalResponseBody = context.Response.Body; // 替换响应流以捕获响应内容 using var responseStream = new MemoryStream(); context.Response.Body = responseStream; try { await _next(context); // 读取并重置响应流 responseStream.Seek(0, SeekOrigin.Begin); var responseBody = await new StreamReader(responseStream).ReadToEndAsync(); var elapsedMs = (DateTimeOffset.UtcNow - startTime).TotalMilliseconds; // 记录合并后的日志 _logger.LogInformation( "HTTP {Method} {Path} responded {StatusCode} in {ElapsedMs:F4} ms. ServiceResponse: {@ResponseBody}", context.Request.Method, context.Request.Path, context.Response.StatusCode, elapsedMs, responseBody); } finally { // 将捕获的响应内容写回原始响应流 responseStream.Seek(0, SeekOrigin.Begin); await responseStream.CopyToAsync(originalResponseBody); context.Response.Body = originalResponseBody; } } } // 注册中间件的扩展方法 public static class CustomRequestLoggingMiddlewareExtensions { public static IApplicationBuilder UseCustomRequestLogging(this IApplicationBuilder builder) { return builder.UseMiddleware<CustomRequestLoggingMiddleware>(); } }
2. 注册中间件
在Program.cs中,替换默认的Serilog请求日志中间件,添加自定义中间件:
var app = builder.Build(); // 替换掉 app.UseSerilogRequestLogging() app.UseCustomRequestLogging(); // 其他中间件配置...
这种方式完全由你控制日志内容和格式,适合复杂场景。
方案三:自定义Enricher结合AsyncLocal传递上下文
通过AsyncLocal存储请求级别的响应数据,再用自定义Enricher将数据注入到请求日志事件中。
1. 创建请求上下文容器
public static class RequestLoggingContext { private static readonly AsyncLocal<Dictionary<string, object>> _contextStore = new AsyncLocal<Dictionary<string, object>>(); public static void Set(string key, object value) { _contextStore.Value ??= new Dictionary<string, object>(); _contextStore.Value[key] = value; } public static object Get(string key) { return _contextStore.Value?.TryGetValue(key, out var value) == true ? value : null; } }
2. 实现自定义Enricher
public class RequestContextEnricher : ILogEventEnricher { public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { var serviceResponse = RequestLoggingContext.Get("ServiceResponse"); if (serviceResponse != null) { logEvent.AddOrUpdateProperty( propertyFactory.CreateProperty("ServiceResponse", serviceResponse, destructureObjects: true)); } } }
3. 配置Serilog和使用上下文
在Program.cs的Serilog配置中添加Enricher:
Log.Logger = new LoggerConfiguration() .Enrich.With<RequestContextEnricher>() // 其他配置(如输出到Elasticsearch)... .CreateLogger();
在PageModel中存储响应数据:
public void OnGet(string question) { var answer = _service.Execute(question); RequestLoggingContext.Set("ServiceResponse", answer); }
这样每次请求日志事件都会被自动添加ServiceResponse字段,实现数据合并。
内容的提问来源于stack exchange,提问作者barteloma
相关产品推荐
相关产品推荐

