Java请求日志流程如何规整:从混乱到可观测的架构实践
目录导读
问题背景:日志混乱的三大痛点
在日常开发中,Java请求日志常出现以下问题:

- 格式不统一:各团队使用不同框架(Logback、Log4j2、SLF4J),导致时间戳格式、Msg内容参差不齐。
- 链路断裂:微服务环境下没有TraceId串联,排查一个请求需登录3台机器手动搜索。
- 无用日志泛滥:打印了过多“调用了XX方法”等无效信息,而关键的请求参数、返回值、耗时、异常栈反而缺失。
用户痛点:当线上出现慢请求或报错时,运维同学往往需要花10分钟以上才能定位到问题节点。
规整核心原则:结构化、链路化、标准化
要规整请求日志,必须遵循三个原则:
1 结构化
所有日志输出为JSON格式,方便ELK或日志平台解析,示例字段:
{
"@timestamp": "2025-04-08T10:00:00.123+08:00",
"level": "INFO",
"trace_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"span_id": "123456",
"url": "/api/order/create",
"method": "POST",
"request_body": "{\"userId\": 1001}",
"response_status": 200,
"duration_ms": 152,
"error": null,
"thread_name": "http-nio-8080-exec-3"
}
2 链路化
在请求入口生成唯一TraceId,穿透到所有下游服务,推荐使用:
- Spring Cloud Sleuth(内置MDC自动注入)
- SkyWalking Agent(无侵入式接入)
- OpenTelemetry(社区主流标准)
3 标准化
定义统一的日志级别规范:
- ERROR:业务异常、系统异常、超时、熔断
- WARN:降级、限流、频率过高、入参校验失败
- INFO:核心业务操作(创建订单、支付成功)、请求入参出参
- DEBUG:仅本地开发打印,线上禁止
实战步骤:从拦截到存储的全链路改造
添加MDC过滤器
在Spring Boot应用中,实现一个OncePerRequestFilter:
@Component
public class LogMDCFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
try {
// 从请求头或生成TraceId
String traceId = request.getHeader("X-Trace-Id");
if (traceId == null) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
MDC.put("trace_id", traceId);
MDC.put("span_id", String.valueOf(System.nanoTime()));
// 记录请求开始时间
long start = System.currentTimeMillis();
chain.doFilter(request, response);
long duration = System.currentTimeMillis() - start;
// 核心:打印结构化日志
if (request.getRequestURI().startsWith("/api/")) {
log.info("request_log: uri={}, method={}, duration={}ms",
request.getRequestURI(), request.getMethod(), duration);
}
} finally {
MDC.clear();
}
}
}
配置Logback JSON编码
在logback-spring.xml中加入:
<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/var/log/app/api-%d{yyyy-MM-dd}.json</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
使用
logstash-logback-encoder库自动输出JSON格式。
统一请求参数打印
使用Spring AOP切面,拦截所有Controller方法:
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public Object logRequest(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
// 打印标准请求日志
log.info("req_log: class={}, method={}, args={}, response={}, cost={}ms",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName(),
joinPoint.getArgs(),
result,
end - start);
return result;
}
集成ELK或云日志平台
- Filebeat → Logstash → Elasticsearch → Kibana
- 或直接使用阿里云SLS、腾讯云CLS等托管服务
- 在Kibana中建立
trace_id、duration_ms索引,方便聚合分析
关键问答:解决你最关心的5个问题
Q1:日志量太大怎么办?每天几TB很费钱?
A:实施全量+采样策略,正常请求采样1%,慢请求(>500ms)全量保留,另外对重复异常日志进行去重压缩。
Q2:TraceId如何在不同服务间传递?
A:通过HTTP请求头X-B3-TraceId(Sleuth默认)或自定义header,在Feign/RestTemplate中自动传播,使用RequestInterceptor拦截器。
Q3:敏感数据(如密码)如何脱敏?
A:在Logstash或Logback层使用MaskingConverter过滤,例如将password替换为,或使用logback-mask-sensitive-data插件。
Q4:我的服务是无状态的,怎么做到全链路?
A:无状态服务依然可以使用TraceId,在网关层(如Zuul/Gateway)生成唯一TraceId,通过RequestHeader传递即可,状态仅存在于日志中。
Q5:新老日志系统如何平滑迁移?
A:采用双写策略:旧格式写一份,新JSON格式写一份,持续运行1周后验证新格式无误,再下线旧格式。
性能与成本平衡:日志采样与异步策略
| 策略 | 适用场景 | 配置示例 |
|---|---|---|
| 异步Appender | 减少I/O阻塞,提升吞吐 | <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender"><queueSize>512</queueSize></appender> |
| 采样率控制 | 常规请求降低存储量 | if (request.getRequestURI().startsWith("/health")) { return; } |
| 慢请求优先 | 保留高价值日志 | if (duration > 1000) { log.warn("slow_request: {}ms", duration); } |
| 日志压缩 | 减少磁盘及网络传输量 | Filebeat配置gzip压缩:compression_level: 5 |
建议:配置每秒2000条的异步队列阈值,超出则丢弃(配合WARN日志记录丢弃行为)。
落地效果与持续优化
改造完成后,团队可快速实现:
- 按TraceId搜索:在Kibana输入
trace_id: xxxx瞬间拉通全链路日志 - 慢请求告警:监控
duration_ms超过90%分位值触发钉钉/企微通知 - 服务依赖拓扑:通过日志中的
span_id和parent_id自动生成调用图
持续优化建议:
- 每月进行一次 日志级别审计,清理无用DEBUG日志
- 定期检查 日志字段覆盖率,确保关键字段(
error_stack、http_status)无缺失 - 引入 日志成本预算,控制单服务每日日志总量不超过10GB
最后一步:将以上配置沉淀到公司内部的应用脚手架或基础镜像中,实现“开箱即用”,让每个新服务都自然产出高质量日志。