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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 18:32:07