Coder Social home page Coder Social logo

aop-log's Introduction

AopLog

AopLog是基于SpringAop和ThreadLocal实现的一个对请求方法埋点信息收集与处理的日志工具包。

设计目的和场景 :

  • 使用Spring AOP拦截方法参数大部分做法基本上大同小异,不用每个项目工程都写AOP拦截处理日志的代码,引入此包即可。
  • 可获取埋点方法的请求参数,响应参数,请求头,以及内部耗时,方法是成功还是失败,自定义步骤记录等等信息。
  • 整个方法完整过程只产生一个埋点信息记录(一个LogData对象),比如@Controller中一次完整的http请求。
  • 收集情况可选,可只在异常时执行收集过程(有些只是为了排查问题打印的日志,程序正常运行时其实毫无意义)。
  • 埋点信息收集,自行实现收集过程,比如埋点日志打印,常见埋点日志写入数据库,写入到文件,写入队列等等。
  • 埋点信息收集不干扰埋点方法正常流程,收集过程异步化处理(默认,可通过注解的asyncMode进行设置),不影响正常请求方法的性能与响应。
  • 只需通过@AopLog注解(或者自定义切面)决定是否埋点收集。

快速开始

项目通过Maven仓库地址 的pom.xml引入。

<dependency>
    <groupId>com.github.ealenxie</groupId>
    <artifactId>aop-log</artifactId>
    <version>2.5</version>
</dependency>

或者通过gradle引入

compile group: 'com.github.ealenxie', name: 'aop-log', version: '2.5'

@AopLog注解使用,进行埋点收集

直接在类(作用类的所有方法)或类方法(作用于方法)上加上注解@AopLog,进行埋点收集

例如 :

@AopLog(type = "测试接口", stackTraceOnErr = true)
@RestController
public class AppController {

    @GetMapping("/app/sayHello")
    public RespBody<String> sayHello() {
        return RespBody.ok("hello EalenXie");
    }

}

自定义全局的日志收集器实现收集 LogCollector

例如只是简单打印,或写入到库等等。(例子只是提供参考说明,收集过程请不要完全照搬,没有考虑到LogData不能被序列化的情况)

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.LogData;
import com.github.collector.LogCollector;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * @author EalenXie create on 2020/9/15 13:46
 * 此为样例参考
 * 配置一个简单的日志收集器 这里只是做了一个log.info打印一下,可以在这里写入到数据库中或者写入
 */
@Slf4j
@Component
public class AopLogCollector implements LogCollector {
    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void collect(LogData logData) {
        try {
            log.info(objectMapper.writeValueAsString(logData));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }
}

配置@Component的全局日志收集器只能配置一个。

接口调用 /say/hello 测试即可看看到控制台打印出结果 :

2020-09-16 16:01:04.782  INFO 2012 --- [AsyncExecutor-2] name.ealen.infra.advice.AopLogCollector  : {"appName":"app-template","host":"127.0.0.1","port":8080,"clientIp":"192.168.110.1","reqUrl":"http://localhost:8080/app/sayHello","httpMethod":"GET","headers":{"User-Agent":"Apache-HttpClient/4.5.10 (Java/11.0.5)"},"type":"测试","content":"","method":"name.ealen.api.facade.AppController#sayHello","args":null,"respBody":{"code":"200","desc":"OK","message":"请求成功","dateTime":"2020-09-16 16:01:04","body":"hello EalenXie"},"logDate":1600243264780,"costTime":1,"threadName":"http-nio-8080-exec-3","threadId":33,"success":true}

埋点日志对象LogData属性说明

LogData 埋点日志对象获取的内容

字段 类型 注释
appName String 应用名称
host String 主机
port int 端口号
clientIp String 请求客户端的Ip
reqUrl String 请求地址
headers Object 请求头部信息(可选择获取) 默认获取user-agent,content-type
tag String 操作标签,默认值undefined
content String 方法步骤内容,默认是空,可使用LogData.step进行内容步骤记录
method String 请求的本地java方法
args Object 方法请求参数
respBody Object 方法响应参数
costTime long 整个方法内部耗时
logDate Date Log产生时间,LogData对象初始化的时间
threadName String 线程名称
threadId long 线程Id
success boolean 执行状态,成功(true)/异常(false)

AopLog 注解选项说明

选项 类型 说明 默认
logOnErr boolean 仅当发生异常时才收集 false
tag String 操作标签 默认值"undefined"
headers String[] 获取的header信息 ,选择要获取哪些header信息 默认"User-Agent","content-type"
args boolean 是否获取请求参数 true
respBody boolean 是否获取响应参数 true
stackTraceOnErr boolean 当目标方法发生异常时,是否追加异常堆栈信息到LogData的content中 false
asyncMode boolean 异步方式收集 true
collector Class<? extends LogCollector> 指定日志收集器 默认不调整收集器,使用全局的日志收集器

LogData的step方法。

记录步骤。(如果某些重要步骤希望被记录下来) 例如 :

import com.github.AopLog;
import com.github.LogData;
import name.ealen.infra.base.resp.RespBody;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;


/**
 * @author EalenXie create on 2020/6/22 14:28
 */
@AopLog(tag = "测试", stackTraceOnErr = true)
@RestController
public class AppController {


    @GetMapping("/app/sayHello")
    public RespBody<String> sayHello() {
        LogData.step("1. 第一步执行完成");
        //......
        LogData.step("2. 第二步执行完成");
        //.....
        LogData.step("3. service的方法执行完成");
        //.....
        return RespBody.ok("hello EalenXie");
    }

}

此时再次接口调用 /say/hello 测试即可看看到控制台打印出结果,重点观察content字段 :

2020-09-16 17:26:20.285  INFO 3284 --- [AsyncExecutor-2] name.ealen.infra.advice.AopLogCollector  : {"appName":"app-template","host":"127.0.0.1","port":8080,"clientIp":"192.168.110.1","reqUrl":"http://localhost:8080/app/sayHello","httpMethod":"GET","headers":{"User-Agent":"Apache-HttpClient/4.5.10 (Java/11.0.5)"},"tag":"测试","content":"1. 第一步执行完成\n2. 第二步执行完成\n3. service的方法执行完成\n","method":"name.ealen.api.facade.AppController#sayHello","args":null,"respBody":{"code":"200","desc":"OK","message":"请求成功","dateTime":"2020-09-16 17:26:20","body":"hello EalenXie"},"logDate":1600248380283,"costTime":1,"threadName":"http-nio-8080-exec-2","threadId":32,"success":true}
"content":"1. 第一步执行完成\n2. 第二步执行完成\n3. service的方法执行完成\n"

不通过@AopLog注解,通过自定义切面进行收集

自定义切面注入AopLogProcessor,调用proceed(config, point)即可

import com.github.AopLogConfig;
import com.github.AopLogProcessor;
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.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * Created by EalenXie on 2021/7/14 10:29
 * 自定义切面
 */
@Aspect
@Component
public class CustomLogDataAspect {

    @Resource
    private AopLogProcessor aopLogProcessor;
    private static final AopLogConfig CONFIG;

    static {
        CONFIG = new AopLogConfig();
        CONFIG.setTag("操作标签");
        CONFIG.setStackTraceOnErr(false);
        CONFIG.setHeaders(new String[]{"content-type", "user-agent"});
    }

    // 自定义切点 execution(public * com.test.web.TestController.*(..))
    @Pointcut("execution(public * com.test.web.TestController.*(..))")
    public void test() {
        //ig
    }

    // 请使用环绕通知 @Around()
    @Around("test()")
    public Object note(ProceedingJoinPoint point) throws Throwable {
        return aopLogProcessor.proceed(CONFIG, point);
    }
}

Change Notes:

有关更改的详细信息,请参阅发布说明

aop-log's People

Contributors

ealenxie avatar lian233 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

aop-log's Issues

LogData对象里面args对象自动toString

image
在与前端调试的时候发现,LogData对象里面args参数,是String类型的,而不是实际的它对象类型。像是对象toString后的结果。用postman调试的时候是正常的,什么类型就是什么类型,不会是toString后的结果。
请教一下~

不打印任何日志

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.4'
    id 'io.spring.dependency-management' version '1.1.4'
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    runtimeOnly 'com.mysql:mysql-connector-j'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    // extend
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
    // Sa-Token 权限认证,在线文档:https://sa-token.cc
    implementation 'cn.dev33:sa-token-spring-boot3-starter:1.37.0'
    implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final'
    implementation 'com.alibaba.fastjson2:fastjson2:2.0.45'
    implementation 'com.github.ealenxie:aop-log:2.5'

}

@Slf4j
@Component
public class AopLogController implements LogCollector {
    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void collect(LogData logData) {
        try {
            log.info(objectMapper.writeValueAsString(logData));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            log.error("333");
        }
    }
}

@Slf4j
@Tag(name = "项目管理相关接口")
@RequiredArgsConstructor
@RequestMapping("/project/")
@RestController
@AopLog(tag = "测试接口")
public class ProjectController {


    private final IProjectService projectService;

    @GetMapping("/list")
    private R list(){

        return R.success( projectService.findAll());
    }

    @PostMapping("/submit")
    private R submit(ProjectBaseDTO dto){
        LogData.step("用户添加项目");
        return R.success(projectService.save(dto));
    }


    @PostMapping("/remove")
    private R remove(){
        return  R.success();
    }
}

没有打印日志

解决No thread-bound request found: Are you referring to request attributes outside of an actual web request

DataExtractor代码的getRequest()方法中
在其中使用RequestContextHolder来获取request信息,发现异步调用时,主线程结束后,子线程就获取不到request,会报以上错误信息。
""""
com.fasterxml.jackson.databind.JsonMappingException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request. (through reference chain: com.github.LogData["args"]->com.google.gson.internal.LinkedTreeMap[3]->com.google.gson.internal.LinkedTreeMap["request"]->com.sun.proxy.$Proxy163["secure"])
""""
解决办法是: .在开启新线程之前,将servletRequestAttributes设置为子线程共享

/**
 * 获取HttpServletRequest对象
 *
 * @return HttpServletRequest
 */
public static HttpServletRequest getRequest() {
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    RequestContextHolder.setRequestAttributes(attributes,true);//设置子线程共享
    
    return attributes != null ? attributes.getRequest() : null;
}

日志输出

那如果不加注解的全部输出日志,加注解的不输入日志怎么写

traceId

可以增加traceId功能吗?

怎样获取logData中的Header对象内容

请教一下,在collect里面怎样获取logData中的header内容,header类似是Object,请教一下怎样转换?如header中有自定义的字段,例如"test": "123",怎样获取"test"的值。另外,想获取header其他自定义字段的值,如何设置?

@slf4j
@component
public class AopLogCollector implements LogCollector {
private ObjectMapper objectMapper = new ObjectMapper();

@Override
public void collect(LogData logData) {
    try {
        log.info(objectMapper.writeValueAsString(logData));
    } catch (JsonProcessingException e) {
        e.printStackTrace();
    }
}

}

多层调用有bug

调用链: ServiceA -> ServiceB -> ServiceC

@AopLog(type = "ServiceA", stackTraceOnErr = true)
@service
public class ServiceA implements IServiceA {

@resource
private IServiceB serviceB;

public void execute() {

LogData.step("A-Begin");
...
serviceB.execute();
LogData.step("A-End");

}
}

@AopLog(type = "ServiceB", stackTraceOnErr = true)
@service
public class ServiceB implements IServiceB {

@resource
private IServiceC serviceC;

public void execute() {

LogData.step("B-Begin");
...
serviceC.execute();
LogData.step("B-End");

}
}

@AopLog(type = "ServiceC", stackTraceOnErr = true)
@service
public class ServiceC implements IServiceC {

LogData.step("C-Begin");
...
LogData.step("C-End");
}

结果:

日志 B-End、C-End丢失

优化建议

1、经测试,spring spel 表达式对性能影响很大,在tag值为默认值undefined的情况下,该代码会使性能下降40%左右
data.setTag(elSupporter.getByExpression(signature.getMethod(), point.getTarget(), point.getArgs(), aopLog.getTag()).toString());

建议修改如下
if (!aopLog.getTag().equals("undefined")) {
data.setTag(elSupporter.getByExpression(signature.getMethod(), point.getTarget(), point.getArgs(), aopLog.getTag()).toString());
}

2、另,默认的异步线程池队列,对比log4j2的disruptor异步处理性能差异巨大,在比较 asyncMode==true && log4j2 asyn==true 和
asyncMode==false && log4j2 asyn==true的情况下,开启aop-log的异步日志记录对性能的损耗大约占到20%

Bug提交

AopLogProcessor中 if (!aopLog.isLogOnErr() || !data.isSuccess()) 该判断是否是一个bug,理论上应该是 if (!aopLog.isLogOnErr() && !data.isSuccess()) 的关系

Before the method

需要在方法执行之前执行一些操作,目前支持吗?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.