项目请求日志详细调用打印

项目中经常需要通过日志定位线上问题,详细的日志有助于快速定位问题,下面通过aopMDC解决.

打印详细请求信息

<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);

日志追踪

日志调用追踪

一次请求,后台会产生一连串的方法调用.如何判断在一次请求中,都调用那些方法,找出对应的日志记录,就需要对打印日志进行线程标记.

  1. 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}: 唯一编码
  1. 在请求执行前,存入当前线程的MDC值.比如使用aop的@Before
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) {
    // 开始打印请求日志
    MDC.put("trace_uuid", UUID.randomUUID().toString());// 加入trace追踪
}

方法调用追踪

在维护老项目或接手一个新项目时,一个方法可能在很多处被调用,对分析不熟悉的业务逻辑造成很大的困难.方法调用链可以清楚显示该方法被调用的路径

  1. 自定义注解@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 {
}
  1. 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 + " ======");
}
  1. 使用示例
@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 + " ======");
    }
}
posted @ 2020-06-22 14:50  林宇风  阅读(1077)  评论(0编辑  收藏  举报