需求背景
- 线上项目出现bug时,可以通过接口的请求参数来排查定位问题。
- 和业务方battle时,能够证明他是自己操作的问题。
效果图
实现思路
- Spring提供了CommonsRequestLoggingFilter过滤器,该过滤器可以在接口请求前和请求后分别打印日志;
- 通过继承CommonsRequestLoggingFilter过滤器,实现部分自己的逻辑;
其他方案对比
- aop的方式:必须要到controller层,才会打印, 有些接口在 过滤器、拦截器层被拦截。
- 拦截器的方式:request.getInputStream()只能调用一次,解决的方法为copy一份流,个人感觉在代码逻辑层面不太清晰。
优缺点分析
缺点
- 无法获取返回值,适合一些不关心返回值的场景(如果接口出现异常,此时也没有返回值)。
- 对于Payload参数,内部使用的是ContentCachingRequestWrapper读取的request.getInputStream()流,所以必须要先调用@RequstBody,才能取到值(可以去看下ContentCachingRequestWrapper的具体实现)
优点
- 实现简单,代码层次逻辑清晰,与业务逻辑解耦。
具体实现
继承CommonsRequestLoggingFilter过滤器,实现部分自己的逻辑
package com.yp.basic.log.filter;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.yp.basic.log.properties.OptLogProperties;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.web.filter.CommonsRequestLoggingFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Set;
/**
* 自定义的请求日志打印过滤器,打印所有接口请求参数、耗时等日志 <br/>
*
* 对比其他两种方式:<br/>
* <ul>
* <li>1、aop的方式:必须要到controller层,才会打印, 有些接口在 过滤器、拦截器层被拦截</li>
* <li>2、拦截器的方式:request.getInputStream()只能调用一次,解决的方法为copy一份流,个人感觉在代码逻辑层面不太清晰</li>
* </ul>
*
* 缺点:<br/>
* <ul>
* <li>1、无法获取返回值,适合一些不关心返回值的场景。</li>
* <li>2. 内部使用的是ContentCachingRequestWrapper读取的request.getInputStream(),必须要先调用@RequstBody,才能取到值。</li>
* </ul>
*
* 优点:<br/>
* <ul>
* <li>1、 实现简单,代码层次逻辑清晰。</li>
* </ul>
*
* @author: wcong
* @date: 2022/11/25 15:17
*/
@Slf4j
public class RequestLogPrintFilter extends CommonsRequestLoggingFilter {
/**
* 接口请求 开始 日志前缀标识,目的是为了提高每次判断时的性能
*/
public final static String REQUEST_START_PREFIX_FLAG = "0";
/**
* 接口请求 开始 日志前缀
*/
private final static String REQUEST_START_PREFIX = "### request start[";
/**
* 接口请求 结束 日志前缀标识,目的是为了提高每次判断时的性能
*/
public final static String REQUEST_END_PREFIX_FLAG = "1";
/**
* 接口请求 结束 日志前缀
*/
private final static String REQUEST_END_PREFIX = "### request end[";
/**
* 是否为prod环境
*/
private static final boolean IS_PROD_EVN;
/**
* 不打印接口请求日志的url集合
*/
private static final Set<String> EXCLUDE_HTTP_LOG_URLS;
static {
final String activeProfile = SpringUtil.getActiveProfile();
final OptLogProperties optLogProperties = SpringUtil.getBean(OptLogProperties.class);
IS_PROD_EVN = "prod".equals(activeProfile);
EXCLUDE_HTTP_LOG_URLS = optLogProperties.getExcludeHttpLogUrls();
}
/**
* 重写父类方法:封装打印消息的格式
*/
@Override
protected String createMessage(HttpServletRequest request, String prefix, String suffix) {
// 是否为不打印的url
if (isExcludeLogUrl((request.getRequestURI()))) {
return null;
}
final StringBuilder messageInfo = getMessageInfo(request, prefix, suffix);
// 请求开始还是结束
if (REQUEST_START_PREFIX_FLAG.equals(prefix)) {
// 请求开始
MDC.put("logStartTime", String.valueOf(System.currentTimeMillis()));
} else {
// 请求结束,记录耗时
final Long logStartTime = Convert.toLong(MDC.get("logStartTime"), 0L);
messageInfo.append("\r\n接口耗时: ").append(System.currentTimeMillis() - logStartTime).append("ms");
}
return messageInfo.toString();
}
/**
* 重写父类方法:请求前调用逻辑
*/
@Override
protected void beforeRequest(HttpServletRequest request, String message) {
// 是否为不打印的url
if (isExcludeLogUrl((request.getRequestURI()))) {
return;
}
doPrintLog(message);
}
/**
* 重写父类方法:请求后调用逻辑
*/
@Override
protected void afterRequest(HttpServletRequest request, String message) {
// 是否为不打印的url
if (isExcludeLogUrl((request.getRequestURI()))) {
return;
}
doPrintLog(message);
}
/**
* 重写父类方法:是否打印日志
*/
@Override
protected boolean shouldLog(HttpServletRequest request) {
// 父类中的逻辑是:logger.isDebugEnabled()
return true;
}
/**
* 统一封装打印的日志格式
*
* @param request javax.servlet.http.HttpServletRequest
* @param prefix 打印前缀
* @param suffix 打印后缀
* @return 封装好的日志格式
*/
private StringBuilder getMessageInfo(HttpServletRequest request, String prefix, String suffix) {
StringBuilder msg = new StringBuilder();
// 判断是 请求开始 还是 请求结束
if (REQUEST_START_PREFIX_FLAG.equals(prefix)) {
msg.append(REQUEST_START_PREFIX);
} else {
msg.append(REQUEST_END_PREFIX);
}
msg.append(StrUtil.format("method={}; ", request.getMethod().toLowerCase()));
msg.append("uri=").append(request.getRequestURI());
// 是否有传递 查询字符串 信息
if (isIncludeQueryString()) {
String queryString = request.getQueryString();
if (queryString != null) {
msg.append('?').append(queryString);
}
}
// 是否有传递 payload 信息
if (isIncludePayload()) {
String payload = getMessagePayload(request);
if (payload != null) {
msg.append("; payload=").append(payload);
}
}
// 是否包含 客户端 信息
if (isIncludeClientInfo()) {
String client = request.getRemoteAddr();
if (StrUtil.isNotBlank(client)) {
msg.append("; client=").append(client);
}
HttpSession session = request.getSession(false);
if (session != null) {
msg.append("; session=").append(session.getId());
}
String user = request.getRemoteUser();
if (user != null) {
msg.append("; user=").append(user);
}
}
msg.append(suffix);
return msg;
}
/**
* 具体打印的方法
*
* @param message 打印的消息
*/
private void doPrintLog(String message) {
// 生产环境打印debug级别
if (IS_PROD_EVN) {
log.debug(message);
} else {
log.info(message);
}
// log.info(message);
}
private Boolean isExcludeLogUrl(String url) {
return EXCLUDE_HTTP_LOG_URLS.contains(url);
}
}
注册过滤器
package com.yp.basic.log;
import com.yp.basic.log.properties.OptLogProperties;
import lombok.AllArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.filter.CommonsRequestLoggingFilter;
/**
* 日志自动配置
*
* @author: wcong
* @date: 2022/11/25 15:17
*/
@EnableAsync
@Configuration
@ConditionalOnWebApplication
@EnableConfigurationProperties(OptLogProperties.class)
public class LogAutoConfiguration {
@Bean
@ConditionalOnProperty(prefix = OptLogProperties.PREFIX, name = "enable-http-log", havingValue = "true", matchIfMissing = true)
public FilterRegistrationBean<CommonsRequestLoggingFilter> logFilterRegistration() {
CommonsRequestLoggingFilter filter = new RequestLogPrintFilter();
// 是否打印header中的内容,参数很多
filter.setIncludeHeaders(false);
// 是否打印查询字符串内容
filter.setIncludeQueryString(true);
// 是否打印 payLoad内容,内部使用的是ContentCachingRequestWrapper读取的request.getInputStream(),必须要先调用@RequstBody,才能取到值。
filter.setIncludePayload(true);
// 是否打印客户端信息(ip、session、remoteUser)
filter.setIncludeClientInfo(true);
// 1024字节(1kb),超出部分截取
// 在UTF-8编码方案中,一个英文字符占用一个字节,一个汉字字符占用三个字节的空间。
filter.setMaxPayloadLength(1024);
// 设置 before request 日志前缀,默认为:Before request [
filter.setBeforeMessagePrefix(RequestLogPrintFilter.REQUEST_START_PREFIX_FLAG);
// 设置 before request 日志后缀,默认为:]
filter.setBeforeMessageSuffix("]");
// 设置 before request 日志前缀,默认为:After request [
filter.setAfterMessagePrefix(RequestLogPrintFilter.REQUEST_END_PREFIX_FLAG);
// 设置 after request 日志后缀,默认为:]
filter.setAfterMessageSuffix("]");
FilterRegistrationBean<CommonsRequestLoggingFilter> registration = new FilterRegistrationBean<>(filter);
registration.addUrlPatterns("/*");
registration.setOrder(0);
registration.setName("commonsRequestLoggingFilter");
return registration;
}
}
OptLogProperties:在yml中提供配置
package com.yp.basic.log.properties;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.HashSet;
import java.util.Set;
import static com.yp.basic.log.properties.OptLogProperties.PREFIX;
/**
* 操作日志配置类
*
*/
@Data
@NoArgsConstructor
@ConfigurationProperties(prefix = PREFIX)
public class OptLogProperties {
public static final String PREFIX = "xxx.log";
/**
* 是否启用
*/
private Boolean enabled = true;
/**
* 是否打印http接口请求日志
*/
private Boolean enableHttpLog = true;
/**
* 不打印http接口请求日志的url
*/
private Set<String> excludeHttpLogUrls = new HashSet<>();
}