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

OpenTelemetry自定义JSON控制台格式化器:对齐.NET与Java日志格式

对齐Java日志格式的.NET自定义日志格式化实现

问题背景

我有两个服务:一个基于.NET开发,一个基于Java开发。Java服务已实现自定义日志格式化器,需要调整.NET应用的日志格式与Java保持一致。

Java服务日志配置

LogTemplatejava.json

{
    "@timestamp": {
        "$resolver": "timestamp",
        "pattern": {
            "format": "yyyy-MM-dd'T'HH:mm:ss.SSSZ",
            "timeZone": "UTC"
        }
    },
    "log.level": {
        "$resolver": "level",
        "field": "name"
    },
    "process.thread.name": {
        "$resolver": "thread",
        "field": "name"
    },
    "log.logger": {
        "$resolver": "logger",
        "field": "name"
    },
    "traceId": {
        "$resolver": "mdc",
        "key": "trace_id"
    },
    "spanId": {
        "$resolver": "mdc",
        "key": "span_id"
    },
    "tenantId": {
        "$resolver": "mdc",
        "key": "tenantId"
    },
    "tenantName": {
        "$resolver": "mdc",
        "key": "tenantName"
    },
    "service.name": "${spring:spring.application.name:-}",
    "service.instance.id": "${env:POD_NAME:-}",
    "message": {
        "$resolver": "message",
        "stringified": true
    },
    "error.type": {
        "$resolver": "exception",
        "field": "className"
    },
    "error.message": {
        "$resolver": "exception",
        "field": "message"
    },
    "error.stack_trace": {
        "$resolver": "exception",
        "field": "stackTrace",
        "stackTrace": {
            "stringified": true
        }
    }
}

log4j.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" name="FP-cloud-configuration">
    <Appenders>
        <Console name="ConsoleAppender" target="SYSTEM_OUT">
            <JsonTemplateLayout eventTemplateUri="classpath:FpLogTemplate.json" />
        </Console>
        <Async name="AsyncConsoleAppender">
            <AppenderRef ref="ConsoleAppender" />
        </Async>
    </Appenders>
    <Loggers>
        <Root>
            <AppenderRef ref="AsyncConsoleAppender" />
        </Root>
    </Loggers>
</Configuration>

Java日志输出示例

{"@timestamp":"2024-07-22T06:36:30.035+0000","log.level":"INFO","process.thread.name":"main","log.logger":"com.authService","service.name":"auth","service.instance.id":"","message":"Starting AuthenticationService using Java 21.0.3 with PID 19723 (/home/user/IdeaProjects-auth/target/classes started by user in /home/user/IdeaProjects/auth)"}
{"@timestamp":"2024-07-22T06:36:30.039+0000","log.level":"INFO","process.thread.name":"main","log.logger":"com.authService","service.name":"auth","service.instance.id":"","message":"The following 1 profile is active: \"lst\""}

当前.NET实现的问题

尝试自定义CustomJsonFormatter对齐格式,但存在以下问题:

  • 无法获取服务名称
  • 输出的是带占位符的原始消息,而非格式化后的完整内容
  • 无法获取Span ID和Trace ID
  • 错误处理结构与Java不一致(Java使用顶级字段而非嵌套对象)

当前.NET代码

自定义格式化器类

public class CustomJsonConsoleFormatter : BaseProcessor<LogRecord>
{
    public override void OnEnd(LogRecord data)
    {
        var logEntry = new
        {
            @timestamp = data.Timestamp.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK", CultureInfo.InvariantCulture),
            log_level = data.LogLevel.ToString(),
            process_thread_name = data.CategoryName, // Adjust if needed to get the actual thread name
            log_logger = data.CategoryName,
            service_name = data.Attributes?.FirstOrDefault(attr => attr.Key == "service.name").Value?.ToString(),
            message = data.Body,
            error = data.Exception != null
                ? new
                {
                    type = data.Exception.GetType().Name,
                    message = data.Exception.Message,
                    stack_trace = data.Exception.StackTrace
                }
                : null
        };

        string logJson = JsonSerializer.Serialize(logEntry);
        Console.WriteLine(logJson);
    }
}

Startup.cs配置

_ = services.AddLogging(logging =>
{
    logging.ClearProviders();
    logging.AddOpenTelemetry(options =>
    {
        options.IncludeFormattedMessage = true;
        options.IncludeScopes = true;
        options.ParseStateValues = true;
        options.AddProcessor(new CustomJsonConsoleFormatter());
    });
});

当前.NET日志输出示例

{"timestamp":"2024-07-22T09:04:48.084Z","log_level":"Information","process_thread_name":"Microsoft.Hosting.Lifetime","log_logger":"Microsoft.Hosting.Lifetime","service_name":null,"message":"Now listening on: {address}","error":null}

解决方案

1. 核心修正点

  • 字段名与Java完全对齐(如log.level而非log_level)
  • 时间格式调整为+0000后缀,匹配Java的UTC格式
  • 错误字段改为顶级属性error.type/error.message/error.stack_trace
  • 使用FormattedMessage获取填充占位符后的完整消息
  • 通过Activity类获取Trace ID和Span ID
  • 从配置/环境变量读取服务名称与实例ID
  • 直接获取当前线程名称,而非用CategoryName代替

2. 完整修正代码

自定义格式化器类

using System.Globalization;
using System.Text.Json;
using System.Threading;
using OpenTelemetry.Logs;
using System.Diagnostics;

public class CustomJsonConsoleFormatter : BaseProcessor<LogRecord>
{
    private readonly string _serviceName;
    private readonly string _serviceInstanceId;

    public CustomJsonConsoleFormatter(string serviceName, string serviceInstanceId)
    {
        _serviceName = serviceName ?? string.Empty;
        _serviceInstanceId = serviceInstanceId ?? string.Empty;
    }

    public override void OnEnd(LogRecord data)
    {
        // 获取链路追踪ID
        var activity = Activity.Current;
        string traceId = activity?.TraceId.ToString() ?? string.Empty;
        string spanId = activity?.SpanId.ToString() ?? string.Empty;

        // 获取租户信息(从属性或作用域中读取)
        string tenantId = GetAttributeOrScopeValue(data, "tenantId");
        string tenantName = GetAttributeOrScopeValue(data, "tenantName");

        // 构建日志条目
        var logEntry = new
        {
            @timestamp = data.Timestamp.ToString("yyyy-MM-dd'T'HH:mm:ss.fff+0000", CultureInfo.InvariantCulture),
            log_level = MapLogLevel(data.LogLevel),
            process_thread_name = Thread.CurrentThread.Name ?? $"Thread-{Thread.CurrentThread.ManagedThreadId}",
            log_logger = data.CategoryName,
            traceId = traceId,
            spanId = spanId,
            tenantId = tenantId,
            tenantName = tenantName,
            service_name = _serviceName,
            service_instance_id = _serviceInstanceId,
            message = data.FormattedMessage ?? data.Body.ToString(),
            error_type = data.Exception?.GetType().FullName,
            error_message = data.Exception?.Message,
            error_stack_trace = data.Exception?.StackTrace
        };

        // 序列化忽略空值,对齐Java输出
        var options = new JsonSerializerOptions
        {
            IgnoreNullValues = true,
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };

        Console.WriteLine(JsonSerializer.Serialize(logEntry, options));
    }

    // 映射.NET日志级别到Java标准名称
    private string MapLogLevel(LogLevel logLevel)
    {
        return logLevel switch
        {
            LogLevel.Trace => "TRACE",
            LogLevel.Debug => "DEBUG",
            LogLevel.Information => "INFO",
            LogLevel.Warning => "WARN",
            LogLevel.Error => "ERROR",
            LogLevel.Critical => "FATAL",
            _ => logLevel.ToString().ToUpper()
        };
    }

    // 从日志属性或作用域中获取指定值
    private string GetAttributeOrScopeValue(LogRecord data, string key)
    {
        // 先检查属性
        if (data.Attributes != null && data.Attributes.TryGetValue(key, out var value))
        {
            return value?.ToString() ?? string.Empty;
        }
        // 再检查作用域
        foreach (var scope in data.Scopes)
        {
            if (scope is IEnumerable<KeyValuePair<string, object>> scopeAttrs)
            {
                var attr = scopeAttrs.FirstOrDefault(s => s.Key == key);
                if (!string.IsNullOrEmpty(attr.Key))
                {
                    return attr.Value?.ToString() ?? string.Empty;
                }
            }
        }
        return string.Empty;
    }
}

Startup.cs配置更新

using Microsoft.Extensions.Configuration;

_ = services.AddLogging(logging =>
{
    logging.ClearProviders();
    var config = services.BuildServiceProvider().GetRequiredService<IConfiguration>();
    
    // 读取服务名称(可配置在appsettings.json或环境变量)
    string serviceName = config["ServiceName"] ?? config["Spring:Application:Name"] ?? string.Empty;
    // 读取实例ID(对应Java的POD_NAME环境变量)
    string serviceInstanceId = Environment.GetEnvironmentVariable("POD_NAME") ?? string.Empty;

    logging.AddOpenTelemetry(options =>
    {
        options.IncludeFormattedMessage = true;
        options.IncludeScopes = true;
        options.ParseStateValues = true;
        options.AddProcessor(new CustomJsonConsoleFormatter(serviceName, serviceInstanceId));
        // 启用链路追踪,确保能获取TraceID/SpanID
        options.AddOtlpExporter();
    });
});

修正后的.NET日志输出示例

{"@timestamp":"2024-07-22T09:04:48.084+0000","logLevel":"INFO","processThreadName":"main","logLogger":"Microsoft.Hosting.Lifetime","traceId":"abc123","spanId":"def456","tenantId":"","tenantName":"","serviceName":"auth","serviceInstanceId":"","message":"Now listening on: http://localhost:5000"}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 15:00:05