找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2929

积分

0

好友

393

主题
发表于 1 小时前 | 查看: 5| 回复: 0

你还在手动计算API耗时吗?看请求开始日志的时间,再看请求结束日志的时间,然后自己心算或者拿计算器减一下?这办法不仅效率低,而且容易出错,当需要排查大量请求的性能问题时,简直是场灾难。

在我们的手写链路追踪系列教程中,我们已经实现了用户信息在全链路中的自动透传。今天,我们来完成一个更实用的功能:全局API性能监控。目标是让系统自动识别每个API接口,并在请求结束时,将URI、耗时和响应状态码清晰地打印在日志里,彻底告别人工计算。

1. 核心思路与实现

实现全局API监控的核心在于 Spring MVC的拦截器(Interceptor)MDC(Mapped Diagnostic Context)。我们将在请求进入时记录开始时间,在请求处理完成时计算耗时,并最终输出到日志。

1.1 增强MDC工具类:记录时间

首先,我们需要在已有的 MdcUtils 工具类中增加记录请求开始时间和计算耗时的功能。

package com.sandwich.logtracing.util;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;

/**
 * @Description: MDC工具类
 */
public class MdcUtils {
    public static final String TRACE_ID = "traceId";
    public static final String USER_ID = "userId";
    public static final String USER_NAME = "username";
    public static final String CLIENT_IP = "clientIp";

    // 新增:用于记录API开始时间和请求耗时
    public static final String START_TIME = "startTime";
    public static final String REQUEST_TIME = "requestTime";

    // ... 其他已有的 get/set 方法(如 setTraceId, getUserId等) ...

    /**
     * 在请求开始时,设置开始时间到MDC
     */
    public static void setStartTime() {
        MDC.put(START_TIME, System.currentTimeMillis() + "");
    }

    /**
     * 获取MDC中记录的开始时间
     */
    public static String getStartTime() {
        return MDC.get(START_TIME);
    }

    /**
     * 计算请求耗时,并设置到MDC中
     */
    public static void setRequestTime() {
        // 计算请求耗时
        String startTimeStr = MDC.get(START_TIME);
        if (startTimeStr != null) {
            long startTime = Long.parseLong(startTimeStr);
            long endTime = System.currentTimeMillis();
            long duration = endTime - startTime;
            MDC.put(REQUEST_TIME, String.valueOf(duration));
        }
    }

    /**
     * 获取计算出的请求耗时
     */
    public static String getRequestTime() {
        return MDC.get(REQUEST_TIME);
    }

    public static void clearAll() {
        MDC.clear();
    }
}

1.2 改造拦截器:植入监控逻辑

接下来,我们在负责链路追踪的拦截器 MDCInterceptor 的关键生命周期方法中插入时间记录和日志输出逻辑。这是实现Java应用全局监控的核心步骤。

1. 在 preHandle 开始位置记录API开始时间:

@Override
public boolean preHandle(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) {
    MdcUtils.setStartTime(); // <-- 新增:记录请求开始时间
    // ... 原有的 traceId, userId 等用户信息设置逻辑 ...
    return true;
}

2. 在 postHandle 中计算请求耗时:
postHandle 方法在处理器执行后、视图渲染前被调用,是计算耗时的合适位置。

@Override
public void postHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
                       @NotNull Object handler, @NotNull ModelAndView modelAndView) {
    // 请求处理完成后计算耗时
    MdcUtils.setRequestTime(); // <-- 新增:计算并设置耗时
}

3. 在 afterCompletion 中输出监控日志并清理:
afterCompletion 在整个请求结束后调用,无论成功或异常。在这里输出最终日志并清理MDC,防止内存泄漏。

@Override
public void afterCompletion(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
                            @NotNull Object handler, @Nullable Exception ex) {
    log.info("API [{}] process finished, request time [{}]ms, response code [{}]",
            request.getRequestURI(), MdcUtils.getRequestTime(), response.getStatus()); // <-- 输出监控日志
    // 请求完全结束后清理MDC,防止内存泄漏
    MdcUtils.clearAll();
}

完整拦截器代码示例:

package com.sandwich.logtracing.interceptor;

import com.sandwich.logtracing.util.MdcUtils;
import com.sandwich.logtracing.util.RandomStrUtils;
import com.sandwich.logtracing.util.ThreadLocalContext;
import io.micrometer.common.util.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Slf4j
@Component
public class MDCInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) {
        MdcUtils.setStartTime();
        // 从header中获取用户信息
        String traceId = request.getHeader("x-request-correlation-id");
        //if the request header don't have a trace id,then generate a random one
        if (StringUtils.isBlank(traceId)) {
            traceId = RandomStrUtils.generateRandomString(15);
        }
        MdcUtils.setTraceId(traceId);
        String userId = request.getHeader("X-User-ID");
        if (StringUtils.isNotBlank(userId)) {
            // 添加用户ID到MDC
            MdcUtils.setUserId(userId);
        }
        String username = request.getHeader("X-Username");

        if (StringUtils.isNotBlank(username)) {
            // 添加用户名到MDC
            MdcUtils.setUserName(username);
        }

        // 可以添加其他请求相关信息
        String clientIp = getClientIp(request);
        MdcUtils.setClientIp(clientIp);

        ThreadLocalContext.setTraceId(traceId);
        ThreadLocalContext.setUserId(userId);
        ThreadLocalContext.setUserName(username);
        ThreadLocalContext.setClientIp(clientIp);

        return true;
    }

    @Override
    public void postHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
                           @NotNull Object handler, @NotNull ModelAndView modelAndView) {
        // 请求处理完成后可以添加响应信息
        MdcUtils.setRequestTime();
    }

    @Override
    public void afterCompletion(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
                                @NotNull Object handler, @Nullable Exception ex) {
        log.info("API [{}] process finished, request time [{}]ms, response code [{}]",
                request.getRequestURI(), MdcUtils.getRequestTime(), response.getStatus());
        // 请求完全结束后清理MDC,防止内存泄漏
        MdcUtils.clearAll();
    }

    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

1.3 优化日志格式(可选)

为了让包含用户信息的日志输出更整齐,我们可以微调一下application.yml中的日志配置模式。这里主要是调整了MDC字段的输出顺序,使其左对齐更美观。

spring:
  application:
    name: log-tracing

# 日志配置
logging:
  level:
    com.sandwich.logtracing: DEBUG
    org.springframework: INFO
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} %X{traceId:-} %replace(%X{userId}){'^(?!$)(.+)$','[userId: $1] '}%replace(%X{username}){'^(?!$)(.+)$','[username: $1] '}%replace(%X{clientIp}){'^(?!$)(.+)$','[clientIp: $1] '}[%thread] %-5level %logger{50} - %msg%n"
    file: "%d{yyyy-MM-dd HH:mm:ss.SSS} %X{traceId:-} %replace(%X{userId}){'^(?!$)(.+)$','[userId: $1] '}%replace(%X{username}){'^(?!$)(.+)$','[username: $1] '}%replace(%X{clientIp}){'^(?!$)(.+)$','[clientIp: $1] '}[%thread] %-5level %logger{50} - %msg%n"
  file:
    name: ./logs/log-tracing.log
  logback:
    rollingpolicy:
      max-file-size: 100MB
      max-history: 30

注意:API耗时信息是通过日志消息(%msg)输出的,并未直接嵌入到日志模式中。这样做灵活性更高,修改起来也更方便。

2. 效果验证

部署代码后,我们发起一个登录请求进行测试。

1. 客户端请求:

$ ./login.sh
Generated traceId from client side: A4LnbakJlqHVcMl
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   144    0   100  100    44    121     53 --:--:-- --:--:-- --:--:--   175
Response from login API:
{
    "responseCode": 200,
    "message": "success",
    "data": "Sandwich login success",
    "traceId": "A4LnbakJlqHVcMl"
}

2. 服务端日志分析:
我们在日志中追踪 traceId: A4LnbakJlqHVcMl,可以看到请求链路中的所有步骤。重点关注最后一行由拦截器输出的监控日志:

2025-09-17 22:30:03.456 A4LnbakJlqHVcMl [userId: 123456] [username: Sandwich] [clientIp: 0:0:0:0:0:0:0:1] [http-nio-8080-exec-2] INFO  c.sandwich.logtracing.interceptor.MDCInterceptor - API [/test/login] process finished, request time [735]ms, response code [200]

这行日志清晰地告诉我们:

  • API接口/test/login
  • 请求耗时735ms
  • 响应状态码200

所有信息一目了然,无需任何人工计算。由于这是在全局拦截器中实现的,因此每一个经过该拦截器的API请求都会自动生成这样的监控日志。

3. 总结与展望

通过本次改造,我们成功为Spring Boot应用添加了轻量级、无侵入的全局API性能监控能力。核心是利用拦截器的生命周期,配合MDC存储临时数据,最终在请求结束时输出关键性能指标。

这种方法有几个优点:

  1. 全局自动:无需在每个Controller方法里手动添加计时代码。
  2. 信息完整:天然关联了traceId、用户信息、耗时、状态码。
  3. 低开销:仅增加少量时间记录和一次日志打印操作。
  4. 灵活:监控日志格式和内容可以方便地在拦截器中调整。

这个监控数据是后续进行性能分析、慢接口排查的基础。你可以直接将日志采集到ELK(Elasticsearch, Logstash, Kibana)或Graylog等日志平台,通过traceIdURI进行聚合查询和统计分析,快速定位性能瓶颈。

更进一步,你还可以将此数据推送至时序数据库(如InfluxDBPrometheus),并利用Grafana绘制实时的API耗时仪表盘,构建更直观的可观测性体系。这正是在复杂后端 & 架构中保障系统稳定性的常见实践。

希望这个实战案例能为你实现应用监控提供清晰的思路。如果你有更好的实现建议或遇到其他问题,欢迎在云栈社区的技术论坛与我们交流讨论。




上一篇:Redis生产环境15条实用规约:避坑、提效与系统稳定指南
下一篇:突发:Unity引擎安全漏洞致《永恒之柱II》等多款游戏从Steam下架
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-11 10:07 , Processed in 0.771422 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表