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
相关产品推荐
相关产品推荐

