17. 日志框架、MDC 与分布式链路追踪

深入 SLF4J、Logback、Log4j2 日志框架,MDC 线程上下文传递,以及基于 Micrometer 与 OpenTelemetry 的全链路追踪实现

日志是系统可观测性的基石。在单体时代,简单的日志文件已足够;但在微服务架构下,一次用户请求可能流经数十个服务,日志散落在各节点,必须依赖统一的日志规范分布式链路追踪才能高效排查问题。

1. SLF4J + Logback 核心架构

1.1 Java 日志框架演进

发展历程:
JDK 1.4: java.util.logging (JUL) ──→ 功能简陋
    │
    ├── Log4j 1.x (Apache, 2001) ──→ 被 Log4j 2 取代,已废弃
    │    └── 性能瓶颈(全局锁)
    │
    ├── SLF4J (2005) ──→ 日志门面(Facade),统一 API
    │    ├── Logback (Log4j 作者编写,原生支持 SLF4J)
    │    └── Log4j 2.x (2014, 不再兼容 1.x)
    │
    └── Log4j 2 (性能最高,支持异步) ──→ 推荐新项目使用

1.2 SLF4J 门面机制

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OrderService {
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);

    public void createOrder(OrderDTO order) {
        // 使用占位符,避免字符串拼接开销(参数不使用时)
        log.info("创建订单: orderId={}, userId={}, amount={}",
            order.getId(), order.getUserId(), order.getAmount());

        // 条件日志(避免高级别日志的参数计算)
        if (log.isDebugEnabled()) {
            log.debug("订单详情: {}", JsonUtils.toJson(order)); // 可能耗时的序列化
        }
    }
}

桥接方案:将第三方库的日志重定向到统一实现。

第三方库日志桥接依赖说明
JCL (commons-logging)jcl-over-slf4jSpring 默认使用
Log4j 1.xlog4j-over-slf4j老系统迁移
JULjul-to-slf4jJava 标准日志
Log4j 2 APIlog4j-to-slf4j使用 Log4j2 API 但走 SLF4J

1.3 Logback 配置详解

<!-- logback-spring.xml -->
<configuration>
    <!-- 彩色控制台输出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%cyan(%d{HH:mm:ss.SSS}) %highlight(%-5level) [%magenta(%thread)] %yellow(%logger{36}) - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 按日期滚动文件(错误日志单独存储) -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        <file>logs/error.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxHistory>30</maxHistory>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <!-- JSON 格式,供 ELK 采集 -->
        </encoder>
    </appender>

    <!-- 按大小滚动(全量日志) -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/application.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>logs/application.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
            <maxFileSize>500MB</maxFileSize>
            <maxHistory>7</maxHistory>
            <totalSizeCap>10GB</totalSizeCap>
        </rollingPolicy>
        <encoder class="net.logstash.logback.encoder.LogstashEncoder" />
    </appender>

    <!-- 异步 Appender(高吞吐场景) -->
    <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
        <queueSize>5000</queueSize>
        <discardingThreshold>0</discardingThreshold>
        <appender-ref ref="FILE" />
    </appender>

    <!-- 日志级别配置 -->
    <logger name="com.myapp.service" level="DEBUG" />
    <logger name="org.springframework.web" level="WARN" />
    <logger name="org.hibernate.SQL" level="DEBUG" />  <!-- 打印 SQL -->

    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="ASYNC_FILE" />
        <appender-ref ref="ERROR_FILE" />
    </root>
</configuration>

生产环境日志格式建议

%d{ISO8601} | %level | [%thread] | %logger{36} | traceId=%X{traceId} | %msg%n

2024-01-28T10:30:25.123+08:00 | INFO | [http-nio-8080-exec-3] | c.m.s.OrderService | traceId=abc123 | 订单创建成功

2. MDC:线程上下文映射

2.1 MDC 基础用法

import org.slf4j.MDC;

@Component
public class TraceFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                     HttpServletResponse response,
                                     FilterChain chain) throws ServletException, IOException {
        String traceId = request.getHeader("X-Trace-Id");
        if (traceId == null) {
            traceId = UUID.randomUUID().toString().replace("-", "");
        }

        // 放入 MDC,当前线程的所有日志自动带上 traceId
        MDC.put("traceId", traceId);
        MDC.put("spanId", generateSpanId());
        MDC.put("userId", getUserId(request));

        try {
            response.setHeader("X-Trace-Id", traceId);
            chain.doFilter(request, response);
        } finally {
            // 必须清理,防止线程复用导致污染
            MDC.clear();
        }
    }
}

2.2 跨线程 MDC 传递

/**
 * Virtual Threads / CompletableFuture / @Async 场景下 MDC 传递
 */
public class MDCPropagatingExecutor implements Executor {
    private final Executor delegate;

    @Override
    public void execute(Runnable task) {
        Map<String, String> context = MDC.getCopyOfContextMap();
        delegate.execute(() -> {
            if (context != null) {
                MDC.setContextMap(context);
            }
            try {
                task.run();
            } finally {
                MDC.clear();
            }
        });
    }
}

// Spring @Async 使用自定义 Executor
@Bean("mdcExecutor")
public Executor mdcExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(4);
    executor.setMaxPoolSize(8);
    executor.setTaskDecorator(new MDCContextTaskDecorator());
    executor.initialize();
    return executor;
}

2.3 Reactive WebFlux 中的 MDC

@Configuration
public class WebFluxTraceConfig {

    public static final String TRACE_ID_KEY = "traceId";

    @Bean
    public WebFilter traceIdWebFilter() {
        return (exchange, chain) -> {
            String traceId = exchange.getRequest().getHeaders()
                .getFirst("X-Trace-Id");
            if (traceId == null) {
                traceId = UUID.randomUUID().toString();
            }

            // 写入 Reactor Context
            return chain.filter(exchange)
                .contextWrite(Context.of(TRACE_ID_KEY, traceId));
        };
    }
}

// 在日志中使用 Hooks
Hooks.onEachOperator("MDC", Operators.lift((scannable, coreSubscriber) -> {
    String traceId = coreSubscriber.currentContext()
        .getOrDefault(TRACE_ID_KEY, "unknown");
    return new MDCSubscriber(coreSubscriber, traceId);
}));

3. 分布式链路追踪

3.1 OpenTelemetry 标准模型

Trace(一次完整请求链路)
  ├── Span A(入口服务 Gateway)trace_id=abc, span_id=a1, parent=null
  │     ├── Span B(订单服务)trace_id=abc, span_id=b1, parent=a1
  │     │     ├── Span C(数据库查询)trace_id=abc, span_id=c1, parent=b1
  │     │     └── Span D(缓存查询)trace_id=abc, span_id=d1, parent=b1
  │     └── Span E(库存服务 RPC)trace_id=abc, span_id=e1, parent=a1
  │
  └── Span 之间通过 Context 传递 trace_id / span_id

Baggage:跨服务传递的业务上下文(如 userId、tenantId)

3.2 Spring Boot 3 + Micrometer Tracing

Spring Boot 3.x 内置 Micrometer Observation API,统一了 Metrics 和 Tracing:

# application.yml
management:
  tracing:
    sampling:
      probability: 1.0  # 采样率 100%(开发环境)
    propagation:
      type: W3C  # 或 B3 (Zipkin)
  zipkin:
    tracing:
      endpoint: http://zipkin:9411/api/v2/spans
  observations:
    key-values:
      application: order-service
      env: production
@Service
public class OrderService {
    private final ObservationRegistry observationRegistry;

    public Order createOrder(CreateOrderRequest request) {
        return Observation.createNotStarted("order.create", observationRegistry)
            .lowCardinalityKeyValue("user.type", request.getUserType())
            .highCardinalityKeyValue("user.id", request.getUserId())
            .observe(() -> {
                // 业务逻辑
                validate(request);
                Order order = persist(request);
                sendEvent(order);
                return order;
            });
    }
}

3.3 自定义 Span 与标签

@Service
public class PaymentClient {
    private final Tracer tracer;

    public PaymentResult charge(ChargeRequest request) {
        Span span = tracer.nextSpan()
            .name("payment.charge")
            .tag("payment.provider", request.getProvider())
            .tag("payment.currency", request.getCurrency())
            .start();

        try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) {
            long start = System.currentTimeMillis();
            PaymentResult result = httpClient.post("/charge", request);
            span.tag("payment.status", result.getStatus());
            return result;
        } catch (Exception e) {
            span.error(e);
            throw e;
        } finally {
            span.end();
        }
    }
}

3.4 跨服务传播

HTTP 传播(W3C Trace Context)

GET /api/orders/123 HTTP/1.1
Host: order-service
Traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
Tracestate: congo=t61rcWkgMzE

Feign 自动传播

@Configuration
public class FeignTraceConfig {
    @Bean
    public RequestInterceptor traceInterceptor(Tracer tracer) {
        return requestTemplate -> {
            Span span = tracer.currentSpan();
            if (span != null) {
                requestTemplate.header("traceparent",
                    String.format("00-%032x-%016x-%02x",
                        span.context().traceId(),
                        span.context().spanId(),
                        1)); // sampled
            }
        };
    }
}

4. ELK / EFK 日志聚合

4.1 架构设计

App (Logstash JSON)
  └── Filebeat(日志采集)
       └── Kafka(缓冲)
            └── Logstash(解析/过滤)
                 └── Elasticsearch(存储)
                      └── Kibana(可视化)

4.2 Filebeat 配置

# filebeat.yml
filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /var/log/app/*.log
  json.keys_under_root: true
  json.add_error_key: true
  processors:
    - add_kubernetes_metadata:
        host: ${NODE_NAME}
        matchers:
        - logs_path:
            logs_path: "/var/log/containers"

output.kafka:
  hosts: ["kafka-1:9092", "kafka-2:9092"]
  topic: "app-logs"
  partition.round_robin:
    reachable_only: true

4.3 Logstash 解析规则

# logstash.conf
filter {
  if [logger_name] =~ /com\.myapp\..*/ {
    mutate { add_tag => ["app_log"] }
  }

  # 提取 traceId 字段供链路检索
  if [traceId] {
    mutate { add_field => { "[@metadata][traceId]" => "%{traceId}" } }
  }

  # 慢查询标记
  if [duration_ms] and [duration_ms] > 1000 {
    mutate { add_tag => ["slow_query"] }
  }

  # 错误等级日志附加环境信息
  if [level] == "ERROR" {
    mutate { add_field => { "alert_needed" => true } }
  }
}

output {
  elasticsearch {
    hosts => ["http://es:9200"]
    index => "app-logs-%{+YYYY.MM.dd}"
  }

  if [alert_needed] {
    # 错误日志同时发送到告警队列
    kafka {
      topic_id => "error-alerts"
    }
  }
}

4.4 Kibana 仪表盘

面板用途
日志总量趋势发现异常流量
ERROR 级别分布快速定位问题服务
Trace ID 聚合按链路查看跨服务日志
慢查询 Top 10性能优化线索
用户信息分布业务洞察

5. 统一日志规范

5.1 日志级别指导

级别使用场景示例
TRACE最详细调试方法入参/出参
DEBUG开发调试SQL 语句、缓存命中
INFO正常业务事件订单创建、支付完成
WARN非致命异常降级处理、重试
ERROR业务失败支付失败、DB 超时

5.2 结构化日志 JSON Schema

{
  "@timestamp": "2024-01-28T10:30:25.123+08:00",
  "level": "INFO",
  "logger": "com.myapp.OrderService",
  "thread": "http-nio-8080-exec-3",
  "message": "订单创建成功",
  "traceId": "abc123def456",
  "spanId": "span789",
  "service": "order-service",
  "environment": "production",
  "context": {
    "orderId": "10086",
    "userId": "9527",
    "amount": 199.99
  }
}

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java-enterprise」更多文章

  1. 限流算法深度解析:令牌桶、漏桶与滑动窗口计数
  2. Java 代码质量:SonarQube、Checkstyle 与 SpotBugs 工程化实践
  3. Spring IoC 容器与依赖注入原理深度剖析