项目请求日志详细调用打印
项目中经常需要通过日志定位线上问题,详细的日志有助于快速定位问题,下面通过aop
和MDC
解决.
打印详细请求信息
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
基本请求信息
// 使用目标类进行日志打印
logger = LoggerFactory.getLogger(joinPoint.getTarget().getClass().getName());
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 打印请求相关参数
logger.info("========================================== Start ==========================================");
// 打印请求 url
logger.info("URL : {}", request.getRequestURL().toString());
// 打印 Http method
logger.info("HTTP Method : {}", request.getMethod());
// 打印调用 controller 的全路径以及执行方法
logger.info("Class Method : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
// 打印请求的 IP
logger.info("IP : {}", (request.getHeader("X-Forwarded-For") == null) ? request.getRemoteAddr() : request.getHeader("X-Forwarded-For"));
// 打印请求Cookie
logger.info("Cookie : {}", cookie(request.getCookies()));
// 打印请求入参 https://blog.csdn.net/weixin_44130081/article/details/98847575
logger.info("Request Args : {}", Arrays.toString(joinPoint.getArgs()));
- 打印请求IP: 支持代理IP打印
- 打印请求入参: 入参没有使用json工具(GSON和FastJSON)进行封装,当请求方法有
HttpServletResponse
参数,比如做文件上传或者excel导入时,会造成内存溢出.可以使用Arrays.toString
打印
异常信息打印
// 打印异常
logger.info("{} : {}", e.getClass().getName(), e.getMessage());
出参和请求耗时打印
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
// 打印出参
GsonBuilder gsonBuilder = new GsonBuilder();
// 处理null值 https://www.jianshu.com/p/83eb1b2bc119
gsonBuilder.serializeSpecialFloatingPointValues();
Gson gson = gsonBuilder.create();
logger.info("Response Args : {}", gson.toJson(result));
// 执行耗时
logger.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);
日志追踪
日志调用追踪
一次请求,后台会产生一连串的方法调用.如何判断在一次请求中,都调用那些方法,找出对应的日志记录,就需要对打印日志进行线程标记.
- 在
resources
目录下新建:logback-spring.xml
,修改<pattern>
规则,示例:
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{trace_uuid}] [%level] [%thread] [%c:%L] - %msg%n</pattern>
- {trace_uuid}: 唯一编码
- 在请求执行前,存入当前线程的
MDC
值.比如使用aop的@Before
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) {
// 开始打印请求日志
MDC.put("trace_uuid", UUID.randomUUID().toString());// 加入trace追踪
}
方法调用追踪
在维护老项目或接手一个新项目时,一个方法可能在很多处被调用,对分析不熟悉的业务逻辑造成很大的困难.方法调用链可以清楚显示该方法被调用的路径
- 自定义注解
@LogTrace
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LogTrace {
}
- aop对注解进行解析
@After("@annotation(com.lyf.annotation.LogTrace)")
public void log_trace(JoinPoint joinPoint) {
JavaLangAccess access = SharedSecrets.getJavaLangAccess();
Throwable throwable = new Throwable();
int depth = access.getStackTraceDepth(throwable);
Class clazz = joinPoint.getTarget().getClass();
String targetName = clazz.getSimpleName();
String methodName = joinPoint.getSignature().getName();
String target = targetName + "." + methodName;
logger.info("====== StackTrace: " + target + " ======");
for (int i = depth - 1; i > 0; i--) {//从1开始不显示当前工具类调用
StackTraceElement ste = access.getStackTraceElement(throwable, i);
// for(int j=1;j<depth-i;j++){
// System.out.print("\t");//缩进
// }
String className = ste.getClassName();
if (className.contains("lyf") && !className.contains("$$") && !className.contains("aop")) {
logger.info(String.format("[%s.%s:%s]", ste.getClassName(), ste.getMethodName(), ste.getLineNumber()));
}
}
logger.info("====== StackTrace: " + target + " ======");
}
- 使用示例
@LogTrace
public User queryUser (Integer id){
return userMapper.queryById(id);
}
完整代码
logback-spring.xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<!-- 控制台输出日志 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{trace_uuid}] [%level] [%c:%L] - %msg%n</pattern>
</encoder>
</appender>
<!--每天生成一个日志文件,保存30天的日志文件。-->
<appender name="DayFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>logs/log.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/log.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{trace_uuid}] [%level] [%thread] [%c:%L] - %msg%n</pattern>
</encoder>
</appender>
<!--指定logger name为包名或类全名 指定级别 additivity设置是否传递到root logger -->
<logger name="slf4j" level="INFO" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="DayFile"/>
</logger>
<!--slf4j2包下的类在ERROR级别时候传递到root logger中-->
<logger name="slf4j2" level="ERROR"/>
<!--根logger控制-->
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="DayFile"/>
</root>
</configuration>
WebLogAspect.java
package com.lyf.aop;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import sun.misc.JavaLangAccess;
import sun.misc.SharedSecrets;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.UUID;
@Aspect
@Component
public class WebLogAspect {
private static Logger logger;
/**
* 以 controller 包下定义的所有请求为切入点
*/
@Pointcut("execution(public * com.lyf.controller..*.*(..))")
public void webLog() {
}
/**
* 在切点之前织入
*
* @param joinPoint
* @throws Throwable
*/
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) {
// 开始打印请求日志
MDC.put("trace_uuid", UUID.randomUUID().toString());// 加入trace追踪
// 使用目标类进行日志打印
logger = LoggerFactory.getLogger(joinPoint.getTarget().getClass().getName());
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 打印请求相关参数
logger.info("========================================== Start ==========================================");
// 打印请求 url
logger.info("URL : {}", request.getRequestURL().toString());
// 打印 Http method
logger.info("HTTP Method : {}", request.getMethod());
// 打印调用 controller 的全路径以及执行方法
logger.info("Class Method : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
// 打印请求的 IP
logger.info("IP : {}", (request.getHeader("X-Forwarded-For") == null) ? request.getRemoteAddr() : request.getHeader("X-Forwarded-For"));
// 打印请求Cookie
logger.info("Cookie : {}", cookie(request.getCookies()));
// 打印请求入参 https://blog.csdn.net/weixin_44130081/article/details/98847575
logger.info("Request Args : {}", Arrays.toString(joinPoint.getArgs()));
}
/**
* 在切点之后织入
*
* @throws Throwable
*/
@After("webLog()")
public void doAfter() throws Throwable {
logger.info("=========================================== End ===========================================");
// 每个请求之间空一行
logger.info("☜☝☞");
}
/**
* 在切点异常之后织入
*
* @throws Throwable
*/
@AfterThrowing(value = "webLog()", throwing = "e")
public void doAfterThrowing(Exception e) throws Throwable {
// 打印异常
logger.info("{} : {}", e.getClass().getName(), e.getMessage());
}
/**
* 环绕
*
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around("webLog()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
// 打印出参
GsonBuilder gsonBuilder = new GsonBuilder();
// 处理null值 https://www.jianshu.com/p/83eb1b2bc119
gsonBuilder.serializeSpecialFloatingPointValues();
Gson gson = gsonBuilder.create();
logger.info("Response Args : {}", gson.toJson(result));
// 执行耗时
logger.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);
return result;
}
private String cookie(Cookie[] cookies) {
StringBuilder stringBuilder = new StringBuilder();
if (cookies != null && cookies.length > 0) {
stringBuilder.append("[");
for (int i = 0; i < cookies.length; i++) {
if (i > 0) {
stringBuilder.append(", ");
}
stringBuilder.append(cookies[i].getName()).append("=").append(cookies[i].getValue());
}
stringBuilder.append("]");
}
return stringBuilder.toString();
}
@After("@annotation(com.lyf.annotation.LogTrace)")
public void log_trace(JoinPoint joinPoint) {
JavaLangAccess access = SharedSecrets.getJavaLangAccess();
Throwable throwable = new Throwable();
int depth = access.getStackTraceDepth(throwable);
Class clazz = joinPoint.getTarget().getClass();
String targetName = clazz.getSimpleName();
String methodName = joinPoint.getSignature().getName();
String target = targetName + "." + methodName;
logger.info("====== StackTrace: " + target + " ======");
for (int i = depth - 1; i > 0; i--) {//从1开始不显示当前工具类调用
StackTraceElement ste = access.getStackTraceElement(throwable, i);
// for(int j=1;j<depth-i;j++){
// System.out.print("\t");//缩进
// }
String className = ste.getClassName();
if (className.contains("lyf") && !className.contains("$$") && !className.contains("aop")) {
logger.info(String.format("[%s.%s:%s]", ste.getClassName(), ste.getMethodName(), ste.getLineNumber()));
}
}
logger.info("====== StackTrace: " + target + " ======");
}
}