下一篇【第70篇】代码性能剖析(Profiling)——生产环境线程栈采样与火焰图分析
上一篇【第72篇】SkyWalking指标基线与异常检测——超越静态阈值告警的智能监控
一、痛苦的四步查找法
在没有Trace-Log关联之前,排查线上问题的典型流程是这样的:
+------------------------------------------------------------------+ | 传统日志排查的痛苦流程 | +------------------------------------------------------------------+ | | | Step 1: SkyWalking UI上看到一个慢请求 | | TraceId: abc-123-def | | ↓ | | Step 2: 到Kibana搜索 "error" | | 返回 50000 条日志 😱 | | ↓ | | Step 3: 缩小时间范围到5分钟 | | 仍有 300 条日志 😓 | | ↓ | | Step 4: 手动搜索 TraceId | | "abc-123-def" → 0结果 | | 什么?日志里没有TraceId! | | ↓ | | Step 5: 根据URL、用户ID等信息人工推断 | | 对应哪些日志行... | | ↓ | | 30分钟过去了,问题还没定位 😭 | | | | 最佳状态应该是: | | Step 1: SkyWalking UI看到异常Trace | | Step 2: 点击"查看关联日志" | | Step 3: 所有相关日志一目了然 | | ↓ | | 30秒解决问题 ✓ | | | +------------------------------------------------------------------+二、TraceId怎么写入日志——MDC的原理
2.1 MDC是什么
MDC (Mapped Diagnostic Context) 是SLF4J提供的一个功能:在当前线程的上下文中存储键值对,日志框架在输出日志时自动将这些键值对嵌入到日志中。
// MDC的使用方式MDC.put("traceId","abc-123-def");log.info("收到订单请求");// 输出: [abc-123-def] 收到订单请求MDC.remove("traceId");// 用完记得清理2.2 SkyWalking自动注入TraceId到MDC
+------------------------------------------------------------------+ + SkyWalking TraceId → MDC 自动注入流程 + +------------------------------------------------------------------+ | | | 请求到达 → SkyWalking Agent拦截 | | │ | | ↓ | | ┌─────────────────────────────┐ │ | │ 1. Agent创建Span │ │ | │ 2. 生成 TraceId │ │ | │ 3. 自动注入 MDC: │ │ | │ MDC.put("traceId", │ │ | │ "abc-123-def.1.xxx") │ │ | └─────────────┬───────────────┘ │ | │ │ | ↓ │ | ┌─────────────────────────────┐ │ | │ 业务代码执行 │ │ | │ log.info("处理订单...") │ │ | │ → 自动携带 traceId! │ │ | └─────────────┬───────────────┘ │ | │ │ | ↓ │ | ┌─────────────────────────────┐ │ | │ Agent停止Span │ │ | │ MDC.remove("traceId") │ │ | └─────────────────────────────┘ │ | | +------------------------------------------------------------------+三、Logback配置——最主流的方式
3.1 Logback完整配置
<?xml version="1.0" encoding="UTF-8"?><configuration><!-- ========================================== --><!-- SkyWalking日志配置 --><!-- ========================================== --><!-- 1. 引入SkyWalking的TraceId --><!-- SkyWalking会自动设置MDC中的"traceId"键 --><!-- 2. 自定义Pattern(包含traceId) --><propertyname="LOG_PATTERN"value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n"/><!-- 关键: %X{traceId} %X{key} 会从MDC中读取key对应的值 如果MDC中没有traceId,输出空字符串 --><!-- 3. 控制台输出 --><appendername="CONSOLE"class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>${LOG_PATTERN}</pattern><charset>UTF-8</charset></encoder></appender><!-- 4. gRPC输出(发送到SkyWalking OAP) --><appendername="GRPC"class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender"><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern></encoder></appender><!-- 5. 文件输出(含TraceId) --><appendername="FILE"class="ch.qos.logback.core.rolling.RollingFileAppender"><file>logs/application.log</file><rollingPolicyclass="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern><maxHistory>30</maxHistory></rollingPolicy><encoder><pattern>${LOG_PATTERN}</pattern></encoder></appender><rootlevel="INFO"><appender-refref="CONSOLE"/><appender-refref="GRPC"/><appender-refref="FILE"/></root></configuration>3.2 日志输出效果
# 配置后,日志会自动携带TraceId 2026-07-02 10:30:00.123 [http-nio-8080-exec-1] [abc123.1.xxx] INFO OrderController - 收到创建订单请求 2026-07-02 10:30:00.125 [http-nio-8080-exec-1] [abc123.1.xxx] DEBUG OrderService - 计算价格: items=3 2026-07-02 10:30:00.200 [http-nio-8080-exec-1] [abc123.1.xxx] INFO OrderService - 订单已保存: orderId=45678 2026-07-02 10:30:00.210 [http-nio-8080-exec-1] [abc123.1.xxx] WARN NotificationService - MQ发送延迟: 45ms 2026-07-02 10:30:00.215 [http-nio-8080-exec-1] [abc123.1.xxx] INFO OrderController - 订单创建完成 # 异步线程中的日志也能正确携带TraceId 2026-07-02 10:30:00.500 [async-pool-1] [abc123.1.xxx] INFO EmailService - 发送订单确认邮件四、Log4j2配置
<?xml version="1.0" encoding="UTF-8"?><Configurationstatus="WARN"><Properties><!-- 注意Log4j2中MDC的语法是 %X{key} --><Propertyname="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] [%X{traceId}] %-5level %c{1} - %msg%n</Property></Properties><Appenders><!-- 控制台 --><Consolename="Console"target="SYSTEM_OUT"><PatternLayoutpattern="${LOG_PATTERN}"/></Console><!-- SkyWalking gRPC Appender --><GRPCLogClientAppendername="GRPCLog"><PatternLayoutpattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %c{1} - %msg%n"/></GRPCLogClientAppender><!-- 文件(JSON格式,方便ELK解析) --><Filename="FileJson"fileName="logs/application.json"><JsonLayoutcomplete="false"compact="true"><KeyValuePairkey="traceId"value="$${ctx:traceId}"/><KeyValuePairkey="timestamp"value="$${date:yyyy-MM-dd HH:mm:ss.SSS}"/><KeyValuePairkey="level"value="$${level}"/><KeyValuePairkey="logger"value="$${logger}"/><KeyValuePairkey="message"value="$${message}"/><KeyValuePairkey="thread"value="$${thread:name}"/></JsonLayout></File></Appenders><Loggers><Rootlevel="INFO"><AppenderRefref="Console"/><AppenderRefref="GRPCLog"/><AppenderRefref="FileJson"/></Root></Loggers></Configuration>五、在SkyWalking UI中查看关联日志
5.1 配置Log Bridge
# agent/config/agent.config# 启用日志插件plugin.toolkit.log.grpc.reporter.server_host=oap-server plugin.toolkit.log.grpc.reporter.server_port=11800 plugin.toolkit.log.grpc.reporter.max_message_size=10485760 plugin.toolkit.log.grpc.reporter.upstream_timeout=30# 或者使用Kafka传输日志plugin.toolkit.log.kafka.reporter.bootstrap_servers=kafka:9092plugin.toolkit.log.kafka.reporter.topic=skywalking-logs5.2 gRPC日志上报的Maven依赖
<dependency><groupId>org.apache.skywalking</groupId><artifactId>apm-toolkit-logback-1.x</artifactId><version>8.16.0</version></dependency><!-- 或 Log4j2 --><dependency><groupId>org.apache.skywalking</groupId><artifactId>apm-toolkit-log4j-2.x</artifactId><version>8.16.0</version></dependency>六、与ELK/Loki的集成方案
+------------------------------------------------------------------+ + 日志管道的完整架构 + +------------------------------------------------------------------+ | | | ┌──────────────────────────────────────────────────────────┐ │ | │ 应用 JVM │ │ | │ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │ │ | │ │SkyWalking │ │ 日志框架 │ │ FileBeat / │ │ │ | │ │Agent │ │(Logback) │ │ Fluentd / │ │ │ | │ │ │ │ │ │ Promtail │ │ │ | │ │ 自动注入 │→ │ %X{traceId}│← │ 采集JSON日志文件 │ │ │ | │ │ traceId │ │ │ │ │ │ │ | │ │ 到MDC │ │ 输出到: │ │ │ │ │ | │ │ │ │ 1.控制台 │ │ │ │ │ | │ │ │ │ 2.文件 │ │ │ │ │ | │ │ │ │ 3.gRPC │ │ │ │ │ | │ └──────────┘ └────┬─────┘ │ │ │ │ | │ │ │ │ │ │ | └─────────────────────┼────────┼──────────────────────────┘ │ | │ │ │ | gRPC直接│ │FileBeat采集 │ | │ │ │ | ┌───────────▼──┐ ┌──▼───────────┐ │ | │ SkyWalking │ │ Elasticsearch │ │ | │ OAP Server │ │ / Loki │ │ | │ │ │ │ │ | │ 日志与Trace │ │ 日志检索 │ │ | │ 关联查询 │ │ 分析 │ │ | └──────────────┘ └───────────────┘ │ | | +------------------------------------------------------------------+6.1 FileBeat配置示例
# filebeat.ymlfilebeat.inputs:-type:logenabled:truepaths:-/var/log/app/*.jsonjson.keys_under_root:truejson.add_error_key:true# 提取traceId作为索引字段fields:trace_id:"%{[traceId]}"fields_under_root:false# 输出到Elasticsearchoutput.elasticsearch:hosts:["elasticsearch:9200"]index:"app-logs-%{+yyyy.MM.dd}"# 使用traceId作为routing key(同一Trace的日志存到同一shard)pipeline:"app-logs-pipeline"6.2 Logstash配置(可选)
# logstash.confinput{beats{port=>5044}}filter{json{source=>"message"}# 解析traceIdif[traceId]{mutate{add_field=>{"skywalking_trace_id"=>"%{traceId}"}}}}output{elasticsearch{hosts=>["elasticsearch:9200"]index=>"app-logs-%{+YYYY.MM.dd}"routing=>"%{skywalking_trace_id}"}}七、结构化日志的最佳实践
7.1 日志格式规范
// 推荐的日志格式规范// 使用SLF4J的参数化日志(避免字符串拼接)log.info("创建订单成功, orderId={}, userId={}, amount={}",orderId,userId,amount);// 不好的写法(字符串拼接有性能开销)log.info("创建订单成功, orderId="+orderId+", userId="+userId);7.2 JSON日志格式规范
{"timestamp":"2026-07-02T10:30:00.123Z","level":"INFO","logger":"com.example.OrderService","thread":"http-nio-8080-exec-1","traceId":"abc123def456.1.1625140800000","message":"创建订单成功","context":{"orderId":"ORD-2026-001","userId":"user-123","amount":199.99},"duration":45}7.3 日志记录的最佳实践检查清单
✅ 日志带TraceId:通过MDC %X{traceId} 自动添加 ✅ 结构化日志:使用JSON格式便于搜索引擎解析 ✅ 关键上下文:记录请求参数、用户ID、业务ID ✅ 异常完整:异常日志包含堆栈信息 ✅ 合适级别:DEBUG/INFO/WARN/ERROR 合理使用 ✅ 避免敏感信息:不记录密码、密钥、身份证号等 ✅ 使用参数化日志:log.info("a={}", a) 而非拼接八、总结
日志与Trace的关联是"鱼和水"的关系——分开各有价值,结合起来才是完整的可观测性:
| 能力 | 日志 | Trace | 日志+Trace |
|---|---|---|---|
| 看到发生了什么 | ✓ | ✓ | ✓ |
| 看到在哪里发生 | ✓ | ✓ | ✓ |
| 看到发生的原因 | ✓ | ✗ | ✓ |
| 看到完整的请求链路 | ✗ | ✓ | ✓ |
| 看到链路的上下文 | ✗ | ✓ | ✓ |
下一篇【第70篇】代码性能剖析(Profiling)——生产环境线程栈采样与火焰图分析
上一篇【第72篇】SkyWalking指标基线与异常检测——超越静态阈值告警的智能监控