基于SpringMVC拦截器实现接口耗时监控功能
作者:CIT
本文呢主要介绍了基于SpringMVC拦截器实现的接口耗时监控功能,统计接口的耗时情况属于一个可以复用的功能点,因此这里直接使用 SpringMVC的HandlerInterceptor拦截器来实现,需要的朋友可以参考下
前言
在日常的项目开发过程中,后端开发人员应该主动去关心自己的接口性能。这种关心需要量化,而量化的直接方式就是对接口的响应时间进行监控,以了解系统性能,帮助判断性能瓶颈。本文基于已有的全链路日志系统进一步补充了接口耗时的方案。已有的全链路日志系统是围绕ELK+Jaeger构建起来的,在Spring Cloud微服务架构中,可以实现跨服务的请求日志追踪 ,帮助我们进行线上问题排查。
服务告警部分则是通过Frostmourne平台来实现了,该平台可以接入Elasticsearch,配置相关的项目监控与告警。当监控到接口超时以后,可以通过接口超时日志中的traceId,在Jaeger平台上查看整个请求链路的耗时分布,快速明确问题发生的位置,提升问题发现与响应的速度。
实现
基本介绍
统计接口的耗时情况属于一个可以复用的功能点,因此这里直接使用 SpringMVC的HandlerInterceptor拦截器来实现,后续抽取成一个公共组件,方便复用。
拦截器接口 HandlerInterceptor
提供了三个方法来实现对请求前、请求后,响应后进行自定义处理,并且拦截器的前置处理和后置处理是具体关联性的。
- preHandle() :在 Controller 方法执行之前执行。即在 HandlerMapping 确定适当的处理程序对象之后调用,但在HandlerAdapter 调用处理程序之前调用。
- postHandle() :在 Controller 方法执行之后执行。即在 HandlerAdapter 实际调用处理程序之后,但在DispatcherServlet 呈现视图之前调用。
- afterCompletion() :完成请求处理后(即渲染视图之后)的回调。 将在处理程序执行的任何结果上被调用,从而允许适当的资源清理。
实现思路
要统计接口处理请求的时长,可以在拦截器的 preHandle() 方法记录请求开始时间(startTime),在 afterCompletion() 方法中记录请求处理完后的结束时间(endTime),请求处理时间(响应时间) = 结束时间 - 开始时间。
实现过程
- 定义一个拦截器
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++; } } }
- 配置拦截器使其生效
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 ******************"); } }
- 添加logback配置,在开发和测试环境由于流量小,可以通过TCP监听的方式直接将接口的耗时日志传输至logstash,生产环境最好还是通过filebeat监听日志文件的方式去实现。
<?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>
- 配置开发、测试环境的logstash传输耗时日志
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}" } }
- 查看耗时结果
- 配置耗时监控与结果验证
以上就是基于SpringMVC拦截器实现的接口耗时监控功能的详细内容,更多关于SpringMVC接口耗时监控的资料请关注脚本之家其它相关文章!