SpringBoot 超詳細的記錄HTTP請求日誌

閒來無事想做個HTTP請求日誌分析以及存檔功能 因爲和客戶端交互數據用的是Protobuf 所以這裏記錄下攔截日誌的實現(Json什麼的其他傳輸協議也做了幾個) 日誌攔截器繼 DispatcherServlet類然後找個地方註冊一下Bean即可 註冊代碼如下 LoggableDispatcherServlet是我們自己的類
記錄的日誌是打印出json 因爲結構比較複雜

    @Bean
    public ServletRegistrationBean dispatcherRegistration() {
        return new ServletRegistrationBean<>(dispatcherServlet());
    }
    @Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
    public DispatcherServlet dispatcherServlet() {
        return new LoggableDispatcherServlet();
    }

然後是攔截類 我這裏用的是protobuf 不是平時的json 所以比較麻煩 爲此還折騰了一會 json或者xml簡單很多 如果你不是protobuf 吧那一部分刪除即可 本文主要圍繞protobuf做處理 其他的處理方式也都在裏面弄了下 可以參考


import com.dexfun.magic.common.util.HttpMessage;
import com.dexfun.magic.protobuf.Transmission;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.protobuf.Message;
import com.google.protobuf.util.JsonFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.util.ContentCachingResponseWrapper;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

@Order(Ordered.HIGHEST_PRECEDENCE)//最高優先級 方便攔截404什麼的
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 {
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
        BufferedServletRequestWrapper bufferedServletRequestWrapper = new BufferedServletRequestWrapper(request);
        ServletInputStream inputStream = bufferedServletRequestWrapper.getInputStream();
        setThrowExceptionIfNoHandlerFound(true);
        ObjectNode rootNode = mapper.createObjectNode();
        ObjectNode reqNode = mapper.createObjectNode();
        ObjectNode resNode = mapper.createObjectNode();
        String method = request.getMethod();
        rootNode.put("method", method);
        rootNode.put("url", request.getRequestURL().toString());
        rootNode.put("remoteAddr", request.getRemoteAddr());
        rootNode.put("x-forwarded-for", request.getHeader("x-forwarded-for"));
        rootNode.set("request", reqNode);
        rootNode.set("response", resNode);
        reqNode.set("headers", mapper.valueToTree(getRequestHeaders(request)));
        if (method.equals("GET")) {
            reqNode.set("body", mapper.valueToTree(request.getParameterMap()));
        } else {
            if (isProtoBufPost(request)) {
                HandlerExecutionChain handlerExecutionChain = getHandler(request);
                if (handlerExecutionChain != null) {
                    Object handler = handlerExecutionChain.getHandler();
                    if (handler instanceof HandlerMethod) {
                        HandlerMethod handlerMethod = (HandlerMethod) handler;
                        for (MethodParameter methodParameter : handlerMethod.getMethodParameters()) {
                            Parameter parameter = methodParameter.getParameter();
                            if (Message.class.isAssignableFrom(parameter.getType())) {
                                Class<Message> type = (Class<Message>) parameter.getType();
                                byte[] contentAsByteArray = inputStream.readAllBytes();
                                Transmission.Request parseFrom = Transmission.Request.parseFrom(HttpMessage.transform(contentAsByteArray));
                                String print = JsonFormat.printer().usingTypeRegistry(JsonFormat.TypeRegistry.newBuilder().add(parseFrom.getData().unpack(type).getDescriptorForType()).build()).print(parseFrom);
                                reqNode.set("body", mapper.readTree(print));
                                reqNode.put("bodyIsJson", true);
                            }
                        }
                    }
                } else {
                	//走到這裏基本就是404的情況了 無法預知 Any的data類型 無法格式化成json 只能轉成文本消息存日誌了 2333
                    byte[] contentAsByteArray = inputStream.readAllBytes();
                    Transmission.Request parseFrom = Transmission.Request.parseFrom(HttpMessage.transform(contentAsByteArray));
                    reqNode.put("body", parseFrom.toString());
                    reqNode.put("bodyIsJson", false);
                }
            } else if (isFormPost(request)) {
                reqNode.set("body", mapper.valueToTree(request.getParameterMap()));
                reqNode.put("bodyIsJson", true);
            } else if (isJsonPost(request)) {
                byte[] contentAsByteArray = inputStream.readAllBytes();
                reqNode.set("body", mapper.readTree(contentAsByteArray));
                reqNode.put("bodyIsJson", true);
            } else if (isTextPost(request) || isXmlPost(request)) {
                byte[] contentAsByteArray = inputStream.readAllBytes();
                reqNode.put("body", new String(contentAsByteArray));
                reqNode.put("bodyIsJson", false);
            } else if (isMediaPost(request)) {
                reqNode.put("body", "Media Request Body ContentLength = " + request.getContentLengthLong());
                reqNode.put("bodyIsJson", false);
            } else {
                byte[] contentAsByteArray = inputStream.readAllBytes();
                reqNode.put("body", "Unknown Request Body ContentLength = " + request.getContentLengthLong() + " body = " + (request.getContentLengthLong() > 2048 ? "content is too long" : new String(contentAsByteArray)));
                reqNode.put("bodyIsJson", false);
            }
        }
        HandlerExecutionChain handlerExecutionChain = getHandler(request);
        if (handlerExecutionChain == null) {
            //手動判斷是不是404 不走系統流程 直接處理 因爲會重定向/error
            resNode.put("status", HttpStatus.NOT_FOUND.value());
            logger.info(rootNode.toString());
            response.setStatus(HttpStatus.NOT_FOUND.value());
            PrintWriter writer = response.getWriter();
            writer.write("Request path not found");
            writer.flush();
            writer.close();
            return;
        }
        System.out.println(handlerExecutionChain);
        try {
            super.doDispatch(bufferedServletRequestWrapper, responseWrapper);
        } finally {
            byte[] responseWrapperContentAsByteArray = responseWrapper.getContentAsByteArray();
            responseWrapper.copyBodyToResponse();//這裏有順序 必須先讀body 然後再調用這個方法 才能繼續讀
            resNode.put("status", response.getStatus());
            Map<String, Object> responseHeaders = getResponseHeaders(response);

            //這裏判斷錯誤攔截是不是吧url改成error了 如果是就做一下替換 替換的值是錯誤攔截器寫到header裏面的
            String url = rootNode.get("url").asText();
            if (url.endsWith("/error")) {
                String path = (String) responseHeaders.get("x-error-path");
                if (!StringUtils.isEmpty(path)) {
                    rootNode.put("url", url.replace("/error", path));
                }
            }
            resNode.set("headers", mapper.valueToTree(responseHeaders));
            if (isProtoBufPost(responseWrapper)) {
                Object handler = handlerExecutionChain.getHandler();
                if (handler instanceof HandlerMethod) {
                    HandlerMethod handlerMethod = (HandlerMethod) handler;
                    MethodParameter returnType = handlerMethod.getReturnType();
                    Method returnTypeMethod = returnType.getMethod();
                    if (returnTypeMethod != null) {
                        if (Message.class.isAssignableFrom(returnTypeMethod.getReturnType()) && response.getStatus() == HttpStatus.OK.value()) {
                            Class<Message> type = (Class<Message>) returnTypeMethod.getReturnType();
                            Transmission.Response parseFrom = Transmission.Response.parseFrom(HttpMessage.transform(responseWrapperContentAsByteArray));
                            if (parseFrom.hasData()) {
                                String print = JsonFormat.printer().usingTypeRegistry(JsonFormat.TypeRegistry.newBuilder().add(parseFrom.getData().unpack(type).getDescriptorForType()).build()).print(parseFrom);
                                resNode.set("body", mapper.readTree(print));
                            } else {
                                String print = JsonFormat.printer().print(parseFrom);
                                resNode.set("body", mapper.readTree(print));
                            }
                            resNode.put("bodyIsJson", true);
                        }
                    }
                }
            } else {
                try {
                    resNode.set("body", mapper.readTree(responseWrapperContentAsByteArray));
                    resNode.put("bodyIsJson", true);
                } catch (Exception e) {
                    resNode.put("body", new String(responseWrapperContentAsByteArray));
                    resNode.put("bodyIsJson", false);
                }
            }
            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> getResponseHeaders(HttpServletResponse response) {
        Map<String, Object> headers = new HashMap<>();
        Collection<String> headerNames = response.getHeaderNames();
        for (String headerName : headerNames) {
            headers.put(headerName, response.getHeader(headerName));
        }
        return headers;
    }

    private boolean isFormPost(HttpServletRequest request) {
        String contentType = request.getContentType();
        return (contentType != null && contentType.contains("x-www-form"));
    }

    private boolean isMediaPost(HttpServletRequest request) {
        String contentType = request.getContentType();
        if (contentType != null)
            return contentType.contains("stream") || contentType.contains("image") || contentType.contains("video") || contentType.contains("audio");
        return false;
    }

    private boolean isTextPost(HttpServletRequest request) {
        String contentType = request.getContentType();
        if (contentType != null)
            return contentType.contains("text/plain");
        return false;
    }

    private boolean isJsonPost(HttpServletRequest request) {
        String contentType = request.getContentType();
        if (contentType != null)
            return contentType.contains("application/json");
        return false;
    }

    private boolean isXmlPost(HttpServletRequest request) {
        String contentType = request.getContentType();
        if (contentType != null)
            return contentType.contains("application/xml");
        return false;
    }

    private boolean isProtoBufPost(HttpServletRequest request) {
        String contentType = request.getContentType();
        if (contentType != null)
            return contentType.contains("application") && contentType.contains("protobuf");
        return false;
    }

    private boolean isProtoBufPost(HttpServletResponse response) {
        String contentType = response.getContentType();
        if (contentType != null)
            return contentType.contains("application") && contentType.contains("protobuf");
        return false;
    }

    class BufferedServletInputStream extends ServletInputStream {
        private ByteArrayInputStream inputStream;
        private ServletInputStream is;

        public BufferedServletInputStream(byte[] buffer, ServletInputStream is) {
            this.is = is;
            this.inputStream = new ByteArrayInputStream(buffer);
        }

        @Override
        public int available() {
            return inputStream.available();
        }

        @Override
        public int read() {
            return inputStream.read();
        }

        @Override
        public int read(byte[] b, int off, int len) {
            return inputStream.read(b, off, len);
        }

        @Override
        public boolean isFinished() {
            return is.isFinished();
        }

        @Override
        public boolean isReady() {
            return is.isReady();
        }

        @Override
        public void setReadListener(ReadListener listener) {
            is.setReadListener(listener);
        }
    }

    class BufferedServletRequestWrapper extends HttpServletRequestWrapper {
        private byte[] buffer;
        private ServletInputStream is;

        public BufferedServletRequestWrapper(HttpServletRequest request) throws IOException {
            super(request);
            this.is = request.getInputStream();
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byteArrayOutputStream.writeBytes(is.readAllBytes());
            this.buffer = byteArrayOutputStream.toByteArray();
        }

        @Override
        public ServletInputStream getInputStream() {
            return new BufferedServletInputStream(this.buffer, this.is);
        }
    }
}

BufferedServletRequestWrapper 和 ContentCachingResponseWrapper類都是爲了能重複讀寫 Stream

代碼中的HttpMessage.transform是我自定義的數據加密工具類 這裏就不多說了

HandlerExecutionChain handlerExecutionChain = getHandler(request);

protobuf的核心處理辦法就是通過DispatcherServlet類的getHandler方法獲取url對應的Controller方法中的參數類型 代碼如上 然後利用JsonFormat輸出入參的參數類類型 因爲我這裏的請求與入參都用自定義protobuf的 Transmission實體類包裝了一下 並且正式data是Any類型 因爲我要在外面封裝一下統一參數 例如時間戳或者簽名字符串來做校驗 防止抓包或者重放請求 其實安全問題最好是做https證書雙向校驗 具體可以參考我的安全系列博文

這裏貼一下我的 protobuf 消息轉換類 這裏面有上面提到的加密邏輯


import com.dexfun.magic.common.exception.CommonException;
import com.dexfun.magic.common.util.HttpMessage;
import com.dexfun.magic.protobuf.Transmission;
import com.google.protobuf.Message;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.lang.NonNull;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

public class ProtoBufHttpMessageConverter extends AbstractHttpMessageConverter<Message> {

    private static final MediaType PROTOBUF = new MediaType("application", "x-protobuf", StandardCharsets.UTF_8);

    private static final String X_PROTOBUF_SCHEMA_HEADER = "X-Protobuf-Schema";

    private static final String X_PROTOBUF_MESSAGE_HEADER = "X-Protobuf-Message";

    public ProtoBufHttpMessageConverter() {
        super(PROTOBUF);
    }

    @Override
    protected boolean supports(@NonNull Class<?> clazz) {
        return Message.class.isAssignableFrom(clazz);
    }

    @Override
    protected MediaType getDefaultContentType(Message message) {
        return PROTOBUF;
    }

    @NonNull
    @Override
    protected Message readInternal(@NonNull Class<? extends Message> clazz, @NonNull HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        InputStream inputStream = inputMessage.getBody();
        DataInputStream dataInputStream = new DataInputStream(inputStream);
        byte[] bytes = new byte[inputStream.available()];
        dataInputStream.readFully(bytes);
        dataInputStream.close();
        inputStream.close();

        Transmission.Request request = Transmission.Request.parseFrom(HttpMessage.transform(bytes));
        if ((System.currentTimeMillis() - request.getTimestamp()) > 1000 * 30) {
            throw new CommonException(HttpStatus.FORBIDDEN.value(), "請檢查時間是否準確");
        }
        return request.getData().unpack(clazz);
    }

    @Override
    protected void writeInternal(@NonNull Message message, @NonNull HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        setProtoHeader(outputMessage, message);
        OutputStream body = outputMessage.getBody();
        if (message instanceof Transmission.Response) {
            body.write(HttpMessage.transform(message.toByteArray()));
        } else {
      		//如果Controller返回的是protobuf沒有手動包裝這裏直接返回包裝類並且默認成功 因爲我做了異常統一處理 失敗情況都是通過拋出異常處理的 最後會貼上統一異常處理類的代碼Demo
            body.write(HttpMessage.transform(HttpMessage.ok(message).toByteArray()));
        }
        body.flush();
        body.close();
    }

    private void setProtoHeader(HttpOutputMessage response, Message message) {
        response.getHeaders().set(X_PROTOBUF_SCHEMA_HEADER, message.getDescriptorForType().getFile().getName());
        response.getHeaders().set(X_PROTOBUF_MESSAGE_HEADER, message.getDescriptorForType().getFullName());
    }
}

需要注意的是writeInternal方法 這裏針對Controller稍微做了一下處理 和攔截那邊的Response輸出做了一下對應 可以讓Controller返回消息響應包裝類或者直接返回 例如如下代碼 會方便一點


@RestController
public class UserController {

    @Autowired
    UserServiceImpl userService;
	//直接返回實體
    @RequestMapping(value = "/login.bin", method = RequestMethod.POST)
    public StringValue login(@RequestBody LoginDto.LoginEntity entity, HttpServletRequest request) throws Exception {
        String remoteAddress = Optional.ofNullable(request.getHeader("x-forwarded-for")).orElse(request.getRemoteAddr());
        String login = userService.login(entity, remoteAddress);
        return StringValue.newBuilder().setValue(login).build();
    }
	//返回包裝的類
    @RequestMapping(value = "/t", method = RequestMethod.POST)
    public Transmission.Response login(@RequestParam String aad, HttpServletRequest request) throws Exception {
        return Transmission.Response.newBuilder().setStatus(200).setMessage("ok").build();
    }

}

最後貼上異常統一處理類代碼


import com.dexfun.magic.common.exception.CommonException;
import com.dexfun.magic.protobuf.Transmission;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.ServletWebRequest;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

/**
 * @author Smile
 */
@Slf4j
@RestController
@RestControllerAdvice
public class RestExceptionHandler implements ErrorController {

    private final ErrorAttributes errorAttributes;

    @Autowired
    public RestExceptionHandler(ErrorAttributes errorAttributes) {
        this.errorAttributes = errorAttributes;
    }
	//返回自定義包裝的類
    @ExceptionHandler(value = CommonException.class)
    public Transmission.Response commonException(CommonException e) {
        return Transmission.Response.newBuilder().setStatus(e.getStatus()).setMessage(e.getMessage()).build();
    }

    @ExceptionHandler(value = Throwable.class)
    public String allException(Throwable e, HttpServletResponse response) {
        log.error("Server Exception", e);
        if (e instanceof HttpMessageNotReadableException) {
            response.setStatus(HttpStatus.NOT_ACCEPTABLE.value());
            try {
                String message = e.getMessage();
                if (StringUtils.isEmpty(message)) {
                    return "Error Not Message";
                }
                return e.getMessage().split(":")[0].split(";")[0];
            } catch (Exception ex) {
                return e.getMessage();
            }
        }
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        return e.getMessage();
    }
	//平時基本不會走這裏 除了404 如果我在日誌攔截器那邊手動判斷處理了404的情況就不會走
    @RequestMapping(value = "/error")
    public String error(HttpServletRequest request, HttpServletResponse response) {
        ServletWebRequest servletWebRequest = new ServletWebRequest(request);
        Map<String, Object> attributes = errorAttributes.getErrorAttributes(servletWebRequest, false);
        Integer status = (Integer) attributes.get("status");
        String path = (String) attributes.get("path");
        response.setHeader("x-error-path", path);//寫入錯誤前的原始url 日誌攔截器要用
        String error = (String) attributes.get("error");
        String message = (String) attributes.get("message");
        message = String.format("Request path %s %s", error, message);
        log.error("Request Exception " + attributes.toString());
        response.setStatus(status);
        return message;
    }

    @Override
    public String getErrorPath() {
        return "/error";
    }
}

以上錯誤攔截 如果是自定義業務異常就轉換成protobuf消息並且http狀態碼永遠是200 其他的異常返回對應http狀態碼 然後body打印錯誤消息 客戶端直接dialog錯誤信息body 因爲這些錯誤都是不可控的 系統發生嚴重問題了 及時解決就行

本文結束 內容僅供參考 如有錯誤 請幫忙指正 蟹蟹~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章