SpringBoot Httpリクエストログを記録する方法


Spring Bootを使ってwebアプリを開発する時に、request、request header、reponse reponse header、uri、methodなどの情報を私達のログに記録したいです。
SpringはDisplatch Servletを使用して要求をブロックし、配布しています。私たちは自分でDisplatch Servletを実現し、要求と応答に対して処理してログに印刷すればいいです。
私たちは自分たちの配布Servletを実現し、それはDispacterServletに引き継がれ、自分たちのdoDisppatchを実現します。

public class LoggableDispatcherServlet extends DispatcherServlet {

  private static final Logger logger = LoggerFactory.getLogger("HttpLogger");

  private static final ObjectMapper mapper = new ObjectMapper();

  @Override
  protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
    ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
    //     json   ,     http     
    ObjectNode rootNode = mapper.createObjectNode();
    rootNode.put("uri", requestWrapper.getRequestURI());
    rootNode.put("clientIp", requestWrapper.getRemoteAddr());
    rootNode.set("requestHeaders", mapper.valueToTree(getRequestHeaders(requestWrapper)));
    String method = requestWrapper.getMethod();
    rootNode.put("method", method);
    try {
      super.doDispatch(requestWrapper, responseWrapper);
    } finally {
      if(method.equals("GET")) {
        rootNode.set("request", mapper.valueToTree(requestWrapper.getParameterMap()));
      } else {
        JsonNode newNode = mapper.readTree(requestWrapper.getContentAsByteArray());
        rootNode.set("request", newNode);
      }

      rootNode.put("status", responseWrapper.getStatus());
      JsonNode newNode = mapper.readTree(responseWrapper.getContentAsByteArray());
      rootNode.set("response", newNode);

      responseWrapper.copyBodyToResponse();

      rootNode.set("responseHeaders", mapper.valueToTree(getResponsetHeaders(responseWrapper)));
      logger.info(rootNode.toString());
    }
  }

  private Map<String, Object> getRequestHeaders(HttpServletRequest request) {
    Map<String, Object> headers = new HashMap<>();
    Enumeration<String> headerNames = request.getHeaderNames();
    while (headerNames.hasMoreElements()) {
      String headerName = headerNames.nextElement();
      headers.put(headerName, request.getHeader(headerName));
    }
    return headers;

  }

  private Map<String, Object> getResponsetHeaders(ContentCachingResponseWrapper response) {
    Map<String, Object> headers = new HashMap<>();
    Collection<String> headerNames = response.getHeaderNames();
    for (String headerName : headerNames) {
      headers.put(headerName, response.getHeader(headerName));
    }
    return headers;
  }

ロギングableDisplatServletでは、HttpServletRequestのInputStreamまたはreaderを通じて要求のデータを取得できますが、直接にここでストリーミングやコンテンツを読み取ったら、後のロジックに進められなくなりますので、キャッシュできるHttpServRequestを実現する必要があります。幸いSpringはContentCachingRequest WrapperとContentCachingResonseWrapperという種類を提供しています。公式文書によると、この二つの種類はちょうどこの仕事をするために来ています。私たちはHttpServRequestとHttpServletResonseを変換すればいいです。
HttpServlet Request wrapper that caches all content read from the input stream and reader,and allows this content to be retrieved via byte array.
Used e.g.by Abstract Request Logggg Filter.Note:As of Spring Fraamework 5.0,this wrapper is built on the Servlet 3.1 API.
Http Servlet Response wrapper that caches all content written to the out put stream and writer,and allows this content to be retrieved via byte array.
Used e.g.by ShallowEtagHeader Filter.Note:As of Spring Framwark 5.0,this wrapper is built on the Servlet 3.1 API.
ロギングappleDiscpatch Servletを実現したら、次にロギングappleDiscatch Servletを使ってリクエストを配信するように指定します。

@SpringBootApplication
public class SbDemoApplication implements ApplicationRunner {

  public static void main(String[] args) {
    SpringApplication.run(SbDemoApplication.class, args);
  }
  @Bean
  public ServletRegistrationBean dispatcherRegistration() {
    return new ServletRegistrationBean(dispatcherServlet());
  }
  @Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
  public DispatcherServlet dispatcherServlet() {
    return new LoggableDispatcherServlet();
  }
}
簡単なコントローラを追加してテストしてください。

@RestController
@RequestMapping("/hello")
public class HelloController {

  @RequestMapping(value = "/word", method = RequestMethod.POST)
  public Object hello(@RequestBody Object object) {
    return object;
  }
}

curlを使用してPostリクエストを送信します。

$ curl --header "Content-Type: application/json" \
 --request POST \
 --data '{"username":"xyz","password":"xyz"}' \
 http://localhost:8080/hello/word
{"username":"xyz","password":"xyz"}
印刷のログを表示:

{
  "uri":"/hello/word",
  "clientIp":"0:0:0:0:0:0:0:1",
  "requestHeaders":{
    "content-length":"35",
    "host":"localhost:8080",
    "content-type":"application/json",
    "user-agent":"curl/7.54.0",
    "accept":"*/*"
  },
  "method":"POST",
  "request":{
    "username":"xyz",
    "password":"xyz"
  },
  "status":200,
  "response":{
    "username":"xyz",
    "password":"xyz"
  },
  "responseHeaders":{
    "Content-Length":"35",
    "Date":"Sun, 17 Mar 2019 08:56:50 GMT",
    "Content-Type":"application/json;charset=UTF-8"
  }
}
もちろんプリントアウトは一行の中にあります。フォーマットしてみました。私たちはまた、ログの中で要求の時間、消費時間、異常情報などを増やすことができます。
以上が本文の全部です。皆さんの勉強に役に立つように、私たちを応援してください。