使用JsonFormatter时如何为JSON日志添加自定义根属性?
解决方案
1. 自定义Json日志格式化器
直接继承原生ConsoleFormatter,重写日志输出逻辑,将自定义属性与默认日志结构合并到根级别:
步骤1:实现自定义格式化器
public class CustomJsonConsoleFormatter : ConsoleFormatter { private readonly JsonSerializerOptions _options; private readonly IHttpContextAccessor _httpContextAccessor; public CustomJsonConsoleFormatter(JsonSerializerOptions options, IHttpContextAccessor httpContextAccessor) : base("CustomJson") { _options = options ?? new JsonSerializerOptions { WriteIndented = false }; _httpContextAccessor = httpContextAccessor; } public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider scopeProvider, TextWriter textWriter) { // 构建默认日志属性集合 var logProperties = new Dictionary<string, object?> { ["Timestamp"] = DateTime.Now.ToString("HH:mm:ss "), ["EventId"] = logEntry.EventId.Id, ["LogLevel"] = logEntry.LogLevel.ToString(), ["Category"] = logEntry.Category, ["Message"] = logEntry.Formatter(logEntry.State, logEntry.Exception) }; // 处理原始State数据 if (logEntry.State is IEnumerable<KeyValuePair<string, object>> stateProperties) { logProperties["State"] = stateProperties.ToDictionary(kv => kv.Key, kv => kv.Value); } // 从上下文获取并添加自定义属性(RequestId/CustomerId等) var httpContext = _httpContextAccessor.HttpContext; if (httpContext != null) { if (httpContext.Items.TryGetValue("RequestId", out var requestId)) logProperties["RequestId"] = requestId; if (httpContext.Items.TryGetValue("CustomerId", out var customerId)) logProperties["CustomerId"] = customerId; if (httpContext.Items.TryGetValue("CustomerName", out var customerName)) logProperties["CustomerName"] = customerName; } // 序列化为JSON并输出 textWriter.WriteLine(JsonSerializer.Serialize(logProperties, _options)); } }
步骤2:注册格式化器
在Program.cs中注入服务并启用自定义格式化:
builder.Services.AddHttpContextAccessor(); builder.Services.AddSingleton<ConsoleFormatter, CustomJsonConsoleFormatter>(sp => new CustomJsonConsoleFormatter(new JsonSerializerOptions(), sp.GetRequiredService<IHttpContextAccessor>())); builder.Services.Configure<ConsoleLoggerOptions>(options => { options.FormatterName = "CustomJson"; });
2. 使用Serilog(推荐)
Serilog原生支持将上下文属性注入日志根级别,配置简单且无需复杂自定义逻辑:
步骤1:安装NuGet包
Install-Package Serilog.AspNetCore Install-Package Serilog.Enrichers.Context
步骤2:配置Serilog并注入上下文属性
在Program.cs中替换默认日志系统:
var builder = WebApplication.CreateBuilder(args); // 配置Serilog输出JSON格式日志,并启用上下文 enrichment builder.Host.UseSerilog((context, config) => { config.WriteTo.Console(new Serilog.Formatting.Json.JsonFormatter()) .Enrich.FromLogContext(); }); // 添加请求中间件,注入自定义属性到日志上下文 app.Use(async (context, next) => { // 实际场景中可从请求头/数据库等获取真实数据 var requestId = Guid.NewGuid().ToString(); var customerId = "BD23ECBE-883B-4D96-ACD3-A526933B6D5B"; var customerName = "Contoso"; // 将属性推入Serilog上下文,自动附加到日志根级别 using (LogContext.PushProperty("RequestId", requestId)) using (LogContext.PushProperty("CustomerId", customerId)) using (LogContext.PushProperty("CustomerName", customerName)) { context.Items["RequestId"] = requestId; await next(); } }); // 其他中间件配置... app.Run();
3. 原生日志范围(Logger Scope)+ 格式化增强
利用原生BeginScope添加上下文属性,再通过自定义格式化器将Scope属性提取到日志根级别:
步骤1:中间件中创建日志范围
app.Use(async (context, next) => { var logger = context.RequestServices.GetRequiredService<ILogger<Program>>(); // 构建自定义属性集合 var scopeProperties = new Dictionary<string, object> { ["RequestId"] = Guid.NewGuid().ToString(), ["CustomerId"] = "BD23ECBE-883B-4D96-ACD3-A526933B6D5B", ["CustomerName"] = "Contoso" }; // 创建日志范围,属性会被后续日志自动关联 using (logger.BeginScope(scopeProperties)) { await next(); } });
步骤2:修改自定义格式化器提取Scope属性
在之前的CustomJsonConsoleFormatter的Write方法中添加Scope属性提取逻辑:
// 遍历日志范围,将属性添加到根级别 scopeProvider?.ForEachScope((scope, state) => { if (scope is IEnumerable<KeyValuePair<string, object>> scopeProperties) { foreach (var kv in scopeProperties) { if (!logProperties.ContainsKey(kv.Key)) logProperties[kv.Key] = kv.Value; } } }, null);
内容的提问来源于stack exchange,提问作者Shubham
相关产品推荐
相关产品推荐

