记录springboot的请求日志

记录每一个请求的header,body以及响应的header,body等等信息。

AccessLogFilter

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Enumeration;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;


/**
 * 
 * 访问日志记录
 * 
 * @author KevinBlandy
 *
 */
@WebFilter(urlPatterns = "/**")
@Component
@Order(-8000)
public class AccessLogFilter extends HttpFilter {
	
	/**
	 * 
	 */
	private static final long serialVersionUID = 2067392425765043106L;
	
	public static final Logger LOGGER = LoggerFactory.getLogger(AccessLogFilter.class);
	
    private static final String LINE_SEPARATOR = System.getProperty("line.separator");
	
    // 请求日志的专用 GSON 格式化
    private Gson logGson;

    @Override
    public void init() throws ServletException {
        super.init();
        this.logGson = new GsonBuilder()
                .serializeNulls()					// null 参数也序列化
                .setPrettyPrinting()                // 格式化后输出
                .disableHtmlEscaping()				// 不编码HTML
                .create();
    }
	
	@Override
	protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {

		HttpServletRequest request = null;
		HttpServletResponse response = null;

		// Multipart 请求,不包装Request
		String contentType = req.getHeader(HttpHeaders.CONTENT_TYPE);
		if (StringUtils.hasLength(contentType) && contentType.startsWith(MediaType.MULTIPART_FORM_DATA_VALUE)) {
			request = req;
		} else {
			request = new ContentCachingRequestWrapper(req);
		}

		// 文件下载,图片验证码 不包装Response
		// TODO 判断方式待优化
		String requestUir = req.getRequestURI();
		
		if (requestUir.contains("down") || requestUir.contains("captcha") || requestUir.contains("favicon.ico")) {
			response = res;
		} else {
			response = new ContentCachingResponseWrapper(res);
		}

		try {
			Instant start = Instant.now();
			
			super.doFilter(request, response, chain);
			
			Instant end = Instant.now();
			
			response.setIntHeader(io.springcloud.constant.HttpHeaders.X_RESPONSE_TIME, (int) (end.toEpochMilli() - start.toEpochMilli()));
			
		} catch (Exception e) {
			LOGGER.error("access error: {}", e.getMessage());
			throw e;
		}

		this.onLog(request, response, this.accessLog(request, response));

		if (response instanceof ContentCachingResponseWrapper){
			((ContentCachingResponseWrapper) response).copyBodyToResponse();  // 把缓存的数据响应给客户端
		}
	}


	/**
	 * 访问日志解析
	 * @param req
	 * @param resp
	 * @return
	 */
	private JsonObject accessLog (HttpServletRequest req, HttpServletResponse resp) {

		JsonObject accessLog = new JsonObject();

		// 请求ID
		accessLog.addProperty("requestId", req.getHeader(io.springcloud.constant.HttpHeaders.X_REQUEST_ID));
		// client address
		accessLog.addProperty("remoteAddress", req.getRemoteAddr());
		// request method
		accessLog.addProperty("method", req.getMethod());
		// request url
		accessLog.addProperty("requestUrl", req.getRequestURL().toString());
		// query params
		accessLog.addProperty("queryParam", req.getQueryString());
		
		// request header
		Enumeration<String> nameEnumeration = req.getHeaderNames();
		JsonObject requestHeader = new JsonObject();
		while (nameEnumeration.hasMoreElements()) {
			String name = nameEnumeration.nextElement();
			Enumeration<String> valueEnumeration = req.getHeaders(name);
			JsonArray headers = new JsonArray(1);
			while (valueEnumeration.hasMoreElements()) {
				headers.add(valueEnumeration.nextElement());
			}
			requestHeader.add(name, headers);
		}
		accessLog.add("requestHeader", requestHeader);
		
		// requestBody
		/**
		 * TODO 在Undertow环境下,如果是请求体超出限制(server.undertow.max-http-post-size),而抛出异常:RequestTooBigException。那么RequestBody读取不到,为空字符串
		 */
		String requestBody = null;

		if (req instanceof ContentCachingRequestWrapper){
			requestBody = new String(((ContentCachingRequestWrapper)req).getContentAsByteArray(), StandardCharsets.UTF_8);
		}

		accessLog.addProperty("requestBody", requestBody);
		
		// response header
		JsonObject responseHeader = new JsonObject();
		for (String headerName : resp.getHeaderNames()) {
			JsonArray jsonArray = new JsonArray(1);
			resp.getHeaders(headerName).forEach(jsonArray::add);
			responseHeader.add(headerName, jsonArray);
		}
		
		// response status
		accessLog.addProperty("responseStatus", resp.getStatus());
		
		// response header
		accessLog.add("responseHeader", responseHeader);

		String responseBody = null;

		if (resp instanceof  ContentCachingResponseWrapper){
			responseBody = new String(((ContentCachingResponseWrapper)resp).getContentAsByteArray(), StandardCharsets.UTF_8);
		}
		
		// response body
		accessLog.addProperty("responseBody", responseBody);
		
		return accessLog;
	}

    protected void onLog(HttpServletRequest request, HttpServletResponse response, JsonObject accessLog) {

        String requestId = request.getHeader(io.springcloud.constant.HttpHeaders.X_REQUEST_ID);

        // 解析请求日志
        String log = logGson.toJson(accessLog);

        LOGGER.debug("access log: {}{}{}", requestId, LINE_SEPARATOR, log);
    }
}