佛山品牌网站设计,如何在百度里建网站,国内优秀的网站,小程序源码网免费下载#x1f3af;导读#xff1a;mzt-biz-log 是一个用于记录操作日志的通用组件#xff0c;旨在追踪系统中“谁”在“何时”对“何事”执行了“何种操作”。该组件通过简单的注解配置#xff0c;如 LogRecord#xff0c;即可实现接口调用的日志记录#xff0c;支持成功与失败… 导读mzt-biz-log 是一个用于记录操作日志的通用组件旨在追踪系统中“谁”在“何时”对“何事”执行了“何种操作”。该组件通过简单的注解配置如 LogRecord即可实现接口调用的日志记录支持成功与失败场景下的差异化日志描述。它还提供了丰富的功能包括但不限于租户隔离、日志子类型划分、条件性日志记录以及枚举值解析等。此外mzt-biz-log 支持自定义日志存储逻辑允许开发者根据业务需求将日志持久化到数据库或其他存储媒介。整体设计简洁高效适用于微服务架构中的日志管理需求。 文章目录 mzt-biz-log介绍具体实现依赖添加注解枚举类型转化为具体值枚举类实现解析器类使用 日志子类型划分日志过滤日志持久化数据库继承存储接口 mzt-biz-log介绍
mzt-biz-log一套通用操作日志组件用来记录「谁」在「什么时间」对「什么」做了「什么事」
github仓库https://github.com/mouzt/mzt-biz-log
具体实现
依赖
dependencygroupIdio.github.mouzt/groupIdartifactIdbizlog-sdk/artifactIdversion3.0.6/version
/dependency添加注解
首先需要在具体的服务启动类中添加注解EnableLogRecord(tenant venue)其中tenant是租户标识我这里设置为了服务的名称一般一个服务或者一个业务下的多个服务都用一个 tenant 就可以了 然后在具体的接口添加注解LogRecord在调用相应的接口之后就会触发日志
Repeatable(LogRecords.class)
Target({ElementType.METHOD, ElementType.TYPE})
Retention(RetentionPolicy.RUNTIME)
Inherited
Documented
public interface LogRecord {String success();String fail() default ;String operator() default ;String type();String subType() default ;String bizNo();String extra() default ;String condition() default ;String successCondition() default ;
}type日志类型可以用来区分不同的接口我这里直接设置为接口名称方便辨识subType日志子类型可以用来区分不同的操作者身份bizNo日志ID可以设置为具体的数据的ID这样查询日志的时候直接使用相应数据的ID来查询例如说bizNo存储的是订单ID后面可以凭借这个来查询该订单相关的日志success接口调用成功之后action存放什么数据action字段是什么看日志持久化就知道了一般通过描述语言拼接字段值来实现快速让用户知道日志的内容fail接口调用异常之后action存放什么数据extra需要记录的额外信息如直接将用户提交的数据的 json 进行存储因为action存储的是简略的信息operator存储操作人信息需要用户的系统已经实现了用户上下文
【成功调用示例】
/*** 增添数据*/
PostMapping(/save)
LogRecord(bizNo {{#id}},type 新增分区,success 场馆ID{{#partitionDO.venueId}} \分区名称{{#partitionDO.name}} \分区类型{{#partitionDO.type}} \描述{{#partitionDO.description}} \场区拥有的场数量{{#partitionDO.num}} \场区状态{{#partitionDO.status}}; \结果:{{#_ret}},fail 接口调用失败失败原因{{#_errorMsg}},extra {{#partitionDO.toString()}},operator {{T(com.vrs.common.context.UserContext).getUsername()}}
)
public Result save(Validated({AddGroup.class}) RequestBody PartitionDO partitionDO) {partitionService.save(partitionDO);// 因为 ID 是存储到数据库中才生成的LogRecord 默认拿不到需要我们将信息手动设置到上下文中LogRecordContext.putVariable(id, partitionDO.getId());return Results.success();
}注意
获取接口返回的结果{{#_ret}}通过日志上下文记录信息因为 id 是存储到数据库中才生成的LogRecord 一开始拿不到需要我们将信息手动设置到上下文中。可以通过LogRecordContext.putVariable(id, partitionDO.getId());来设置键值对然后在注解中凭借键来获取值就可以如bizNo {{#id}} 接口调用成功的日志内容如下
【logRecord】logLogRecord(idnull, tenantvenue, type新增分区, subType, bizNo1868205198032568320, operatoradmin, action场馆ID12345 分区名称篮球场A区 分区类型营业中 描述提供标准篮球设施包括篮球和球架。 场区拥有的场数量4 场区状态篮球; 结果:Result(code0, messagenull, datanull, requestIdnull)
, failfalse, createTimeSun Dec 15 16:03:23 CST 2024, extraPartitionDO(venueId1865271207637635072, name篮球场A区, type1, description提供标准篮球设施包括篮球和球架。, num4, status1), codeVariable{MethodNamesave, ClassNameclass com.vrs.controller.PartitionController})【失败调用示例】
首先在接口中模拟一个除以 0 异常即System.out.println(1/0);。然后在注解中添加fail 接口调用失败失败原因{{#_errorMsg}}其中#_errorMsg获取的是异常的信息
PostMapping(/save)
LogRecord(bizNo {{#id}},type 新增分区,success 场馆ID{{#partitionDO.venueId}} \分区名称{{#partitionDO.name}} \分区类型{{#partitionDO.type}} \描述{{#partitionDO.description}} \场区拥有的场数量{{#partitionDO.num}} \场区状态{{#partitionDO.status}}; \结果:{{#_ret}},fail 接口调用失败失败原因{{#_errorMsg}},extra {{#partitionDO.toString()}},operator {{T(com.vrs.common.context.UserContext).getUsername()}}
)
public Result save(Validated({AddGroup.class}) RequestBody PartitionDO partitionDO) {partitionService.save(partitionDO);// 因为 ID 是存储到数据库中才生成的LogRecord 默认拿不到需要我们将信息手动设置到上下文中LogRecordContext.putVariable(id, partitionDO.getId());System.out.println(1/0);return Results.success();
}调用失败之后的日志如下
【logRecord】logLogRecord(idnull, tenantvenue, type新增分区, subType, bizNo1868210725902950400, operatoradmin, action接口调用失败失败原因/ by zero, failtrue, createTimeSun Dec 15 16:25:21 CST 2024, extraPartitionDO(venueId1865271207637635072, name篮球场A区, type1, description提供标准篮球设施包括篮球和球架。, num4, status1), codeVariable{MethodNamesave, ClassNameclass com.vrs.controller.PartitionController})枚举类型转化为具体值
上面日志输出中分区类型和场区状态的是具体的数字值 如果说想要将类型对应为具体的值应该如何实现呢
枚举类
【场馆类型枚举】
package com.vrs.enums;import lombok.Getter;
import lombok.RequiredArgsConstructor;/*** 场馆类型枚举*/
RequiredArgsConstructor
public enum PartitionStatusEnum {BASKET_BALL(1, 篮球),FOOT_BALL(2, 足球),BADMINTON(3, 羽毛球),VOLLEYBALL(4, 排球),TABLE_TENNIS(5, 乒乓球),TENNIS(6, 网球),SWIMMING(7, 游泳),GYMNASTICS(8, 体操),FITNESS_CENTER(9, 健身房),HANDBALL(10, 手球),ICE_SKATING(11, 滑冰),SKATEBOARDING(12, 滑板),CLIMBING(13, 攀岩),CYCLING_INDOOR(14, 室内自行车),YOGA(15, 瑜伽);Getterprivate final int type;Getterprivate final String value;/*** 根据 type 找到对应的 value** param type 要查找的类型代码* return 对应的描述值如果没有找到抛异常*/public static String findValueByType(int type) {for (PartitionStatusEnum target : PartitionStatusEnum.values()) {if (target.getType() type) {return target.getValue();}}throw new IllegalArgumentException();}
}【场区状态枚举】
package com.vrs.enums;import lombok.Getter;
import lombok.RequiredArgsConstructor;/*** 场区状态枚举*/
RequiredArgsConstructor
public enum VenueTypeEnum {CLOSED(0, 已关闭),OPEN(1, 营业中),MAINTAIN(2, 维护中);Getterprivate final int type;Getterprivate final String value;/*** 根据 type 找到对应的 value** param type 要查找的类型代码* return 对应的描述值如果没有找到抛异常*/public static String findValueByType(int type) {for (VenueTypeEnum target : VenueTypeEnum.values()) {if (target.getType() type) {return target.getValue();}}throw new IllegalArgumentException();}
}实现解析器类
转换类需要继承IParseFunction接口然后实现两个方法
functionName返回解析器的标识后面需要在注解中使用来辨识不同的解析器apply主要用来实现解析工作如将枚举类型转化为具体的值
package com.vrs.biglog;import com.mzt.logapi.service.IParseFunction;
import com.vrs.enums.PartitionStatusEnum;
import org.springframework.stereotype.Component;/*** Author dam* create 2024/12/15 16:43*/
Component
public class PartitionStatusEnumParse implements IParseFunction {Overridepublic String functionName() {return PartitionStatusEnumParse;}Overridepublic String apply(Object value) {return PartitionStatusEnum.findValueByType(Integer.parseInt(value.toString()));}
}
package com.vrs.biglog;import com.mzt.logapi.service.IParseFunction;
import com.vrs.enums.VenueTypeEnum;
import org.springframework.stereotype.Component;/*** Author dam* create 2024/12/15 16:43*/
Component
public class VenueTypeEnumParse implements IParseFunction {Overridepublic String functionName() {return VenueTypeEnumParse;}Overridepublic String apply(Object value) {return VenueTypeEnum.findValueByType(Integer.parseInt(value.toString()));}
}使用
PostMapping(/save)
LogRecord(bizNo {{#id}},type 新增分区,success 场馆ID{{#partitionDO.venueId}} \分区名称{{#partitionDO.name}} \分区类型{VenueTypeEnumParse{#partitionDO.type}} \描述{{#partitionDO.description}} \场区拥有的场数量{{#partitionDO.num}} \场区状态{PartitionStatusEnumParse{#partitionDO.type}};\结果:{{#_ret}},fail 接口调用失败失败原因{{#_errorMsg}},extra {{#partitionDO.toString()}},operator {{T(com.vrs.common.context.UserContext).getUsername()}}
)
public Result save(Validated({AddGroup.class}) RequestBody PartitionDO partitionDO) {partitionService.save(partitionDO);// 因为 ID 是存储到数据库中才生成的LogRecord 默认拿不到需要我们将信息手动设置到上下文中LogRecordContext.putVariable(id, partitionDO.getId());return Results.success();
}注意分区类型{VenueTypeEnumParse{#partitionDO.type}}中使用了解析器的标识
重新运行之后发现枚举类型已经转化了具体值 日志子类型划分
日志子类型划分为了区分日志的所属身份比如说普通用户修改了数据管理员也修改了数据。但通常指允许管理员查看用户的操作日志不允许普通用户查看管理员的操作日志。因此可以使用subType字段来做一些区分后面实现日志查询的时候针对用户的身份对该字段做一些处理即可
LogRecord(bizNo {{#id}},type 新增分区,subType {{T(com.vrs.common.context.UserContext).getUserType()}},success 场馆ID{{#partitionDO.venueId}} \分区名称{{#partitionDO.name}} \分区类型{VenueTypeEnumParse{#partitionDO.type}} \描述{{#partitionDO.description}} \场区拥有的场数量{{#partitionDO.num}} \场区状态{PartitionStatusEnumParse{#partitionDO.type}}; \结果:{{#_ret}},fail 接口调用失败失败原因{{#_errorMsg}},extra {{#partitionDO.toString()}},operator {{T(com.vrs.common.context.UserContext).getUsername()}}
)日志过滤
只有在满足一定条件的时候才记录日志可以使用condition字段比如说用户提交的数量为null才记录日志
LogRecord(bizNo {{#id}},type 新增分区,subType {{T(com.vrs.common.context.UserContext).getUserType()}},success 场馆ID{{#partitionDO.venueId}} \分区名称{{#partitionDO.name}} \分区类型{VenueTypeEnumParse{#partitionDO.type}} \描述{{#partitionDO.description}} \场区拥有的场数量{{#partitionDO.num}} \场区状态{PartitionStatusEnumParse{#partitionDO.type}}; \结果:{{#_ret}},fail 接口调用失败失败原因{{#_errorMsg}},extra {{#partitionDO.toString()}},operator {{T(com.vrs.common.context.UserContext).getUsername()}},condition {{#partitionDO.num null}}
)日志持久化
数据库
DROP TABLE IF EXISTS mt_biz_log;
CREATE TABLE mt_biz_log (id bigint NOT NULL COMMENT ID,create_time datetime,update_time datetime,is_deleted tinyint default 0 COMMENT 逻辑删除 0没删除 1已删除,tenant varchar(50) DEFAULT NULL COMMENT 租户,type varchar(50) DEFAULT NULL COMMENT 类型,sub_type varchar(50) DEFAULT NULL COMMENT 子类型,class_name varchar(100) DEFAULT NULL COMMENT 方法名称,method_name varchar(100) DEFAULT NULL COMMENT 方法名称,operator varchar(50) DEFAULT NULL COMMENT 操作人员,action longtext COMMENT 操作,extra longtext COMMENT 其他补充,status tinyint DEFAULT NULL COMMENT 操作状态 (0正常 1异常),PRIMARY KEY (id) USING BTREE
) COMMENT操作日志表;【实体类】
package com.vrs.entity;import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.vrs.domain.base.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;import java.io.Serializable;/*** 操作日志表* TableName mt_biz_log*/
TableName(value mt_biz_log)
Data
NoArgsConstructor
AllArgsConstructor
Builder
public class MtBizLog extends BaseEntity implements Serializable {/*** 租户*/private String tenant;/*** 类型*/private String type;/*** 子类型*/private String subType;/*** 方法名称*/private String className;/*** 方法名称*/private String methodName;/*** 操作人员*/private String operator;/*** 操作*/private String action;/*** 其他补充*/private String extra;/*** 操作状态 (0正常 1异常)*/private Integer status;TableField(exist false)private static final long serialVersionUID 1L;
}增删改查方法我这里就不再介绍了请大家自行实现
继承存储接口
只需要实现ILogRecordService接口然后重写record方法然后在该方法里面调用持久化方法即可
我这里统一将所有日志记录到一个表中如果想要根据业务分表存储可以根据logRecord.getTenant()和logRecord.getType()来判断存储到哪个表即可
package com.vrs.service.impl;import com.mzt.logapi.beans.CodeVariableType;
import com.mzt.logapi.beans.LogRecord;
import com.mzt.logapi.service.ILogRecordService;
import com.vrs.entity.MtBizLog;
import com.vrs.service.MtBizLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;import java.util.List;/*** Author dam* create 2024/12/15 21:02*/
Slf4j
Service
RequiredArgsConstructor
public class BizlogStoreService implements ILogRecordService {private final MtBizLogService mtBizLogService;Overridepublic void record(LogRecord logRecord) {mtBizLogService.save(MtBizLog.builder().tenant(logRecord.getTenant()).type(logRecord.getType()).subType(logRecord.getSubType()).className(logRecord.getCodeVariable().get(CodeVariableType.ClassName).toString()).methodName(logRecord.getCodeVariable().get(CodeVariableType.MethodName).toString()).operator(logRecord.getOperator()).action(logRecord.getAction()).extra(logRecord.getExtra()).status(logRecord.isFail() ? 1 : 0).build());}Overridepublic ListLogRecord queryLog(String bizNo, String type) {return null;}Overridepublic ListLogRecord queryLogByBizNo(String bizNo, String type, String subType) {return null;}
}