log4j2中配置logId唯一标识来标记同一个请求

Log4j2日志唯一标识实践
本文介绍如何在Log4j2中使用ThreadContext为每次请求生成唯一标识(logId),并将其贯穿整个请求过程,包括前端和服务端。通过在log4j2.xml配置文件中指定PatternLayout,可以在日志输出中包含该唯一标识。
在项目的开发过程中,对于log打印时,一次request到response的请求,以及到响应到客户端的数据的请求,我们希望都能够串起来,因此,需要有一个唯一id来标识,这里我们通过log4j2中的logId来实现。
Log4j2中包含一个类:ThreadContext(实际上是一个本地线程)
可以自己生成一个唯一id:比如:uuid
logId = uuid;
然后在过滤器或者拦截器之前设置:ThreadContext.put("logId", logId);

log4j2.xml中配置如下:

<Console name="console" target="SYSTEM_OUT" follow="true">
    <PatternLayout pattern="[LOGID:%X{logId}] %d{yyyy/MM/dd HH:mm:ss} [%p] %c{1} (%F:%L) %msg%n" />
    <ThresholdFilter level="trace" onMatch="ACCEPT" onMismatch="DENY" />
</Console>
即在pattern中添加{logId}来打印出logId的唯一标识。
PS:我们可以将logId通过response中设置header字段作为一个唯一标识,这样前端和服务端就通过logId完全贯穿起来。
package com.jxyunge.sign.mybatis.entity.sys; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.jxyunge.sign.annotation.SearchParam; import com.jxyunge.sign.enums.sys.SearchConditionEnum; import com.jxyunge.sign.mybatis.entity.BaseEntity; import lombok.Data; import java.io.Serializable; import java.util.Date; /** * 系统日志 * @author zanmall */ @Data @TableName("tb_sys_log") public class SysLog extends BaseEntity implements Serializable { private static final long serialVersionUID = 1L; /* *//** * 主键 *//* @SearchParam(name = {"主键"},condition = {SearchConditionEnum.eq}) @TableId(type = IdType.ASSIGN_UUID) private String id;*/ /** * 用户名 */ private String username; /** * 用户操作 */ private String operation; /** * 请求方法 */ private String method; /** * 请求参数 */ private String params; /** * 执行时长(毫秒) */ private Long time; /** * IP地址 */ private String ip; /** * 创建时间 */ private Date createDate; /** * 业务id */ private String bizId; } package com.jxyunge.sign.aspect; import cn.hutool.core.date.SystemClock; import com.fasterxml.jackson.databind.ObjectMapper; import com.jxyunge.sign.mybatis.entity.sys.Admin; import com.jxyunge.sign.mybatis.entity.sys.SysLog; import com.jxyunge.sign.mybatis.service.sys.AdminService; import com.jxyunge.sign.mybatis.service.sys.SysLogService; import com.jxyunge.sign.request.input.common.PageParam; import com.jxyunge.sign.tools.IpHelper; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import java.util.*; import java.util.stream.Collectors; /** * @author lgh */ @Slf4j @Aspect @AllArgsConstructor public class SysLogAspect { private final AdminService adminService; private final SysLogService sysLogService; private ObjectMapper objectMapper; @Around("@annotation(sysLog)") public Object around(ProceedingJoinPoint joinPoint, com.jxyunge.sign.annotation.SysLog sysLog) throws Throwable { long beginTime = SystemClock.now(); //执行方法 Object result = joinPoint.proceed(); //执行时长(毫秒) long time = SystemClock.now() - beginTime; SysLog sysLogEntity = new SysLog(); if(sysLog != null){ //注解上的描述 sysLogEntity.setOperation(sysLog.value()); } //请求的方法名 String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); sysLogEntity.setMethod(className + "." + methodName + "()"); sysLogEntity.setParams(null); //请求的参数 Object[] args = joinPoint.getArgs(); if(args != null && args.length > 0) { String params = null; try { if(args.length > 0 && args[0] != null && Objects.equals(args[0].getClass().getName(), PageParam.class.getName())){ PageParam<Object> pageParam = (PageParam<Object>) args[0]; List<Object> records = pageParam.getRecords(); // 过滤掉不可序列化的对象 List<Object> filteredRecords = filterSerializableObjects(records); params = objectMapper.writeValueAsString(filteredRecords); } else { // 过滤参数中的不可序列化对象 Object[] filteredArgs = filterSerializableObjects(args); params = objectMapper.writeValueAsString(filteredArgs); } } catch (Exception e) { // 如果序列化失败,记录参数类型信息或简单处理 params = getSafeParamRepresentation(args); log.warn("参数序列化失败,使用备用方案: {}", e.getMessage()); } sysLogEntity.setParams(params); } //设置IP地址 sysLogEntity.setIp(IpHelper.getIpAddr()); // 设置业务数据ID(String类型) String businessId = extractIdFromResult(result); if (businessId != null) { sysLogEntity.setBizId(businessId); } //用户名 Admin admin = adminService.getAdminFromReq(); if (!Objects.isNull(admin)) { sysLogEntity.setUsername(admin.getUsername()); } sysLogEntity.setTime(time); sysLogEntity.setCreateDate(new Date()); //保存系统日志 sysLogService.save(sysLogEntity); return result; } private String extractIdFromResult(Object result) { try { // 如果result为null,直接返回null if (result == null) { return null; } Class<?> clazz = result.getClass(); // 第一层:尝试从result的data字段/getData方法中获取实际数据 Object dataObject = null; try { // 尝试调用getData()方法 java.lang.reflect.Method getDataMethod = clazz.getMethod("getData"); dataObject = getDataMethod.invoke(result); } catch (Exception ignored) { // 如果没有getData方法,尝试直接访问data字段 try { java.lang.reflect.Field dataField = clazz.getDeclaredField("data"); dataField.setAccessible(true); dataObject = dataField.get(result); } catch (Exception ignored2) { // 如果既没有getData方法也没有data字段,直接使用result本身 dataObject = result; } } // 如果data为null,返回null if (dataObject == null) { return null; } // 第二层:从data对象中提取ID Class<?> dataClass = dataObject.getClass(); // 优先使用getter方法 try { java.lang.reflect.Method getIdMethod = dataClass.getMethod("getId"); Object idValue = getIdMethod.invoke(dataObject); if (idValue != null) { return idValue.toString(); } } catch (Exception ignored) { // 如果没有getId方法,尝试直接访问字段 } // 备用方案:直接访问字段 java.lang.reflect.Field[] fields = dataClass.getDeclaredFields(); for (java.lang.reflect.Field field : fields) { if (field.getName().equals("id") || field.getName().endsWith("Id")) { field.setAccessible(true); Object value = field.get(dataObject); if (value != null) { return value.toString(); } } } } catch (Exception e) { log.warn("从返回结果中提取ID失败: {}", e.getMessage()); } return null; } // 过滤可序列化对象的方法 private List<Object> filterSerializableObjects(List<Object> objects) { if (objects == null) return new ArrayList<>(); return objects.stream() .filter(this::isSerializable) .collect(Collectors.toList()); } private Object[] filterSerializableObjects(Object[] objects) { if (objects == null) return new Object[0]; return Arrays.stream(objects) .filter(this::isSerializable) .toArray(); } // 检查对象是否可序列化 private boolean isSerializable(Object obj) { if (obj == null) return true; try { // 尝试快速判断是否为常见不可序列化类型 Class<?> clazz = obj.getClass(); String className = clazz.getName(); // 过滤常见的不可序列化类 if (className.contains("java.util.Collections$") || className.contains("HttpServletRequest") || className.contains("HttpServletResponse") || className.contains("Session") || className.contains("ServletContext") || className.startsWith("org.apache.catalina") || className.startsWith("org.springframework.security")) { return false; } // 简单测试序列化 objectMapper.writeValueAsString(obj); return true; } catch (Exception e) { return false; } } // 获取安全的参数表示 private String getSafeParamRepresentation(Object[] args) { if (args == null || args.length == 0) { return "[]"; } List<String> paramInfos = new ArrayList<>(); for (int i = 0; i < args.length; i++) { if (args[i] != null) { String className = args[i].getClass().getSimpleName(); paramInfos.add("arg" + i + ": " + className); } else { paramInfos.add("arg" + i + ": null"); } } try { return objectMapper.writeValueAsString(paramInfos); } catch (Exception e) { return paramInfos.toString(); } } } package com.jxyunge.sign.annotation; import java.lang.annotation.*; /** * 自定义日志注解 * @author linyan */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SysLog { String value() default ""; } 我想要实现一个操作日志的功能,可查看到相关模块的新增,删除,编辑记录,点击详情可查看明细,看到相关的字段的改变,还能实现搜索功能,请你根据我提供给你的代码和需求,生成实现的代码,给出具体的步骤
07-31
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值