基于SpringMVC拦截器的接口耗时监控方案

前言

在日常的项目开发过程中,后端开发人员应该主动去关心自己的接口性能。这种关心需要量化,而量化的直接方式就是对接口的响应时间进行监控,以了解系统性能,帮助判断性能瓶颈。本文基于已有的全链路日志系统进一步补充了接口耗时的方案。已有的全链路日志系统是围绕ELK+Jaeger构建起来的,在Spring Cloud微服务架构中,可以实现跨服务的请求日志追踪 ,帮助我们进行线上问题排查。

服务告警部分则是通过Frostmourne平台来实现了,该平台可以接入Elasticsearch,配置相关的项目监控与告警。当监控到接口超时以后,可以通过接口超时日志中的traceId,在Jaeger平台上查看整个请求链路的耗时分布,快速明确问题发生的位置,提升问题发现与响应的速度。

实现

基本介绍

统计接口的耗时情况属于一个可以复用的功能点,因此这里直接使用 SpringMVC的HandlerInterceptor拦截器来实现,后续抽取成一个公共组件,方便复用。

拦截器接口 HandlerInterceptor 提供了三个方法来实现对请求前、请求后,响应后进行自定义处理,并且拦截器的前置处理和后置处理是具体关联性的。

  • preHandle() :在 Controller 方法执行之前执行。即在 HandlerMapping 确定适当的处理程序对象之后调用,但在HandlerAdapter 调用处理程序之前调用。
  • postHandle() :在 Controller 方法执行之后执行。即在 HandlerAdapter 实际调用处理程序之后,但在DispatcherServlet 呈现视图之前调用。
  • afterCompletion() :完成请求处理后(即渲染视图之后)的回调。 将在处理程序执行的任何结果上被调用,从而允许适当的资源清理。

实现思路

要统计接口处理请求的时长,可以在拦截器的 preHandle() 方法记录请求开始时间(startTime),在 afterCompletion() 方法中记录请求处理完后的结束时间(endTime),请求处理时间(响应时间) = 结束时间 - 开始时间。

实现过程

  1. 定义一个拦截器
java 复制代码
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.extra.servlet.ServletUtil;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
​
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
​
/**
 * 拦截器,统计接口耗时
 */
@Slf4j
public class TimeConsumingInterceptor implements HandlerInterceptor {
​
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 记录请求开始时间
        request.setAttribute("_startTime", System.currentTimeMillis());
        return true;
    }
​
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
        // no need to override
    }
​
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        // 请求结束时间
        Long endTime = System.currentTimeMillis();
        try {
            // 从HttpServletRequest获取开始时间
            Long startTime = (long) request.getAttribute("_startTime");
            String clientIP = ServletUtil.getClientIP(request, "");
            String fullUrl = getFullUrl(request);
            Long cost = endTime - startTime;
            MDC.put("cost_time", cost.toString());
            MDC.put("request_url", fullUrl);
            MDC.put("client_ip", clientIP);
            // 打印接口信息及耗时
            log.info("client IP {}, url {}, cost {}ms", clientIP, fullUrl, cost);
        } catch (Exception e) {
            log.error("fail to calculate time cost", e);
        } finally {
            MDC.remove("cost_time");
            MDC.remove("request_url");
            MDC.remove("client_ip");
        }
    }
​
    /**
     * 获取完整的URL路径
     *
     * @param request 请求对象{@link HttpServletRequest}
     * @return 完整的URL路径
     */
    private String getFullUrl(HttpServletRequest request) {
        //记录请求参数
        StringBuilder sb = new StringBuilder();
        String method = request.getMethod();
        sb.append(method).append(" ");
        sb.append(request.getRequestURL().toString());
        if (RequestMethod.POST.name().equals(method)) {
            //获取参数
            Map<String, String[]> pm = request.getParameterMap();
            Set<Map.Entry<String, String[]>> es = pm.entrySet();
            Iterator<Map.Entry<String, String[]>> iterator = es.iterator();
            appendPathVariable(iterator, sb);
        }
        return sb.toString();
    }
​
    private void appendPathVariable(Iterator<Map.Entry<String, String[]>> iterator, StringBuilder sb) {
        int pointer = 0;
        while (iterator.hasNext()) {
            if (pointer == 0) {
                sb.append("?");
            } else {
                sb.append("&");
            }
            Map.Entry<String, String[]> next = iterator.next();
            String key = next.getKey();
            String[] value = next.getValue();
            for (int i = 0; i < value.length; i++) {
                if (i != 0) {
                    sb.append("&");
                }
                if (value[i].length() <= 20) {
                    sb.append(key).append("=").append(value[i]);
                } else {
                    sb.append(key).append("=").append(CharSequenceUtil.subPre(value[i], 20)).append("...");
                }
            }
            pointer++;
        }
    }
}
  1. 配置拦截器使其生效
java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
​
@Slf4j
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
​
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TimeConsumingInterceptor())
                // 需拦截的URI配置
                .addPathPatterns("/**")
                // 不需拦截的URI配置
                .excludePathPatterns("/swagger/**", "/static/**", "/resource/**");
        log.info("***************** ADD TIME CONSUMING INTERCEPTOR  ******************");
    }
}
  1. 添加logback配置,在开发和测试环境由于流量小,可以通过TCP监听的方式直接将接口的耗时日志传输至logstash,生产环境最好还是通过filebeat监听日志文件的方式去实现。
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <contextName>log</contextName>
​
    <property name="logback.logDir" value="${LOG_PATH}"/>
    <springProperty name="logback.appName" scope="context" source="spring.application.name"/>
    <springProperty name="logback.elastic" scope="context" source="logback.elastic"/>
    <springProperty name="env" scope="context" source="spring.profiles.active"/>
    <springProperty name="serverIP" scope="context" source="spring.cloud.client.ip-address" defaultValue="0.0.0.0"/>
    <property name="commonLayoutPattern"
              value="[${serverIP}] %d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} [%mdc{trace_id:-N/A}] ${LOG_LEVEL_PATTERN:-%p} ${PID:- } --- [%t] %logger{39}.%method[%line] : %m%n"/>
​
    <appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
        <!--展示格式 layout -->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>${commonLayoutPattern}</pattern>
        </layout>
    </appender>
​
    <appender name="logStash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
        <!--可以访问的logstash日志收集端口-->
        <destination>192.168.xxx.xxx:4560</destination>
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <timestamp>
                    <timeZone>Asia/Shanghai</timeZone>
                </timestamp>
                <pattern>
                    <pattern>
                        {
                        "appName": "${logback.appName}-${env}",
                        "serverIP": "${serverIP}",
                        "traceId":"%mdc{trace_id:-N/A}",
                        "requestUrl":"%mdc{request_url:-N/A}",
                        "clientIP":"%mdc{client_ip:-N/A}",
                        "costTime": "%mdc{cost_time:-N/A}"
                        }
                    </pattern>
                </pattern>
            </providers>
        </encoder>
    </appender>
​
    <springProfile name="dev,pre">
        <logger name="com.xxx.xxx.log.autoconfigure.TimeConsumingInterceptor" additivity="false">
            <appender-ref ref="logStash"/>
            <appender-ref ref="consoleLog"/>
            <appender-ref ref="fileRequestLog"/>
        </logger>
    </springProfile>
​
    <springProfile name="prod">
        <logger name="com.xxx.xxx.log.autoconfigure.TimeConsumingInterceptor" additivity="false">
            <appender-ref ref="fileRequestLog"/>
        </logger>
    </springProfile>
​
    <appender name="asyncRequestLog" class="ch.qos.logback.classic.AsyncAppender">
        <discardingThreshold>0</discardingThreshold>
        <queueSize>1024</queueSize>
        <appender-ref ref="fileRequestLog"/>
    </appender>
</configuration>
  1. 配置开发、测试环境的logstash传输耗时日志
schema 复制代码
input {
  tcp {
    host => "192.168.xxx.xxx"
    port => 4560
    codec => json_lines
  }
}
​
​
filter {
  mutate {
    convert => {
      "costTime" => "integer"
    }
  }
}
​
output {
  elasticsearch {
    hosts => ["http://192.168.xxx.xxx:9200"]
    index => "request-%{[appName]}-%{+YYYY.MM.dd}"
    }
}
  1. 查看耗时结果

6. 配置耗时监控与结果验证

参考

www.elastic.co/guide/en/lo...

github.com/AutohomeCor...

blog.csdn.net/ffyyhh99551...

blog.csdn.net/u013845177/...

blog.csdn.net/QIU17616165...

相关推荐
jessecyj26 分钟前
SpringBoot详解
java·spring boot·后端
爱吃的小肥羊33 分钟前
刚刚!Claude最强大模型泄露,Anthropic紧急封锁
后端
qqty121734 分钟前
Spring Boot管理用户数据
java·spring boot·后端
bearpping1 小时前
SpringBoot最佳实践之 - 使用AOP记录操作日志
java·spring boot·后端
一叶飘零_sweeeet1 小时前
线上故障零扩散:全链路监控、智能告警与应急响应 SOP 完整落地指南
java·后端·spring
开心就好20253 小时前
不同阶段的 iOS 应用混淆工具怎么组合使用,源码混淆、IPA混淆
后端·ios
架构师沉默3 小时前
程序员如何避免猝死?
java·后端·架构
椰奶燕麦3 小时前
Windows PackageManager (winget) 核心故障排错与通用修复指南
后端
zjjsctcdl3 小时前
springBoot发布https服务及调用
spring boot·后端·https
zdl6864 小时前
Spring Boot文件上传
java·spring boot·后端