Java请求日志流程如何规整

wen java案例 28

Java请求日志流程如何规整:从混乱到可观测的架构实践

目录导读

  1. 问题背景:日志混乱的三大痛点
  2. 规整核心原则:结构化、链路化、标准化
  3. 实战步骤:从拦截到存储的全链路改造
  4. 关键问答:解决你最关心的5个问题
  5. 性能与成本平衡:日志采样与异步策略
  6. 落地效果与持续优化

问题背景:日志混乱的三大痛点

在日常开发中,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或云日志平台

  • FilebeatLogstashElasticsearchKibana
  • 或直接使用阿里云SLS、腾讯云CLS等托管服务
  • 在Kibana中建立trace_idduration_ms索引,方便聚合分析

关键问答:解决你最关心的5个问题

Q1:日志量太大怎么办?每天几TB很费钱?
A:实施全量+采样策略,正常请求采样1%,慢请求(>500ms)全量保留,另外对重复异常日志进行去重压缩

Q2:TraceId如何在不同服务间传递?
A:通过HTTP请求头X-B3-TraceId(Sleuth默认)或自定义header,在Feign/RestTemplate中自动传播,使用RequestInterceptor拦截器。

Q3:敏感数据(如密码)如何脱敏?
A:在LogstashLogback层使用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_idparent_id自动生成调用图

持续优化建议

  • 每月进行一次 日志级别审计,清理无用DEBUG日志
  • 定期检查 日志字段覆盖率,确保关键字段(error_stackhttp_status)无缺失
  • 引入 日志成本预算,控制单服务每日日志总量不超过10GB

最后一步:将以上配置沉淀到公司内部的应用脚手架基础镜像中,实现“开箱即用”,让每个新服务都自然产出高质量日志。

抱歉,评论功能暂时关闭!