springboot配置請求日志
一般情況下,接口請求都需要日志記錄,Java springboot中的日志記錄相對復雜一點
經過實踐,以下方案可行,記錄一下完整過程
一、創建日志數據模型
創建實體類,也就是日志文件中要記錄的數據格式
我是放在我的實體類包中,也就算pojo包下
package org.example.pojo;import lombok.Data;@Data
public class RequestLog {private String url; // 請求URLprivate String method; // HTTP方法private long startTime; // 開始時間private long endTime; // 結束時間private String remoteIp; // 客戶端IPprivate String queryString; // 查詢參數private String requestBody; // 請求體(根據需求謹慎記錄)private Integer status; // 響應狀態碼private Long costTime; // 耗時(ms)@Overridepublic String toString(){return "{" +"\"url\":\"" + url + '\"' +", \"method\":\"" + method + '\"' +", \"remoteIp\":\"" + remoteIp + '\"' +", \"status\":" + status +", \"costTime\":" + costTime +", \"queryString\":\"" + (queryString != null ? queryString : "") + '\"' +", \"requestBody\":\"" + (requestBody != null ? requestBody : "") + '\"' +'}';}
}
這里有個小問題,requestBody似乎取不到,比較麻煩,但不影響記錄
二、創建攔截器
其實就是每次請求前,讀取請求信息,并添加到請求實體中去
我放在interceptor包下
package org.example.interceptor;import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.pojo.RequestLog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;@Component
public class RequestLoggingInterceptor implements HandlerInterceptor {private static final Logger logger = LoggerFactory.getLogger("REQUEST_LOG");private static final String ATTRIBUTE_REQUEST_LOG = "requestLog";@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {RequestLog requestLog = new RequestLog();requestLog.setStartTime(System.currentTimeMillis());requestLog.setUrl(request.getRequestURI());requestLog.setMethod(request.getMethod());// request請求中的原始iprequestLog.setRemoteIp(request.getRemoteHost());// 自定義的獲取ip方法,考慮了使用代理的情況
// requestLog.setRemoteIp(getClientIp(request));requestLog.setQueryString(request.getQueryString());
// 下面這兩行會報錯
// requestLog.setRequestBody(request.getReader().toString());
// requestLog.setRequestBody(request.getRequestBody());request.setAttribute(ATTRIBUTE_REQUEST_LOG, requestLog);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {RequestLog requestLog = (RequestLog) request.getAttribute(ATTRIBUTE_REQUEST_LOG);if (requestLog != null) {requestLog.setEndTime(System.currentTimeMillis());requestLog.setCostTime(System.currentTimeMillis() - requestLog.getStartTime());requestLog.setStatus(response.getStatus());logger.info(requestLog.toString());}}/*** 獲取客戶端真實IP(考慮了代理情況)*/private String getClientIp(HttpServletRequest request) {String ip = request.getHeader("X-Forwarded-For");if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("Proxy-Client-IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("WL-Proxy-Client-IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getRemoteAddr();}return ip;}
}
幾個基礎問題記錄下:
- Component注解,將這個方法注入到springboot項目中,使其成為項目組件
- preHandle和afterCompletion都是HandlerInterceptor中原有的方法,重寫方法時,必須保證參數完全一致,同時需要拋出異常
- slf4j在maven中并沒有顯示引入,應該是springboot自帶的
三、注冊攔截器
需要寫一個配置類,我放在config包中
package org.example.config;import org.example.interceptor.RequestLoggingInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate RequestLoggingInterceptor requestLoggingInterceptor;@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOrigins("http://localhost:5173", "http://localhost:5174").allowedMethods("*").allowedHeaders("*").allowCredentials(true).maxAge(3600);}@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(requestLoggingInterceptor).addPathPatterns("/**"); // 攔截所有請求路徑,都需要添加日志}
}
幾點說明:
- addCorsMappings用來處理跨域,重寫父類方法
- 攔截器在addInterceptors,也是重寫的
四、配置 Logback 以將日志保存到文件
在 src/main/resources
目錄下創建或修改 logback-spring.xml
文件,為請求日志配置一個獨立的追加器 (Appender),并將其輸出到文件
<?xml version="1.0" encoding="UTF-8"?>
<configuration><!-- 定義日志文件存儲路徑和文件名 --><property name="LOG_PATH" value="./logs" /><property name="REQUEST_LOG_FILE" value="${LOG_PATH}/request.log" /><!-- 控制臺輸出配置(可選) --><appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern></encoder></appender><!-- 為請求日志配置獨立的滾動文件Appender --><appender name="REQUEST_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><file>${REQUEST_LOG_FILE}</file> <!-- 當前活動的日志文件 --><encoder><!-- 配置輸出格式,因為我們在Interceptor中已輸出JSON字符串,這里只需消息本身 --><pattern>%msg%n</pattern> </encoder><!-- 滾動策略:按日期和大小滾動 --><rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"><!-- 滾動后的文件命名模式:按天歸檔,超過大小則遞增,并自動壓縮 --><fileNamePattern>${REQUEST_LOG_FILE}.%d{yyyy-MM-dd}.%i.gz</fileNamePattern><maxFileSize>50MB</maxFileSize> <!-- 每個文件最大大小 --><maxHistory>30</maxHistory> <!-- 保留30天的歷史日志 --><totalSizeCap>5GB</totalSizeCap> <!-- 所有請求日志文件總大小上限 --></rollingPolicy></appender><!-- 專門為我們的請求日志logger配置,指向獨立的文件Appender,并不再向上傳遞(additivity=false) --><logger name="REQUEST_LOG" level="INFO" additivity="false"><appender-ref ref="REQUEST_FILE" /><!-- 如果需要同時在控制臺看到,可以加上 <appender-ref ref="CONSOLE"/> --></logger><!-- 根日志記錄器(其他日志的輸出配置) --><root level="INFO"><appender-ref ref="CONSOLE" /></root>
</configuration>
五、日志文件
{"url":"/api/layout/positions", "method":"POST", "remoteIp":"0:0:0:0:0:0:0:1", "status":200, "costTime":310, "queryString":""}
{"url":"/api/user/login", "method":"POST", "remoteIp":"0:0:0:0:0:0:0:1", "status":200, "costTime":19, "queryString":""}
{"url":"/api/user/login", "method":"POST", "remoteIp":"0:0:0:0:0:0:0:1", "status":200, "costTime":10, "queryString":"", "requestBody":"{"}
{"url":"/error", "method":"POST", "remoteIp":"0:0:0:0:0:0:0:1", "status":500, "costTime":27, "queryString":"", "requestBody":" "username": "huangg","}
{"url":"/api/user/login", "method":"POST", "remoteIp":"0:0:0:0:0:0:0:1", "status":200, "costTime":283, "queryString":"", "requestBody":""}
{"url":"/api/user/login", "method":"POST", "remoteIp":"0:0:0:0:0:0:0:1", "status":200, "costTime":10, "queryString":"", "requestBody":"org.apache.catalina.connector.CoyoteReader@6906fda1"}
{"url":"/error", "method":"POST", "remoteIp":"0:0:0:0:0:0:0:1", "status":500, "costTime":27, "queryString":"", "requestBody":"org.apache.catalina.connector.CoyoteReader@6906fda1"}
{"url":"/api/user/login", "method":"POST", "remoteIp":"0:0:0:0:0:0:0:1", "status":200, "costTime":301, "queryString":"", "requestBody":""}
{"url":"/api/user/login", "method":"POST", "remoteIp":"127.0.0.1", "status":200, "costTime":3, "queryString":"", "requestBody":""}
{"url":"/api/user/login", "method":"POST", "remoteIp":"192.168.0.94", "status":200, "costTime":4, "queryString":"", "requestBody":""}
- 如果本機上發起請求,請求域名用的是localhost,則remoteIp是ipv6地址,0:0:0:0:0:0:0:1
- 如果本機上發起請求,請求域名用的是127.0.0.1,則remoteIp是127.0.0.1
- 如果使用真實ip地址192.168.0.94,則remoteIp是真實IP192.168.0.94