本网站(662p.com)打包出售,且带程序代码数据,662p.com域名,程序内核采用TP框架开发,需要联系扣扣:2360248666 /wx:lianweikj
精品域名一口价出售:1y1m.com(350元) ,6b7b.com(400元) , 5k5j.com(380元) , yayj.com(1800元), jiongzhun.com(1000元) , niuzen.com(2800元) , zennei.com(5000元)
需要联系扣扣:2360248666 /wx:lianweikj
Spring Boot 记录请求响应日志的常用手段
phpren · 284浏览 · 发布于2021-08-11 +关注

 

某些业务需求需要追踪我们的接口访问情况,也就是把请求和响应记录下来。基本的记录维度包含了请求入参(路径query参数,请求体)、请求路径(uri)、请求方法(method)、请求头(headers)以及响应状态、响应头、甚至包含了敏感的响应体等等。今天总结了几种方法,你可以按需选择。

某些业务需求需要追踪我们的接口访问情况,也就是把请求和响应记录下来。基本的记录维度包含了请求入参(路径query参数,请求体)、请求路径(uri)、请求方法(method)、请求头(headers)以及响应状态、响应头、甚至包含了敏感的响应体等等。今天总结了几种方法,你可以按需选择。

请求追踪的实现方式

网关层

很多网关设施都具有httptrace的功能,可以帮助我们集中记录请求流量的情况。Orange、Kong、Apache Apisix这些基于Nginx的网关都具有该能力,就连Nginx本身也提供了记录httptrace日志的能力。

优点是可以集中的管理httptrace日志,免开发;缺点是技术要求高,需要配套的分发、存储、查询的设施。

Spring Boot Actuator

在Spring Boot中,其实提供了简单的追踪功能。你只需要集成:


  1. <dependency> 

  2.     <groupId>org.springframework.boot</groupId> 

  3.     <artifactId>spring-boot-starter-actuator</artifactId> 

  4. </dependency> 

开启/actuator/httptrace:


  1. management: 

  2.   endpoints: 

  3.     web: 

  4.       exposure: 

  5.        include: 'httptrace' 

就可以通过http://server:port/actuator/httptrace获取最近的Http请求信息了。

不过在最新的版本中可能需要显式的声明这些追踪信息的存储方式,也就是实现HttpTraceRepository接口并注入Spring IoC。

例如放在内存中并限制为最近的100条(不推荐生产使用):


  1. @Bean 

  2. public HttpTraceRepository httpTraceRepository(){ 

  3.     return new InMemoryHttpTraceRepository(); 

追踪日志以json格式呈现:

Spring Boot Actuator记录的httptrace

记录的维度不多,当然如果够用的话可以试试。

优点在于集成起来简单,几乎免除开发;缺点在于记录的维度不多,而且需要搭建缓冲消费这些日志信息的设施。

CommonsRequestLoggingFilter

Spring Web模块还提供了一个过滤器CommonsRequestLoggingFilter,它可以对请求的细节进行日志输出。配置起来也比较简单:


  1. @Bean 

  2. CommonsRequestLoggingFilter  loggingFilter(){ 

  3.     CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter(); 

  4.     // 记录 客户端 IP信息 

  5.     loggingFilter.setIncludeClientInfo(true); 

  6.     // 记录请求头 

  7.     loggingFilter.setIncludeHeaders(true); 

  8.     // 如果记录请求头的话,可以指定哪些记录,哪些不记录 

  9.     // loggingFilter.setHeaderPredicate(); 

  10.     // 记录 请求体  特别是POST请求的body参数 

  11.     loggingFilter.setIncludePayload(true); 

  12.     // 请求体的大小限制 默认50 

  13.     loggingFilter.setMaxPayloadLength(10000); 

  14.     //记录请求路径中的query参数  

  15.     loggingFilter.setIncludeQueryString(true); 

  16.     return loggingFilter; 

而且必须开启对CommonsRequestLoggingFilter的debug日志:


  1. logging: 

  2.   level: 

  3.     org: 

  4.       springframework: 

  5.         web: 

  6.           filter: 

  7.             CommonsRequestLoggingFilter: debug 

一次请求会输出两次日志,一次是在第一次经过过滤器前;一次是完成过滤器链后。

CommonsRequestLoggingFilter记录请求日志

这里多说一句其实可以改造成输出json格式的。

优点是灵活配置、而且对请求追踪的维度全面,缺点是只记录请求而不记录响应。

ResponseBodyAdvice

Spring Boot统一返回体其实也能记录,需要自行实现。这里借鉴了CommonsRequestLoggingFilter解析请求的方法。响应体也可以获取了,不过响应头和状态因为生命周期还不清楚,这里获取还不清楚是否合适,不过这是一个思路。


  1. /** 

  2.  * @author felord.cn 

  3.  * @since 1.0.8.RELEASE 

  4.  */ 

  5. @Slf4j 

  6. @RestControllerAdvice(basePackages = {"cn.felord.logging"}) 

  7. public class RestBodyAdvice implements ResponseBodyAdvice<Object> { 

  8.     private static final int DEFAULT_MAX_PAYLOAD_LENGTH = 10000; 

  9.     public static final String REQUEST_MESSAGE_PREFIX = "Request ["; 

  10.     public static final String REQUEST_MESSAGE_SUFFIX = "]"; 

  11.     private ObjectMapper objectMapper = new ObjectMapper(); 

  12.     @Override 

  13.     public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { 

  14.         return true; 

  15.     } 

  16.     @SneakyThrows 

  17.     @Override 

  18.     public Object beforeBodyWrite(Object body, 

  19.                                  MethodParameter returnType, 

  20.                                   MediaType selectedContentType, 

  21.                                   Class<? extends HttpMessageConverter<?>> selectedConverterType, 

  22.                                   ServerHttpRequest request, 

  23.                                   ServerHttpResponse response) { 

  24.         ServletServerHttpRequest servletServerHttpRequest = (ServletServerHttpRequest) request; 

  25.         log.debug(createRequestMessage(servletServerHttpRequest.getServletRequest(), REQUEST_MESSAGE_PREFIX, REQUEST_MESSAGE_SUFFIX)); 

  26.         Rest<Object> objectRest; 

  27.         if (body == null) { 

  28.             objectRest = RestBody.okData(Collections.emptyMap()); 

  29.         } else if (Rest.class.isAssignableFrom(body.getClass())) { 

  30.             objectRest = (Rest<Object>) body; 

  31.         } 

  32.         else if (checkPrimitive(body)) { 

  33.             return RestBody.okData(Collections.singletonMap("result", body)); 

  34.         }else { 

  35.             objectRest = RestBody.okData(body); 

  36.         } 

  37.         log.debug("Response Body ["+ objectMapper.writeValueAsString(objectRest) +"]"); 

  38.         return objectRest; 

  39.     } 

  40.     private boolean checkPrimitive(Object body) { 

  41.         Class<?> clazz = body.getClass(); 

  42.         return clazz.isPrimitive() 

  43.                 || clazz.isArray() 

  44.                 || Collection.class.isAssignableFrom(clazz) 

  45.                 || body instanceof Number 

  46.                 || body instanceof Boolean 

  47.                 || body instanceof Character 

  48.                 || body instanceof String; 

  49.     } 

  50.     protected String createRequestMessage(HttpServletRequest request, String prefix, String suffix) { 

  51.         StringBuilder msg = new StringBuilder(); 

  52.         msg.append(prefix); 

  53.         msg.append(request.getMethod()).append(" "); 

  54.         msg.append(request.getRequestURI()); 

  55.         String queryString = request.getQueryString(); 

  56.         if (queryString != null) { 

  57.             msg.append('?').append(queryString); 

  58.         } 

  59.         String client = request.getRemoteAddr(); 

  60.         if (StringUtils.hasLength(client)) { 

  61.             msg.append(", client=").append(client); 

  62.         } 

  63.         HttpSession session = request.getSession(false); 

  64.         if (session != null) { 

  65.             msg.append(", session=").append(session.getId()); 

  66.        } 

  67.         String user = request.getRemoteUser(); 

  68.         if (user != null) { 

  69.             msg.append(", user=").append(user); 

  70.         } 

  71.         HttpHeaders headers = new ServletServerHttpRequest(request).getHeaders(); 

  72.        msg.append(", headers=").append(headers); 

  73.         String payload = getMessagePayload(request); 

  74.         if (payload != null) { 

  75.             msg.append(", payload=").append(payload); 

  76.         } 

  77.         msg.append(suffix); 

  78.         return msg.toString(); 

  79.     } 

  80.     protected String getMessagePayload(HttpServletRequest request) { 

  81.         ContentCachingRequestWrapper wrapper = 

  82.                 WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class); 

  83.         if (wrapper != null) { 

  84.             byte[] buf = wrapper.getContentAsByteArray(); 

  85.             if (buf.length > 0) { 

  86.                 int length = Math.min(buf.length, DEFAULT_MAX_PAYLOAD_LENGTH); 

  87.                try { 

  88.                     return new String(buf, 0, length, wrapper.getCharacterEncoding()); 

  89.                 } catch (UnsupportedEncodingException ex) { 

  90.                     return "[unknown]"; 

  91.                 } 

  92.             } 

  93.         } 

  94.         return null; 

  95.     } 

别忘记配置ResponseBodyAdvice的logging级别为DEBUG。

logstash-logback-encoder

这个是logstash的logback编码器,可以结构化输出httptrace为json。引入:


  1. <dependency> 

  2.     <groupId>net.logstash.logback</groupId> 

  3.     <artifactId>logstash-logback-encoder</artifactId> 

  4.     <version>6.6</version> 

  5. </dependency> 

配置logback的ConsoleAppender为LogstashEncoder:


  1. <configuration> 

  2.     <appender name="jsonConsoleAppender" class="ch.qos.logback.core.ConsoleAppender"> 

  3.         <encoder class="net.logstash.logback.encoder.LogstashEncoder"/> 

  4.     </appender> 

  5.     <root level=" INFO"> 

  6.         <appender-ref ref="jsonConsoleAppender"/> 

  7.    </root> 

  8. </configuration> 

然后同样实现一个解析的Filter:


  1. import org.slf4j.Logger; 

  2. import org.slf4j.LoggerFactory; 

  3. import org.slf4j.MDC; 

  4. import org.springframework.core.annotation.Order; 

  5. import org.springframework.stereotype.Component; 

  6. import javax.servlet.*; 

  7. import javax.servlet.http.HttpServletRequest; 

  8. import javax.servlet.http.HttpServletResponse; 

  9. import java.io.IOException; 

  10. import java.util.UUID; 

  11. /** 

  12.  * @author felord.cn 

  13.  * @since 1.0.8.RELEASE 

  14.  */ 

  15. @Order(1) 

  16. @Component 

  17. public class MDCFilter implements Filter { 

  18.     private final Logger LOGGER = LoggerFactory.getLogger(MDCFilter.class); 

  19.     private final String X_REQUEST_ID = "X-Request-ID"; 

  20.     @Override 

  21.     public void doFilter(ServletRequest request, 

  22.                          ServletResponse response, 

  23.                          FilterChain chain) throws IOException, ServletException { 

  24.         HttpServletRequest req = (HttpServletRequest) request; 

  25.         HttpServletResponse res = (HttpServletResponse) response; 

  26.         try { 

  27.             addXRequestId(req); 

  28.             LOGGER.info("path: {}, method: {}, query {}", 

  29.                     req.getRequestURI(), req.getMethod(), req.getQueryString()); 

  30.             res.setHeader(X_REQUEST_ID, MDC.get(X_REQUEST_ID)); 

  31.             chain.doFilter(request, response); 

  32.         } finally { 

  33.             LOGGER.info("statusCode {}, path: {}, method: {}, query {}", 

  34.                     res.getStatus(), req.getRequestURI(), req.getMethod(), req.getQueryString()); 

  35.             MDC.clear(); 

  36.         } 

  37.     } 


  38.     private void addXRequestId(HttpServletRequest request) { 

  39.         String xRequestId = request.getHeader(X_REQUEST_ID); 

  40.         if (xRequestId == null) { 

  41.             MDC.put(X_REQUEST_ID, UUID.randomUUID().toString()); 

  42.         } else { 

  43.            MDC.put(X_REQUEST_ID, xRequestId); 

  44.         } 

  45.     } 

这里解析方式其实还可以更加精细一些。

然后所有的日志都可以结构化为json了:


  1. {"@timestamp":"2021-08-10T23:48:51.322+08:00","@version":"1","message":"statusCode 200, path: /log/get, method: GET, query 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"} 

总结

今天介绍了不少记录追踪接口请求响应的方法,总有一款适合你。


相关推荐

PHP实现部分字符隐藏

沙雕mars · 1324浏览 · 2019-04-28 09:47:56
Java中ArrayList和LinkedList区别

kenrry1992 · 907浏览 · 2019-05-08 21:14:54
Tomcat 下载及安装配置

manongba · 968浏览 · 2019-05-13 21:03:56
JAVA变量介绍

manongba · 961浏览 · 2019-05-13 21:05:52
什么是SpringBoot

iamitnan · 1086浏览 · 2019-05-14 22:20:36
加载中

0评论

评论
我从小喜欢编程,一直在学习中,从未停止,未来也是如此!
小鸟云服务器
扫码进入手机网页