基于SpringMVC攔截器實(shí)現(xiàn)接口耗時(shí)監(jiān)控功能
前言
在日常的項(xiàng)目開(kāi)發(fā)過(guò)程中,后端開(kāi)發(fā)人員應(yīng)該主動(dòng)去關(guān)心自己的接口性能。這種關(guān)心需要量化,而量化的直接方式就是對(duì)接口的響應(yīng)時(shí)間進(jìn)行監(jiān)控,以了解系統(tǒng)性能,幫助判斷性能瓶頸。本文基于已有的全鏈路日志系統(tǒng)進(jìn)一步補(bǔ)充了接口耗時(shí)的方案。已有的全鏈路日志系統(tǒng)是圍繞ELK+Jaeger構(gòu)建起來(lái)的,在Spring Cloud微服務(wù)架構(gòu)中,可以實(shí)現(xiàn)跨服務(wù)的請(qǐng)求日志追蹤 ,幫助我們進(jìn)行線上問(wèn)題排查。
服務(wù)告警部分則是通過(guò)Frostmourne平臺(tái)來(lái)實(shí)現(xiàn)了,該平臺(tái)可以接入Elasticsearch,配置相關(guān)的項(xiàng)目監(jiān)控與告警。當(dāng)監(jiān)控到接口超時(shí)以后,可以通過(guò)接口超時(shí)日志中的traceId,在Jaeger平臺(tái)上查看整個(gè)請(qǐng)求鏈路的耗時(shí)分布,快速明確問(wèn)題發(fā)生的位置,提升問(wèn)題發(fā)現(xiàn)與響應(yīng)的速度。
實(shí)現(xiàn)
基本介紹
統(tǒng)計(jì)接口的耗時(shí)情況屬于一個(gè)可以復(fù)用的功能點(diǎn),因此這里直接使用 SpringMVC的HandlerInterceptor攔截器來(lái)實(shí)現(xiàn),后續(xù)抽取成一個(gè)公共組件,方便復(fù)用。
攔截器接口 HandlerInterceptor
提供了三個(gè)方法來(lái)實(shí)現(xiàn)對(duì)請(qǐng)求前、請(qǐng)求后,響應(yīng)后進(jìn)行自定義處理,并且攔截器的前置處理和后置處理是具體關(guān)聯(lián)性的。
- preHandle() :在 Controller 方法執(zhí)行之前執(zhí)行。即在 HandlerMapping 確定適當(dāng)?shù)奶幚沓绦驅(qū)ο笾笳{(diào)用,但在HandlerAdapter 調(diào)用處理程序之前調(diào)用。
- postHandle() :在 Controller 方法執(zhí)行之后執(zhí)行。即在 HandlerAdapter 實(shí)際調(diào)用處理程序之后,但在DispatcherServlet 呈現(xiàn)視圖之前調(diào)用。
- afterCompletion() :完成請(qǐng)求處理后(即渲染視圖之后)的回調(diào)。 將在處理程序執(zhí)行的任何結(jié)果上被調(diào)用,從而允許適當(dāng)?shù)馁Y源清理。
實(shí)現(xiàn)思路
要統(tǒng)計(jì)接口處理請(qǐng)求的時(shí)長(zhǎng),可以在攔截器的 preHandle() 方法記錄請(qǐng)求開(kāi)始時(shí)間(startTime),在 afterCompletion() 方法中記錄請(qǐng)求處理完后的結(jié)束時(shí)間(endTime),請(qǐng)求處理時(shí)間(響應(yīng)時(shí)間) = 結(jié)束時(shí)間 - 開(kāi)始時(shí)間。
實(shí)現(xiàn)過(guò)程
- 定義一個(gè)攔截器
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; ? /** * 攔截器,統(tǒng)計(jì)接口耗時(shí) */ @Slf4j public class TimeConsumingInterceptor implements HandlerInterceptor { ? @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 記錄請(qǐng)求開(kāi)始時(shí)間 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 { // 請(qǐng)求結(jié)束時(shí)間 Long endTime = System.currentTimeMillis(); try { // 從HttpServletRequest獲取開(kāi)始時(shí)間 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); // 打印接口信息及耗時(shí) 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 請(qǐng)求對(duì)象{@link HttpServletRequest} * @return 完整的URL路徑 */ private String getFullUrl(HttpServletRequest request) { //記錄請(qǐng)求參數(shù) StringBuilder sb = new StringBuilder(); String method = request.getMethod(); sb.append(method).append(" "); sb.append(request.getRequestURL().toString()); if (RequestMethod.POST.name().equals(method)) { //獲取參數(shù) 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配置,在開(kāi)發(fā)和測(cè)試環(huán)境由于流量小,可以通過(guò)TCP監(jiān)聽(tīng)的方式直接將接口的耗時(shí)日志傳輸至logstash,生產(chǎn)環(huán)境最好還是通過(guò)filebeat監(jiān)聽(tīng)日志文件的方式去實(shí)現(xiàn)。
<?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"> <!--可以訪問(wèn)的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>
- 配置開(kāi)發(fā)、測(cè)試環(huán)境的logstash傳輸耗時(shí)日志
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}" } }
- 查看耗時(shí)結(jié)果
- 配置耗時(shí)監(jiān)控與結(jié)果驗(yàn)證
以上就是基于SpringMVC攔截器實(shí)現(xiàn)的接口耗時(shí)監(jiān)控功能的詳細(xì)內(nèi)容,更多關(guān)于SpringMVC接口耗時(shí)監(jiān)控的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java中try-catch的使用及注意細(xì)節(jié)
現(xiàn)在有很多的語(yǔ)言都支持try-catch,比如常見(jiàn)的就是c++,java等,這篇文章主要給大家介紹了關(guān)于Java中try-catch的使用及注意細(xì)節(jié)的相關(guān)資料,文中通過(guò)圖文以及實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06解決JAVA8 Collectors.toMap value為null報(bào)錯(cuò)的問(wèn)題
這篇文章主要介紹了解決JAVA8 Collectors.toMap value為null報(bào)錯(cuò)的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-01-01java LocalDateTime加時(shí)間,計(jì)算兩個(gè)時(shí)間的差方式
文章介紹了如何在Java中使用LocalDateTime類添加時(shí)間并計(jì)算兩個(gè)時(shí)間的差值,通過(guò)比較來(lái)總結(jié)個(gè)人經(jīng)驗(yàn),并鼓勵(lì)讀者參考和支持腳本之家2025-03-03Java字符串格式化功能?String.format用法詳解
String類的format()方法用于創(chuàng)建格式化的字符串以及連接多個(gè)字符串對(duì)象,熟悉C語(yǔ)言的同學(xué)應(yīng)該記得C語(yǔ)言的sprintf()方法,兩者有類似之處,format()方法有兩種重載形式2024-09-09SpringBoot使用Jasypt對(duì)配置文件和數(shù)據(jù)庫(kù)密碼加密
在做數(shù)據(jù)庫(kù)敏感信息保護(hù)時(shí),應(yīng)加密存儲(chǔ),本文就來(lái)介紹一下SpringBoot使用Jasypt對(duì)配置文件和數(shù)據(jù)庫(kù)密碼加密,具有一定的參考價(jià)值,感興趣的可以了解一下2024-02-02SpringBoot和Vue2項(xiàng)目配置https協(xié)議過(guò)程
本文詳細(xì)介紹了SpringBoot項(xiàng)目和Vue2項(xiàng)目的部署流程及SSL證書(shū)配置,對(duì)于SpringBoot項(xiàng)目,需將.pfx文件放入resources目錄并配置server,然后打包部署,Vue2項(xiàng)目中,涉及檢查nginx的SSL模塊、編譯新的nginx文件2024-10-10如何從eureka獲取服務(wù)的ip和端口號(hào)進(jìn)行Http的調(diào)用
這篇文章主要介紹了如何從eureka獲取服務(wù)的ip和端口號(hào)進(jìn)行Http的調(diào)用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03springboot CompletableFuture并行計(jì)算及使用方法
CompletableFuture基于 Future 和 CompletionStage 接口,利用線程池、回調(diào)函數(shù)、異常處理、組合操作等機(jī)制,提供了強(qiáng)大而靈活的異步編程功能,這篇文章主要介紹了springboot CompletableFuture并行計(jì)算及使用方法,需要的朋友可以參考下2024-05-05淺談Java設(shè)計(jì)模式之開(kāi)放封閉原則
本篇文章主要介紹了淺談Java設(shè)計(jì)模式之開(kāi)放封閉原則,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-03-03