当前位置: 首页 > 后端技术 > Java

如何跟踪SpringMVC接口的请求响应

时间:2023-04-01 19:31:39 Java

有些业务需求需要跟踪我们的接口访问,即记录请求和响应。基本记录维度包括请求入参(路径查询参数、请求体)、请求路径(uri)、请求方法(method)、请求头(headers)、响应状态、响应头,甚至敏感的响应体等等。.今天总结了几种方法,大家可以根据自己的需要选择。请求跟踪的实现方法网关层很多网关设施都有httptrace的功能,可以帮助我们集中记录请求流量。Orange、Kong、ApacheApisix等基于Nginx的网关都具备这种能力,甚至Nginx本身也提供了记录httptrace日志的能力。优点是httptrace日志可以集中管理,免开发;缺点是技术要求高,需要分发、存储、查询等配套设施。SpringBootActuator其实在SpringBoot中提供了一个简单的跟踪功能。你只需要整合::exposure:include:'httptrace',可以通过http://server:port/actuator/httptrace获取最新的Http请求信息。但在最新版本中,可能需要显式声明这些跟踪信息的存储方式,即实现HttpTraceRepository接口,注入SpringIoC。例如,将其放入内存并将其限制为最近的100个条目(不建议用于生产用途):@BeanpublicHttpTraceRepositoryhttpTraceRepository(){returnnewInMemoryHttpTraceRepository();}Tracelogs以json格式呈现:记录不多,当然如果够用的话可以试试。优点是易于集成,几乎免开发;缺点是记录的维度不多,需要搭建缓冲和消费这些日志信息的设施。CommonsRequestLoggingFilterSpringWeb模块还提供了一个过滤器CommonsRequestLoggingFilter,它可以记录请求的细节。配置也比较简单:@BeanCommonsRequestLoggingFilterloggingFilter(){CommonsRequestLoggingFilterloggingFilter=newCommonsRequestLoggingFilter();//记录客户端IP信息loggingFilter.setIncludeClientInfo(true);//记录请求头loggingFilter.setIncludeHeaders(true);//如果logging为请求头,可以指定哪些记录,哪些不记录//loggingFilter.setHeaderPredicate();//记录请求体,特别是POST请求的body参数loggingFilter.setIncludePayload(true);//请求体的大小限制默认为50loggingFilter.setMaxPayloadLength(10000);//记录请求路径中的查询参数loggingFilter.setIncludeQueryString(true);returnloggingFilter;}并且必须打开CommonsRequestLoggingFilter的调试日志:logging:level:org:springframework:web:filter:CommonsRequestLoggingFilter:debugonce请求会输出两次日志,一次在第一次通过过滤器之前;完成过滤器链后一次。这里多说一句,其实可以转化为输出json格式。优点是配置灵活,请求跟踪维度全面。缺点是只记录请求,不记录响应。ResponseBodyAdviceSpringBoot统一返回体其实是可以记录的,需要自己实现。这里借用了CommonsRequestLoggingFilter的方法来解析请求。响应体也可以获取,但是由于生命周期的关系,响应头和状态不明确,所以不清楚这里获取是否合适,但这是一个思路。/***@authorfelord.cn*@since1.0.8.RELEASE*/@Slf4j@RestControllerAdvice(basePackages={"cn.felord.logging"})publicclassRestBodyAdviceimplementsResponseBodyAdvice{privatestaticfinalintDEFAULT_MAX_PAYLOAD_LENGTH复制代码=10000;publicstaticfinalStringREQUEST_MESSAGE_PREFIX="请求[";publicstaticfinalStringREQUEST_MESSAGE_SUFFIX="]";privateObjectMapperobjectMapper=newObjectMapper();@Overridepublicbooleansupports(MethodParameterreturnType,Class>converterType){returntrue;}@SneakyThrows@OverridepublicObjectbeforeBodyWrite(Objectbody,MethodParameterreturnType,MediaTypeselectedContentType,Class>selectedConverterType,ServerHttpRequest请求,ServerHttpResponse响应){ServletServerHttpRequestservletServerHttpRequest=(ServletServerHttpRequest)请求;log.debug(createRequestMessage(servletServerHttpRequest.getServletRequest(),REQUEST_MESSAGE_PREFIX,REQUEST_MESSAGE_SUFFIX));休息<对象>objectRest;if(body==null){objectRest=RestBody.okData(Collections.emptyMap());}elseif(Rest.class.isAssignableFrom(body.getClass())){objectRest=(Rest)body;}elseif(checkPrimitive(body)){returnRestBody.okData(Collections.singletonMap("result",body));}else{objectRest=RestBody.okData(body);}log.debug("响应体["+objectMapper.writeValueAsString(objectRest)+"]");返回对象休息;}privatebooleancheckPrimitive(Objectbody){Classclazz=body.getClass();返回clazz.isPrimitive()||clazz.isArray()||Collection.class.isAssignableFrom(clazz)||bodyinstanceofNumber||bodyinstanceof布尔值||bodyinstanceof字符||字符串的主体实例;}protectedStringcreateRequestMessage(HttpServletRequestrequest,Stringprefix,Stringsuffix){StringBuildermsg=newStringBuilder();msg.append(前缀);msg.append(request.getMethod()).append("");msg.append(request.getRequestURI());字符串查询字符串=request.getQueryString();if(queryString!=null){msg.append('?').append(queryString);}Stringclient=request.getRemoteAddr();如果(StringUtils.hasLength(client)){msg.append(",client=").append(client);}HttpSessionsession=request.getSession(false);如果(会话!=空){msg.append(",session=").append(session.getId());}Stringuser=request.getRemoteUser();if(user!=null){msg.append(",user=").append(user);}HttpHeadersheaders=newServletServerHttpRequest(request).getHeaders();msg.append(",headers=").append(headers);Stringpayload=getMessagePayload(请求);if(payload!=null){msg.append(",payload=").append(payload);}msg.append(后缀);返回msg.toString();}protectedStringgetMessagePayload(HttpServletRequestrequest){ContentCachingRequestWrapperwrapper=WebUtils.getNativeRequest(request,ContentCachingRequestWrapper.class);if(wrapper!=null){byte[]buf=wrapper.getContentAsByteArray();如果(buf.length>0){intlength=Math.min(buf.length,DEFAULT_MAX_PAYLOAD_LENGTH);尝试{returnnewString(buf,0,length,wrapper.getCharacterEncoding());}catch(UnsupportedEncodingExceptionex){返回“[未知]”;}}}返回空值;}}不要忘记配置ResponseBodyAdvice的logginglevel为DEBUGlogstash-logback-encoder这是logstash的logbackencoder,可以将httptrace结构化的输出为json。简介:net.logstash.logbacklogstash-logback-encoder6.6添加一个ConsoleAppender作为LogstashEncoder到logback配置:然后也实现一个解析过的Filter:importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.slf4j.MDC;导入org.springframework.core.annotation.Order;导入org.springframework.stereotype.Component;导入javax.servlet.*;导入javax.servlet.http.HttpServletRequest;导入javax.servlet.http.HttpServletResponse;导入java.io.IOException;importjava.util.UUID;/***@authorfelord.cn*@since1.0.8.RELEASE*/@Order(1)@ComponentpublicclassMDCFilterimplementsFilter{privatefinalLoggerLOGGER=LoggerFactory.getLogger(MDCFilter.class);privatefinalStringX_REQUEST_ID="X-Request-ID";@OverridepublicvoiddoFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain)throwsIOException,ServletException{HttpServletRequestreq=(HttpServletRequest)请求;HttpServletResponseres=(HttpServletResponse)响应;尝试{addXRequestId(req);LOGGER.info("路径:{},方法:{},查询{}",req.getRequestURI(),req.getMethod(),req.getQueryString());res.setHeader(X_REQUEST_ID,MDC.get(X_REQUEST_ID));chain.doFilter(请求,响应);}finally{LOGGER.info("statusCode{},path:{},method:{},query{}",res.getStatus(),req.getRequestURI(),req.getMethod(),req.getQueryString());MDC.clear();}}privatevoidaddXRequestId(HttpServletRequestrequest){StringxRequestId=request.getHeader(X_REQUEST_ID);if(xRequestId==null){MDC.put(X_REQUEST_ID,UUID.randomUUID().toString());}else{MDC.put(X_REQUEST_ID,xRequestId);}}}这里的解析方式其实可以更细化。不仅可以记录接口请求日志,还可以结构化为json:{"@timestamp":"2021-08-10T23:48:51.322+08:00","@version":"1","message":"statusCode200,路径:/log/get,方法:GET,查询foo=xxx&bar=ooo","logger_name":"cn.felord.logging.MDCFilter","thread_name":"http-nio-8080-exec-1","level":"INFO","level_value":20000,"X-Request-ID":"7c0db56c-b1f2-4d85-ad9a-7ead67660f96"}总结一下,今天介绍了很多记录和跟踪接口请求响应的方法,比较简单。如果你的Linktracking可能会在项目做大的时候用到,以后有机会再把这个坑填上。当然,也许你有更好的方法,欢迎留言分享。关注公众号:Felordcn获取更多资讯个人博客:https://felord.cn