Spring Boot 記錄 Http 請求日誌

在使用Spring Boot開發 web api 的時候但願把 requestrequest headerresponse reponse header , uri, method 等等的信息記錄到咱們的日誌中,方便咱們排查問題,也能對系統的數據作一些統計。java

Spring 使用了 DispatcherServlet 來攔截並分發請求,咱們只要本身實現一個 DispatcherServlet 並在其中對請求和響應作處理打印到日誌中便可。web

咱們實現一個本身的分發 Servlet ,它繼承於 DispatcherServlet,咱們實現本身的 doDispatch(HttpServletRequest request, HttpServletResponse response) 方法。shell

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;
    }

LoggableDispatcherServlet 中,咱們能夠經過 HttpServletRequest 中的 InputStreamreader 來獲取請求的數據,但若是咱們直接在這裏讀取了流或內容,到後面的邏輯將沒法進行下去,因此須要實現一個能夠緩存的 HttpServletRequest。好在 Spring 提供這樣的類,就是 ContentCachingRequestWrapperContentCachingResponseWrapper, 根據官方的文檔這兩個類正好是來幹這個事情的,咱們只要將 HttpServletRequestHttpServletResponse 轉化便可。json

HttpServletRequest wrapper that caches all content read from the input stream and reader, and allows this content to be retrieved via a byte array.

Used e.g. by AbstractRequestLoggingFilter. Note: As of Spring Framework 5.0, this wrapper is built on the Servlet 3.1 API.api

HttpServletResponse wrapper that caches all content written to the output stream and writer, and allows this content to be retrieved via a byte array.
Used e.g. by ShallowEtagHeaderFilter. Note: As of Spring Framework 5.0, this wrapper is built on the Servlet 3.1 API.緩存

實現好咱們的 LoggableDispatcherServlet後,接下來就是要指定使用 LoggableDispatcherServlet 來分發請求。app

@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();
    }
}

增長一個簡單的 Controller 來測試一下curl

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

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

使用 curl 發送一個 Post 請求:ide

$ 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"
    }
}

固然打印出來是在一行中的,我進行了一下格式化。咱們還能夠在日誌中增長請求的時間,耗費的時間以及異常信息等。

相關文章
相關標籤/搜索