紧接上一篇,我们虽然成功地将springboot服务接入到了plumeLog日志系统,但是仍遗留了几个问题待解决:
1、对于用户的一次http调用,我们需要在整个调用链中(涉及多个微服务)保持traceId是同一个
2、虽然日志接入成功了,但是请求接口的路径、方法、参数及返回结果没有记录
3、控制台、本地文件日志中也要输出traceId的值
一、配置feign调用透传traceId(使用自定义Http头)
对于第一个问题,笔者的解决思路是配置feign拦截器,在feign客户端将PlumeLog上下文中的traceId手动塞入到 traceId Http请求头中
java
public class TraceConst {
public static final String TRACE_ID = "traceId";
}
java
import com.plumelog.core.TraceId;
import com.tingcream.tmccloud.baseweb.tracelog.util.TraceConst;
import feign.RequestInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class TraceFeignConfig {
@Bean
public RequestInterceptor traceIdRequestInterceptor() {
return template -> {
// 优先从Plumelog上下文获取,降级从MDC获取
String traceId = TraceId.logTraceID.get();
if (traceId == null || traceId.isEmpty()) {
traceId = MDC.get(TraceConst.TRACE_ID);
}
// 拿到了traceid ,放到http请求头中,带入到下游feign服务调用
if (traceId != null && !traceId.isEmpty()) {
log.info("feign调用透传http请求头traceId:{}",traceId);
template.header(TraceConst.TRACE_ID, traceId);
}
};
}
}
二、使用自定义AOP切面记录详细请求参数日志
对于第二个问题,我们需要配置一个AOP切面切入所有的controller层接口即可
java
import com.alibaba.fastjson2.JSON;
import com.plumelog.core.TraceId;
import com.tingcream.tmccloud.baseweb.tracelog.annotation.IgnoreTraceLog;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* traceLog日志切面 (plume日志)
*/
@Slf4j
@Aspect
@Component
public class TraceLogAspect {
/**
* 参数最大长度限制(防止大对象撑爆日志) 2M
*/
private static final int MAX_PARAM_LENGTH = 2048000;
// /**
// * 需要脱敏的字段名
// */
// private static final String[] SENSITIVE_FIELDS = {"password", "token", "secret", "authorization"};
@Pointcut("execution(* com.tingcream..controller..*.*(..))")
public void webLog() {}
@Around("webLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 检查是否需要忽略日志记录
if (shouldIgnore(joinPoint)) {
// 直接执行方法,不记录日志
return joinPoint.proceed();
}
long startTime = System.currentTimeMillis();
String traceId = TraceId.logTraceID.get();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes != null ? attributes.getRequest() : null;
// 开始日志
log.info("=== 请求开始 === traceId: {}", traceId);
if (request != null) {
log.info("请求URL:{},请求方法:{},请求IP:{}", request.getRequestURL(), request.getMethod(), request.getRemoteAddr());
}
// 记录请求参数(过滤掉文件流、响应对象等)
Object[] args = joinPoint.getArgs();
Object[] loggableArgs = Arrays.stream(args)
.filter(arg -> !(arg instanceof MultipartFile))
.filter(arg -> !(arg instanceof HttpServletRequest))
.filter(arg -> !(arg instanceof HttpServletResponse))
.toArray();
if (loggableArgs.length > 0) {
String argsJson = JSON.toJSONString(loggableArgs);
// 限制长度
if (argsJson.length() > MAX_PARAM_LENGTH) {
argsJson = argsJson.substring(0, MAX_PARAM_LENGTH) + "... (truncated)";
}
log.info("请求参数: {}", argsJson);
} else {
log.info("请求参数: 无参数");
}
// 执行目标方法
Object result = joinPoint.proceed();
// 记录返回值和耗时
long elapsedTime = System.currentTimeMillis() - startTime;
String resultJson = JSON.toJSONString(result);
// 限制返回值长度
if (resultJson.length() > MAX_PARAM_LENGTH) {
resultJson = resultJson.substring(0, MAX_PARAM_LENGTH) + "... (truncated)";
}
log.info("返回结果: {}", resultJson);
log.info("耗时: {} ms", elapsedTime);
log.info("=== 请求结束 === traceId: {}", traceId);
return result;
}
/**
* 判断是否需要忽略日志记录
*/
private boolean shouldIgnore(ProceedingJoinPoint joinPoint) {
// 1. 获取目标类
Class<?> targetClass = joinPoint.getTarget().getClass();
// 2. 获取目标方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 3. 检查方法上是否有 @IgnoreTraceLog 注解
if (method.isAnnotationPresent(IgnoreTraceLog.class)) {
return true;
}
// 4. 检查类上是否有 @IgnoreTraceLog 注解
if (targetClass.isAnnotationPresent(IgnoreTraceLog.class)) {
return true;
}
return false;
}
}
java
import java.lang.annotation.*;
/**
* 忽略日志切面切入
*/
@Target({ElementType.METHOD, ElementType.TYPE}) // 可作用于方法或类
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IgnoreTraceLog {
String value() default "";
}
三、配置logback、slf4j 的控制台及文件日志输出traceId
我们在上一篇的 MyTraceIdFilter中加入了
MDC.put(TRACE_ID, traceId);
这样logback、slf4j等门面日志框架也能从上下文中拿到traceId了,因此我们只需在log.pattern 加上%X{traceId} 即可在控制台、本地日志文件中展示追踪码了。
XML
<property name="log.pattern"
value="[追踪码:%X{traceId}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"></property>
<!-- 控制台 appender-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 文件 滚动日志 (all)-->
<appender name="allLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 当前日志输出路径、文件名 -->
<file>${log.path}/all.log</file>
<!--日志输出格式-->
<encoder>
<pattern>${log.pattern}</pattern>
<charset>UTF-8</charset>
</encoder>
<!--历史日志归档策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 历史日志: 归档文件名 -->
<fileNamePattern>${log.path}/%d{yyyy-MM, aux}/all.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<!--单个文件的最大大小-->
<maxFileSize>64MB</maxFileSize>
<!--日志文件保留天数-->
<maxHistory>40</maxHistory>
</rollingPolicy>
</appender>
最后搞好后,我们再查看下plumeLog日志,会得到这样的效果:

请求接口URL、参数、返回结果,以及请求接口耗时 查看都一目了然!!


跨多服务的调用链也能看的非常清楚