日志是系统可观测性的基石。在单体时代,简单的日志文件已足够;但在微服务架构下,一次用户请求可能流经数十个服务,日志散落在各节点,必须依赖统一的日志规范与分布式链路追踪才能高效排查问题。
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-slf4j | Spring 默认使用 |
| Log4j 1.x | log4j-over-slf4j | 老系统迁移 |
| JUL | jul-to-slf4j | Java 标准日志 |
| Log4j 2 API | log4j-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
}
}
延伸阅读
- 监控系统架构设计 — 可观测性体系的整体设计
- 云原生可观测性 — Metrics、Logging、Tracing 三支柱
- Java 容器化与 K8s 部署 — 容器环境下的日志采集
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。