本文档讲述内容与代码示例可参见 Shoulder-Demo1 |
能力激活方式:
<dependency>
<groupId>cn.itlym</groupId>
<artifactId>shoulder-starter-operation-log</artifactId>
</dependency>
compile 'cn.itlym:shoulder-starter-operation-log'
AOP 操作日志 @OperationLog
基本使用
Shoulder
为方便地记录操作日志,提供了 @OperationLog
@OperationLogParam
注解,使用示例如下
@RestController
@RequestMapping("user")
public class MyUserController {
@OperationLog(operation = "testOpLogAnnotation")
@GetMapping("testOpLogAnnotation")
public String register(
// 参数未加注解,不会记录到操作日志
@Cookie String cookie,
// 参数名默认为变量名 param0
@OperationLogParam String param0,
// 设置支持多语言,自定义参数名
@OperationLogParam(supportI18n = true, name = "myParam") String param1) {
return "ok";
}
}
该写法日志记录示例:
2024-06-18 18:22:27.953 INFO [39536] --- [ulder-opLogger1] SHOULDER-OPERATION : userId:"system.demo1.0",terminalAddress:"127.0.0.1",terminalId:"349D273CCE05DBAC0565374297BA1654",terminalInfo:"Mozilla/5.0 (Macintosh; Intel Mac OS X …",operation:"test",tenantCode:"DEFAULT",appId:"demo1",terminalType:"1",result:"0",operationTime:"2024-06-18T18:22:23.625 +0800",endTime:"2024-06-18T18:22:23.652 +0800",userAgent:"..."
若使用 @OperationLogParam
,日志将额外记录以下内容:
params:"[{"name"="testOpLogAnnotation.param0", "value"=""},{"name"="testOpLogAnnotation.myParam", "value"=""}]"
若带有 @OperationLog 的方法中发生异常,Shoulder 会自动修改日志记录状态为 success=false
|
@OperationLog 默认只记录方法中携带 @OperationLogParam 声明的参数,您可以通过配置 logAllParams = true 来记录所有参数, 示例: @OperationLog(operation = "xxx", logAllParams = true) 。
|
若您想修改日志打印格式,请参考进阶部分 [自定义:操作日志格式] |
操作日志 AOP 是基于 Spring AOP 实现的,所以遵循 Spring AOP 使用方式,比如注解必须加在 @Bean 类型对象的非 static 的 public 方法上,且对于 this.xxxMethod 是无效的(this. 写法未过 Spring 的代理)。简单起见,建议初学者在 Controller 对外暴露的接口方法上使用 @OperationLog 注解。
|
填充更多字段
可以在有日志上下文的方法中使用 OpLogContextHolder.getLog()
获取当前的操作日志对象,以实现更多操作。
@OperationLog(operation = "testLogObj")
@GetMapping("testLogObj")
public String testLogObj() {
// 从日志上下文中拿出日志 DTO,getObjectType 获取类型
OpLogContextHolder.getLog().setXXX();
return OpLogContextHolder.getLog().getObjectType();
}
OpLogContextHolder.getLog() 方法是取 ThreadLocal 中的日志对象,若线程上下文中 logDTO 为 null (如误清理、@OperationLog 嵌套使用),则将会出现 NullPointException 异常!
|
完整的操作日志
可参考 日志记录规范-操作日志 中定义的字段:
必填的字段已经在第一列加粗。 |
字段名称 | 长度 | 必填 | 说明 |
---|---|---|---|
操作者相关信息 |
|||
userId |
128 |
是 |
用户标识(如用户名、userId);为系统内部操作时(如定时任务、接收消息通知),填写执行操作的服务实例名,格式为 |
userName |
128 |
否 |
用户昵称 |
personId |
64 |
否 |
用户对应人员标识 |
userOrgId |
128 |
否 |
用户所属 |
userOrgName |
255 |
否 |
用户组名称。 |
terminalType |
1 |
是 |
操作者所使用终端的类别 |
ip |
255 |
否 |
操作者所在机器 |
terminalId |
128 |
否 |
操作者所使用终端的唯一标识:如 |
terminalInfo |
256 |
否 |
操作者所使用终端详情信息,如浏览器中的 |
操作动作相关信息 |
|||
operation |
255 |
是 |
操作动作标识。 |
operationTime |
128 |
是 |
操作时间,格式为 |
params |
- |
否 |
操作对应业务方法/接口的入参,一般关键/特殊业务才会使用。 |
result |
1 |
是 |
业务操作结果: |
errorCode |
32 |
否 |
当操作结果为失败时,记录操作失败对应的错误码。 |
detailKey |
128 |
否 |
操作详情( 支持 多语言时填写),多语言 key 格式: |
detailItems |
- |
否 |
操作详情占位参数( 支持 多语言时填写),用于填充 |
detail |
4096 |
否 |
操作详情( 不支持 多语言时填写)详细的描述用户的操作内容,也可填写被操作对象的 |
被操作对象相关信息 |
|||
objectType |
128 |
否 |
操作对象的类型标识。 |
objectId |
128 |
否 |
操作对象的标识,若存在多个值时, 以 |
objectName |
255 |
否 |
操作对象的名称,若存在多个值时,以 |
其他信息 |
|||
businessId |
128 |
否 |
存放与本次操作所关联其他业务操作的业务号。用于多个请求完共同完成一个业务功能时。如:上传csv进行数据的批量导入场景:上传导入文件、校验导入数据、点击确认导入、导入成功业务相关可以填同一个标识符 |
appId |
128 |
是 |
服务唯一标识。 |
traceId |
128 |
否 |
调用链标识。 |
自定义扩展字段 |
- |
否 |
扩展字段,用于个别服务需要,一般留空。 |
代码中可参考 OperationLogDTO 。
|
国际化
您可使用特殊格式的 i18nKey
代替实际文本,并在展示时还原为需要的语言。操作日志中以下字段支持国际化。
字段名称 | 说明 | 建议方案 |
---|---|---|
operation |
操作动作标识 |
支持多语言时添加特定前缀,如 |
objectType |
被操作对象类型 |
支持多语言时添加特定前缀,如 |
detail |
操作详情 |
使用 |
terminalType |
终端类型 |
采用枚举/数据字典,展示层翻译 |
result |
操作结果 |
采用枚举/数据字典,展示层翻译 |
appId |
应用标识 |
结合基本规范,采用数据字典,展示层翻译 |
errorCode |
错误码 |
结合错误码规范,给出错误原因和排查建议 |
operationTime |
操作时间 |
结合国际化规范,时间格式可改变 |
public static final String OPERATION_CREATE_USER = "op.op.user.create.i18n";
@OperationLog(operation = OPERATION_CREATE_USER)
public void createUser() {
//... 操作日志中 operation 被记录为 op.op.user.create.i18n
}
参考 Shoulder-国际化说明,新增对应语言资源文件即可。
以中文为例,只需要在 reousrce/language/zh_CN/userModule.properties
中加入对应配置即可在查询时做对应的翻译展示,其他语言同理。
op.op.user.create.i18n=创建用户
进阶使用
以下部分适合有一定开发经验的进阶开发者,会涉及一些 Java 、Spring Boot 的进阶知识点。
|
简化代码
Shoulder
提供了一些接口,合理使用可大幅简化复杂操作下的业务代码,代码对比:
以货物管理场景为例,操作员通过系统从仓库中出库一个货物,希望系统完整记录操作员信息、货物信息、操作时间、操作结果、操作员所在设备信息、请求 traceId
内容。
以下两种方式都可以正确记录这些信息:
@OperationLog(operation = "出货")
public OutboundResult outbound(User operator, Goods goods, HttpServletRequest request) {
OutboundResult result = goodsService.outbound(goods);
OpLogContextHolder.getLog()
// 填写用户、部门信息
.setUserId(operator.getUserId())
.setUserName(operator.getUserName())
.setUserOrgId(operator.getUserOrgName())
.setUserOrgName(operator.getUserOrgName())
// 操作对象
.setObjectId(goods.getId())
.setObjectName(goods.getName())
.setObjectType(goods.getType());
return result;
}
@OperationLog(operation = "出货")
public void outbound(User operator, Goods goods, HttpServletRequest request) {
OutboundResult result = goodsService.outbound(goods);
OpLogContextHolder.setOperator(operator);
OpLogContextHolder.setOperableObject(goods);
// 操作人设备信息、处理应用、操作结果等信息将由框架自动填充,无需填写
return result;
}
// 省略 UserI implements Operator...
// 省略 Goods implements Operable...
合理使用这些接口可大幅减少您的代码~ Operable 、OperateResult 、OperationDetailAble 、Operator 、OperateRecord
|
多操作对象 & 批量记录
场景举例:
场景1:某一接口,允许创建一个用户,在接口调用完毕后,需要记录一条操作日志,保存此次操作信息,同时记录该用户的id信息。 场景2:某一接口,允许批量添加 10 个用户,在接口调用完毕后,需要记录一条操作日志,保存此次操作信息,同时记录这 10 个用户的id信息。 场景3:某一接口,允许批量添加/导入 100 个用户,在接口调用完毕后,需要记录 |
Shoulder
@OperationLog
可以做到一个方法添加注解,共记录 M
个操作对象,N
条操作日志,每条日志记录 M / N
个操作对象。
跨线程使用
借助 Shoulder-Core
中的 ThreadEnhancer
,可实现操作日志跨线程使用!
开启跨线程后若只需要子线程记录日志,主线程不需要记录,可以调用 OpLogContextHolder.disableAutoLog() 关闭本次日志记录。
|
@Service
public class DemoService {
@Async // Spring 的异步注解
public void asyncTest() {
Thread.sleep(2000);
}
}
@Service
public class OperationDemoService {
@Autowired
private DemoService demoService;
@Autowired
@Qualifier("shoulderThreadPool") // 这里显式使用了 shoulder 线程池,若希望使用自定义线程池,要确保自己的线程池已作为 Bean 被 Spring 管理
Executor shoulderThreadPool;
@OperationLog(operation = "asyncTest")
public String async() {
demoService.asyncTest();
// 将在 demoService.asyncTest 方法结束后会打印操作日志
return "return~";
}
@OperationLog(operation = "threadpoolTest")
public String threadpoolTest() {
shoulderThreadPool.asyncTest();
// 将在 demoService.asyncTest 方法结束后会打印操作日志
return "return~";
}
shoulderThreadPool.execute(xxx)
不同线程之间操作也是线程安全的,操作日志线程变量拷贝使用了深克隆,父线程修改操作日志内容,不会影响子线程的日志内容,反之亦然。 |
切换存储
存储至数据库
请确保数据库相关依赖已引入并成功连接数据库,并在您数据库中传教完毕表 log_operation
。
CREATE TABLE IF NOT EXISTS log_operation
(
id bigint auto_increment comment '主键' primary key,
app_id varchar(32) not null comment '应用id',
version varchar(64) null comment '应用版本',
instance_id varchar(64) null comment '操作服务器节点标识(支持集群时用于定位具体哪台服务器执行)',
user_id varchar(64) not null comment '用户标识',
user_name varchar(64) null comment '用户名',
user_real_name varchar(128) null comment '用户真实姓名',
user_org_id varchar(64) null comment '用户组标识',
user_org_name varchar(64) null comment '用户组名',
terminal_type int not null comment '终端类型。0:服务内部定时任务等触发;1:浏览器;2:客户端;3:移动App;4:小程序。推荐前端支持多语言',
terminal_address varchar(64) null comment '操作者所在终端地址,如 IPv4(15) IPv6(46)',
terminal_id varchar(64) null comment '操作者所在终端标识,如PC的 MAC;手机的 IMSI、IMEI、ESN、MEID;甚至持久化的 UUID',
terminal_info varchar(255) null comment '操作者所在终端信息,如操作系统类型、浏览器、版本号等',
object_type varchar(128) null comment '操作对象类型;建议支持多语言',
object_id varchar(128) null comment '操作对象id',
object_name varchar(128) null comment '操作对象名称',
operation_param text null comment '触发该操作的参数, json 格式',
operation varchar(128) not null comment '操作动作;建议支持多语言',
detail text null comment '操作详情。详细的描述用户的操作内容、json对象,仅在深入排差时查看',
detail_i18n_key varchar(128) null comment '操作详情对应的多语言key',
detail_i18n_item varchar(255) null comment '填充 detail_i18n_key 对应的多语言翻译。数组类型',
result int not null comment '操作结果,0成功;1失败;2部分成功;建议支持多语言',
error_code varchar(32) null comment '错误码',
operation_time timestamp not null comment '操作触发时间,注意采集完成后替换为日志服务所在服务器时间',
end_time timestamp null comment '操作结束时间',
duration bigint null comment '操作持续时间,冗余字段,单位 ms',
trace_id varchar(64) null comment '调用链id',
relation_id varchar(64) null comment '关联的调用链id/业务id',
tenant_code varchar(20) default '' null comment '租户编码',
create_time timestamp default CURRENT_TIMESTAMP null comment '数据入库时间',
update_time timestamp default CURRENT_TIMESTAMP null comment '数据更新时间,日志表在非必要的订正前提下,一般不更新',
extended_field0 varchar(1024) null,
extended_field1 varchar(1024) null,
extended_field2 varchar(1024) null,
extended_field3 varchar(1024) null,
extended_field4 varchar(1024) null
)
comment '业务日志';
CREATE INDEX IF NOT EXISTS idx_operation_time
on log_operation (operation_time);
CREATE INDEX IF NOT EXISTS idx_terminal_address
on log_operation (terminal_address);
CREATE INDEX IF NOT EXISTS idx_trace_id
on log_operation (trace_id);
CREATE INDEX IF NOT EXISTS idx_user_id
on log_operation (user_id);
只需一项配置即可改为写到
shoulder.log.operation.logger.type=jdbc
shoulder:
log:
operation:
logger:
type: jdbc
完成设置后,操作日志将由 JdbcOperationLogger
保存至您的数据库内。
API 查询接口
数据库保存方式下,若您的依赖嗨包含 shoulder-web
,Shoulder
将自动开启日志查询接口(POST /api/v1/oplogs/page
),让搭建操作日志可视化页面更简单。
shoulder-web
依赖引入方式:在 pom.xml
中引入 shoulder-web-starter
Unresolved directive in shoulder_op_log.adoc - include::shoulder/modele/shoulder_web.adoc[tags=import_shoulder_web]
若您想调整接口路径或关闭该功能,修改相关配置即可
# 是否开启操作日志 api 查询接口,true 开启 false 关闭
shoulder.web.ext.oplog.enable=true
shoulder.web.ext.oplog.path=/api/v1/oplogs
shoulder:
web:
ext:
oplog:
# 是否开启操作日志 api 查询接口,true 开启 false 关闭
enable: true
path: /api/v1/oplogs
异步记录 & 缓冲记录
为提高记录性能,保障业务服务的吞吐,Shoulder
为您提供了“异步记录”与“缓冲记录”操作日志的能力,可在不影响主线程的情况下处理记录流程,若开启了“缓冲记录”,还会合并多条日志统一记录以减少 IO 操作。
开启方式:
# 是否以异步线程记录操作日志,默认开
shoulder.log.operation.logger.async=true
# 异步记录的线程数,默认1
shoulder.log.operation.logger.threadNum=1
# 异步记录的线程名
shoulder.log.operation.logger.threadName=shoulder-opLogger
# 是否开启缓冲记录,默认 false 不开启
shoulder.log.operation.logger.buffered=true
# 缓冲记录-缓冲区刷新间隔时间(满足后立即记录)
shoulder.log.operation.logger.flushInterval=10s
# 缓冲记录-当缓冲区日志积压数量超过多少会刷新缓冲区(满足后立即记录)
shoulder.log.operation.logger.flushThreshold=10
# 缓冲记录-日志记录器单次最大记录数,用于保护记录器,避免单次批量提交的压力过大
shoulder.log.operation.logger.perFlushMax=20
shoulder:
log:
operation:
logger:
# 是否以异步线程记录操作日志
async: true
# 异步记录的线程数
threadNum: 1
# 异步记录的线程名
threadName: shoulder-opLogger
# 是否开启缓冲记录,默认 false 不开启
buffered: true
# 缓冲记录-缓冲区刷新间隔时间(满足后立即记录)
flushInterval: 10s
# 缓冲记录-当缓冲区日志积压数量超过多少会刷新缓冲区(满足后立即记录)
flushThreshold: 10
# 缓冲记录-日志记录器单次最大记录数,用于保护记录器,避免单次批量提交的压力过大
perFlushMax: 20
关于操作日志记录器的配置类可参考 OperationLogProperties.LoggerProperties
|
二次加工:记录前拦截完善日志内容
Shoulder
允许您在记录日志前做拦截,以实现筛选日志或加工日志内容等操作,实现 OperationLoggerInterceptor
接口,并将实现类作为 @Bean
注入至 Spring
上下文中即可。
示例:
日志格式 & 采集分析
Shoulder
根据常用的日志加工组件及其扩展,默认提供了两种日志格式,并允许您自定义日志格式。
JSON 格式 JsonOperationLogFormatter
考虑到在计算资源紧张的服务器部署,同时又需要做日志分析的场景,如配合 Kibana
/ Prometheus
/ Loki
等技术做进一步分析,这时,JSON
格式的日志将节省一部分资源,且无需经过 LogStash
/ Fluent
等日志采集组件的加工即可提交给日志分析服务器,节省部分部署资源。
开启方式
自定义:操作日志格式
对于一些成熟组织,可能配置了统一的日志采集 / 解析规范或脚本,将日志格式转成已有的格式会更利于日志分析。
在修改日志格式时,请注意修改或关闭日志格式校验,否则会因格式错误而记录失败!请参考 日志格式 & 采集分析 部分介绍。 |
您可以实现 OperationLogParamValueConverter
接口,并将实现类作为 @Bean
注入至 Spring
上下文中即可。
示例:
规范日志格式
Shoulder
允许您为操作日志制定专门的规范,以在软件交付时需要确保日志格式符合规范,如 a 字段格式必须是xxx,b 字段长度必须在 xxx 以内。
日志格式校验 OperationLogValidator
ShoulderOperationLogValidator
是内置的日志校验器,校验规则参见 [_完整的操作日志],但未在框架中激活强制校验功能,若您想开启,请参考以下代码:
@Configuration
public class OpLogConfiguration {
@Bean
public OperationLogValidator operationLogValidator() {
return new OperationLogValidator();
}
}