摘要:业务系统中难免要记录敏感操作,可以通过硬编码或者简单注解实现,但是都不够优雅,今天分享一个自己写的日志工具。
这是参考美团的一篇文章所实现的日志工具,能够灵活地记录操作日志,并且具有较好的扩展性,欢迎尝鲜。下面是这个工具的介绍和使用教程。
代码仓库:https://github.com/elltor/oplog
预览效果图:
功能
- 通过表达式解析生成美观的表达式,支持解析入参、自定义变量、返回值(_ret)、错误信息(_errMsg)
- 可以通过条件控制是否记录日志
- 提供自定义函数扩展
- 提供自定获取上下文用户扩展
- 支持嵌套、支持多线程使用情景
使用和效果预览:
1/**
2 * 示例 1 - 基本使用
3 * 记录订单编号和返回值信息。
4 *
5 * 通过日志持久化实现类 {@link com.elltor.example.config.log.PersistenceLogServiceImpl } 打印结果
6 * 通过 {@link com.elltor.example.config.log.LogRecordOperatorGetImpl } 自动获取操作用户
7 * 通过自定义函数 {@link com.elltor.example.config.log.OrderDetailParseFunction } 解析订单详细信息
8 */
9// 打印日志注解
10@LogRecord(
11 // 必填,方法执行成功生成的模板(执行过程未捕获到异常), orderDetail为自定义函数名
12 success = "订单详细信息: {#orderDetail(#id)}, 执行状态码:{#_ret.status}",
13 // 可选,执行失败的模板(捕捉到错误)
14 fail = "获取订单失败 id: {#id}, 执行状态码: {#_ret.status}",
15 // 可选,业务id
16 bizNo = "{#id}",
17 // 可选,日志的分类
18 category = LogType.ORDER)
19@ApiOperation("获取订单信息")
20@GetMapping("/{id}")
21public Object getOrderById(@PathVariable("id") Long id)throws Exception{
22 // 模拟成功失败
23 Result success=new Result("success",200,orderService.getOrderById(id));
24 Result fail=new Result("ERROR",500,orderService.getOrderById(id));
25
26 // 随机成功失败
27 return new Random().nextInt(10)>=5?success:fail;
28 }
解析 @LogRecord 注解后的效果 :
1{
2 "success": "订单详细信息: 【 id : 1001订单名称:男士卫衣一件 地址:北京市海淀区 】, 执行状态码:200",
3 "fail": "获取订单失败 id: 1001, 执行状态码: 200",
4 "operator": "zhangsan13",
5 "bizNo": "1001",
6 "category": "order",
7 "detail": "",
8 "condition": "true",
9 "complete": true,
10 "timestamp": 1648710916438
11}
特性
-
便于使用。 提供
starter
, 通过注解开启功能并自动配置。 -
灵活。 工具提供了自定义函数、具有表达式解析的日志模版和注解层面的日志持久化控制。
-
可扩展。 提供了自定义函数、自定义日志操作用户、自定义持久化日志方式。
-
性能。 通过异步和缓存的方式提升处理性能。
业务逻辑架构
使用方法
前言
oplog-example
是一个演示模块,你可以通过这个项目快速了解工具的使用和功能。 启动项目后点击链接 http://localhost:8080/swagger-ui/index.html
访问 swagger
文档就可以尝试功能了。
1. 开始使用
开启日志记录功能 :
1
2@EnableLogRecord(tenant = "com.elltor.biz", mode = AdviceMode.PROXY)
3@SpringBootApplication
4public class Application {
5 //.....
6}
添加注解 @LogRecord
到你要记录日志的方法上。
被注解注释的方方法的入参和返回值将被作为日志模版的上下文。
1 @LogRecord(success = "查询了用户, 用户id : {#userid}", category = "user")
2public Object getUserByUserid(String userid){
3 return new Object();
4 }
好了,到这里你就可以使用基本的功能了。
2. 自定义日志持久化方式
基础父类 AbstractLogRecordService
并且实现持久化方法 record(Record record)
。
1
2@Component
3@Slf4j
4public class PersistenceLogServiceImpl extends AbstractLogRecordService {
5 @Override
6 public void record(Record record) {
7 log.info("example 包中 : {}", record);
8 }
9}
3. 自定义日志操作用户
实现接口 IOperatorGetService
:
1
2@Component
3public class LogRecordOperatorGetImpl implements IOperatorGetService {
4 @Override
5 public Operator getUser() {
6 User user = ContextHolder.currentUser();
7 Operator op = new Operator();
8 op.setUsername(user.getUsername());
9 op.setName(user.getName());
10 return op;
11 }
12}
4. 自定义解析函数
注意:
- 自定义的解析函数必须是静态方法. 如下面的
userDetail
方法。 - 返回值总是
String
类型的。
实现接口 IParseFunction
:
1
2@Component
3public class UserDetailFunction implements IParseFunction {
4
5 @Component
6 private UserDao userDao;
7
8 @Override
9 public Method functionMethod() {
10 Method method = null;
11 try {
12 method = UserDetailFunction.class.getDeclaredMethod("userDetail", String.class);
13 } catch (NoSuchMethodException e) {
14 e.printStackTrace();
15 }
16 return method;
17 }
18
19 // custom function
20 static String userDetail(String userid) {
21 User u = userDao.getUserByUserid(userid);
22 return u.getName() + " " + u.getSex() + " " + u.getAge();
23 }
24}
5. 使用约定
在注解 LogRecord
中的字段 :
- success : 成功方法模版,必填字段。
- condition : 是否记录日志。值为 boolean 值, 但类型为
String
。 - operator : 总是通过实现
IOperatorGetService
接口的类获取或者由你指定。 - fail : 用来记录方法执行失败的模版文案。
使用技术
- SpEL(Spring Expression Language)
- Spring Message Service
- Java Annotation
- Spring Boot Auto-configuration
实现参考
https://tech.meituan.com/2021/09/16/operational-logbook.html