当前位置: 首页 > 科技观察

SpringBoot常见的记录请求响应日志的方式

时间:2023-03-14 13:14:45 科技观察

有些业务需求需要跟踪我们的接口访问,即记录请求和响应。基本记录维度包括请求入参(路径查询参数、请求体)、请求路径(uri)、请求方法(method)、请求头(headers)、响应状态、响应头,甚至敏感的响应体等等。.今天总结了几种方法,大家可以根据自己的需要选择。请求跟踪的实现方法网关层很多网关设施都有httptrace的功能,可以帮助我们集中记录请求流量。Orange、Kong、ApacheApisix等基于Nginx的网关都具备这种能力,甚至Nginx本身也提供了记录httptrace日志的能力。优点是无需开发即可集中管理httptrace日志;缺点是技术要求高,需要分发、存储、查询等配套设施。SpringBootActuator其实在SpringBoot中提供了一个简单的跟踪功能。你只需要集成:org.springframework.bootspring-boot-starter-actuator打开/actuator/httptrace:management:endpoints:web:exposure:include:'httptrace',可以通过http://server:port/actuator/httptrace获取最新的Http请求信息。但在最新版本中,可能需要显式声明这些跟踪信息的存储方式,即实现HttpTraceRepository接口,注入SpringIoC。比如放入内存,限制为最新100条(不推荐生产使用):@BeanpublicHttpTraceRepositoryhttpTraceRepository(){returnnewInMemoryHttpTraceRepository();}Trace日志以json格式呈现:SpringBoot记录的httptrace记录的维度actuator不多,当然如果够用的话可以试试。优点是易于集成,几乎免开发;缺点是记录的维度不多,需要搭建缓冲和消费这些日志信息的设施。CommonsRequestLoggingFilterSpringWeb模块还提供了一个过滤器CommonsRequestLoggingFilter,它可以记录请求的细节。配置也比较简单:@BeanCommonsRequestLoggingFilterloggingFilter(){CommonsRequestLoggingFilterloggingFilter=newCommonsRequestLoggingFilter();//记录客户端IP信息loggingFilter.setIncludeClientInfo(true);//记录请求头loggingFilter.setIncludeHeaders(true);//如果记录请求头,哪些记录可以指定,哪些不记录//loggingFilter.setHeaderPredicate();//记录请求体,特别是POST请求的body参数loggingFilter.setIncludePayload(true);//请求体的大小限制defaultsto50loggingFilter.setMaxPayloadLength(10000);//记录请求路径中的查询参数:CommonsRequestLoggingFilter:debug会为一个请求输出两次Logs,一次在第一次通过过滤器之前;完成过滤器链后一次。CommonsRequestLoggingFilter记录请求日志这里再多说一句其实可以转化为输出json格式。优点是配置灵活,请求跟踪维度全面。缺点是只记录请求,不记录响应。ResponseBodyAdviceSpringBoot统一返回体其实是可以记录的,需要自己实现。这里借用了CommonsRequestLoggingFilter的方法来解析请求。响应体也可以获取,但是由于生命周期的关系,响应头和状态不明确,所以不清楚这里获取是否合适,但这是一个思路。/***@authorfelord.cn*@since1.0.8.RELEASE*/@Slf4j@RestControllerAdvice(basePackages={"cn.felord.logging"})publicclassRestBodyAdviceimplementsResponseBodyAdvice{privatestaticfinalintDEFAULT_MAX_PAYLOAD_LENGTH=10000;publicstaticfinalStringREQUEST_LENGTH=10000;publicstaticfinalStringREQUEST_REQUEST_REQUEST[PREFIX];publicstaticfinalStringREQUEST_MESSAGE_SUFFIX="]";privateObjectMapperobjectMapper=newObjectMapper();@Overridepublicbooleansupports(MethodParameterreturnType,Class>converterType){returntrue;}@SneakyThrows@OverridepublicObjectbeforeBodyWrite(Objectbody,MethodParameterreturnType,MediaHtendType,TypeHttext>Converttext?selectedConverterType,ServerHttpRequestrequest,ServerHttpResponseresponse){ServletServerHttpRequestservletServerHttpRequest=(ServletServerHttpRequest)请求;log.debug(createRequestMessage(servletServerHttpRequest.getServletRequest(),REQUEST_MESSAGE_PREFIX,REQUEST_MESSAGE_SUFFIX));RestobjectRest;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);}日志。debug("ResponseBody["+objectMapper.writeValueAsString(objectRest)+"]");returnobjectRest;}privatebooleancheckPrimitive(Objectbody){Classclazz=body.getClass();returnclazz.isPrimitive()||clazz.isArray()||Collection.class.isAssignableFrom(clazz)||bodyinstanceofNumber||bodyinstanceofBoolean||bodyinstanceofCharacter||bodyinstanceofString;}protectedStringcreateRequestMessage(HttpServletRequestrequest,Stringprefix,Stringsuffix){StringBuildermsg=newStringBuilder();msg.append(prefix);msg.append(request.getMethod()).append("");msg.append(request.getRequestURI());StringqueryString=request.getQueryString();if(queryString!=null){msg.append('?').append(queryString);}Stringclient=request.getRemoteAddr();if(StringUtils.hasLength(client)){msg.append(",client=").append(client);}HttpSessionsession=request.getSession(false);if(session!=null){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(request);if(payload!=null){msg.append(",payload=").append(payload);}msg.append(后缀);returnmsg.toString();}protectedStringgetMessagePayload(HttpServletRequestrequest){ContentCachingRequestWrapperwrapper=WebUtils.getNativeRequest(request,ContentCachingRequestWrapper.class);if(wrapper!=null){byte[]buf=wrapper.getContentAsByteArray();if(buf.length>0){intlength=Math.min(buf.length,DEFAULT_MAX_PAYLOAD_LENGTH);try{returnnewString(buf,0,length,wrapper.getCharacterEncoding());}catch(UnsupportedEncodingExceptionex){return"[unknown]";}}}returnnull;}不要忘记将ResponseBodyAdvice的日志记录级别配置为DEBUGlogstash-logback-encoder。这是logstash的logbackencoder,可以将httptrace结构化的输出为json。引入:net.logstash.logbacklogstash-logback-encoder6.6配置logback的ConsoleAppender为LogstashEncoder:然后相同的实现一个解析的过滤器:importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.slf4j.MDC;importorg.springframework.core.annotation。命令;importorg.springframework.stereotype.Component;importjavax.servlet.*;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.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)request;HttpServletResponseres=(HttpServletResponse)response;try{addXRequestId(req);LOGGER.info("path:{},method:{},query{}",req.getRequestURI(),req.getMethod(),req.getQueryString());res.setHeader(X_REQUEST_ID,MDC.get(X_REQUEST_ID));chain.doFilter(request,response);}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);如果(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,path:/log/get,method:GET,queryfoo=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”}总结今天我们介绍了很多记录和跟踪接口请求响应的方法,总有一种适合你。本文转载自微信公众号“码农小胖哥”,可通过以下二维码关注。转载本文请联系码农小胖公众号。