本文档讲述内容与代码示例可参见 Shoulder-Demo1

能力激活方式:

Maven
<dependency>
    <groupId>cn.itlym</groupId>
    <artifactId>shoulder-starter-operation-log</artifactId>
</dependency>
Gradle
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 类型对象的非 staticpublic 方法上,且对于 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 异常!

完整的操作日志

可参考 日志记录规范-操作日志 中定义的字段:

必填的字段已经在第一列加粗。
Table 1. 操作日志格式
字段名称 长度 必填 说明

操作者相关信息

userId

128

用户标识(如用户名、userId);为系统内部操作时(如定时任务、接收消息通知),填写执行操作的服务实例名,格式为 system.应用标识.实例标识

userName

128

用户昵称

personId

64

用户对应人员标识

userOrgId

128

用户所属 用户组群组部门 的编号。

userOrgName

255

用户组名称。

terminalType

1

操作者所使用终端的类别 0 表示 系统内部1 表示 浏览器2 表示 APP3 表示 PC客户端

ip

255

操作者所在机器 IP;系统内部操作时,填写服务所在机器 IP

terminalId

128

操作者所使用终端的唯一标识:如 MAC 地址。

terminalInfo

256

操作者所使用终端详情信息,如浏览器中的 UserAgent

操作动作相关信息

operation

255

操作动作标识。

operationTime

128

操作时间,格式为 年-月-日T时:分:秒.毫秒时区,如 2017-09-20T13:42:38.349+08:00

params

-

操作对应业务方法/接口的入参,一般关键/特殊业务才会使用。JSON 形式,见下方 操作日志参数格式

result

1

业务操作结果:0 表示 成功正确1 表示 失败不正确2 表示 部分成功

errorCode

32

当操作结果为失败时,记录操作失败对应的错误码。

detailKey

128

操作详情( 支持 多语言时填写),多语言 key 格式:op.detail.<操作内容标识>,支持占位符,采用 %1, %2, %n 形式,n表示第n个参数;不支持多语言时留空。

detailItems

-

操作详情占位参数( 支持 多语言时填写),用于填充 detailKey 的占位符。

detail

4096

操作详情( 不支持 多语言时填写)详细的描述用户的操作内容,也可填写被操作对象的 JSON 字符串。

被操作对象相关信息

objectType

128

操作对象的类型标识。

objectId

128

操作对象的标识,若存在多个值时, 以 , 分隔,如 [1,2,3,4,5]

objectName

255

操作对象的名称,若存在多个值时,以 , 分隔,如 [角色1、角色2]

其他信息

businessId

128

存放与本次操作所关联其他业务操作的业务号。用于多个请求完共同完成一个业务功能时。如:上传csv进行数据的批量导入场景:上传导入文件、校验导入数据、点击确认导入、导入成功业务相关可以填同一个标识符

appId

128

服务唯一标识。

traceId

128

调用链标识。

自定义扩展字段

-

扩展字段,用于个别服务需要,一般留空。

代码中可参考 OperationLogDTO

完整参数记录样例

示例:

自定义操作日志格式-校验

国际化

您可使用特殊格式的 i18nKey 代替实际文本,并在展示时还原为需要的语言。操作日志中以下字段支持国际化。

Table 2. 多语言字段对照表
字段名称 说明 建议方案

operation

操作动作标识

支持多语言时添加特定前缀,如 op.op.<操作动作标识>

objectType

被操作对象类型

支持多语言时添加特定前缀,如 op.objType.<操作对象类型标识>

detail

操作详情

使用 detailKey 作为多语言key,格式:op.detail.<操作内容标识>,detailItem 填充占位符

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=创建用户

基础配置

若想关闭操作日志相关所有功能,可通过设置 shoulder.log.operation.enable=false 实现。

若想让值为 null 的参数值输出自定义格式,如 "NUL",可设置 shoulder.log.operation.nullParamOutput=NUL


推荐阅读
快速上路指引
 可前往 目录页 查看 Shoulder 其他模块功能。
面向进阶使用者
 继续阅读本文档 以查看 Shoulder-操作日志 进阶能力。

进阶使用

以下部分适合有一定开发经验的进阶开发者,会涉及一些 JavaSpring 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...
合理使用这些接口可大幅减少您的代码~ OperableOperateResultOperationDetailAbleOperatorOperateRecord

多操作对象 & 批量记录

场景举例:

场景1:某一接口,允许创建一个用户,在接口调用完毕后,需要记录一条操作日志,保存此次操作信息,同时记录该用户的id信息。

场景2:某一接口,允许批量添加 10 个用户,在接口调用完毕后,需要记录一条操作日志,保存此次操作信息,同时记录这 10 个用户的id信息。

场景3:某一接口,允许批量添加/导入 100 个用户,在接口调用完毕后,需要记录 10 条操作日志,保存此次操作信息,同时以每条日志记录 10 个用户信息,分别记录这 100 个用户的id信息。(避免单条日志过长过大而无法采集或检索困难)

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)
不同线程之间操作也是线程安全的,操作日志线程变量拷贝使用了深克隆,父线程修改操作日志内容,不会影响子线程的日志内容,反之亦然。

切换存储

以日志形式输出【默认】

这是 Shoulder 的默认日志输出方式,实际输出目标会以日志系统为准,无需设置。

存储至数据库

请确保数据库相关依赖已引入并成功连接数据库,并在您数据库中传教完毕表 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);

只需一项配置即可改为写到

Properties
shoulder.log.operation.logger.type=jdbc
YAML
shoulder:
  log:
    operation:
      logger:
        type: jdbc

完成设置后,操作日志将由 JdbcOperationLogger 保存至您的数据库内。

API 查询接口

数据库保存方式下,若您的依赖嗨包含 shoulder-webShoulder 将自动开启日志查询接口(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]

若您想调整接口路径或关闭该功能,修改相关配置即可

Properties
# 是否开启操作日志 api 查询接口,true 开启 false 关闭
shoulder.web.ext.oplog.enable=true
shoulder.web.ext.oplog.path=/api/v1/oplogs
YAML
shoulder:
  web:
    ext:
      oplog:
        # 是否开启操作日志 api 查询接口,true 开启 false 关闭
        enable: true
        path: /api/v1/oplogs
空存储

shoulder.log.operation.logger.type=none

适合部分场景暂时不需要输出日志,但希望操作日志能随时启用的场景。

自定义 Logger

Shoulder 允许您自定义保存操作日志的存储方式,实现 OperationLogger 接口,并将实现类作为 @Bean 注入至 Spring 上下文中即可。

自定义操作日志存储
@Component
public class MyOperationLogger implements OperationLogger {
    //...自行处理日志,如上报到日志中心、发送至 MQ..
}

异步记录 & 缓冲记录

为提高记录性能,保障业务服务的吞吐,Shoulder 为您提供了“异步记录”与“缓冲记录”操作日志的能力,可在不影响主线程的情况下处理记录流程,若开启了“缓冲记录”,还会合并多条日志统一记录以减少 IO 操作。

开启方式:

Properties
# 是否以异步线程记录操作日志,默认开
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
YAML
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 根据常用的日志加工组件及其扩展,默认提供了两种日志格式,并允许您自定义日志格式。

逗号分割格式【默认】 KeyValueContextBuilder

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

自定义:日志格式校验

实现 OperationLogValidator 接口,并将实现类作为 @Bean 注入至 Spring 上下文中即可。

自定义操作日志格式-校验
@Component
public class ShoulderOperationLogValidator implements OperationLogValidator {

    @Override
    public void validate(OperationLogDTO log) {
        // 您的校验逻辑
    }
}