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
<think>好的,用户之前询问了关于ThreadContext中logId无法移除的问题,现在他们想了解如何在log4j2.xml中配置全链路的logID。我需要根据之前的分析,提供配置方法,确保logID在日志中全程可见,尤其是在异步和多线程环境下。 首先,用户可能是遇到了分布式系统中日志追踪困难的问题,特别是在微服务架构下,一个请求可能经过多个服务,每个服务又有多个线程处理。他们需要一种方法来确保每个请求的日志都有唯一的logID,方便追踪和排查问题。 接下来,我需要回顾之前提到的ThreadContext的使用,以及可能遇到的问题,比如线程复用和未正确清除上下文。现在用户需要的是配置log4j2.xml,使得logID能够自动记录到每条日志中,并且在整个调用链中保持一致。 首先,应该考虑如何定义logID。通常,可以在请求入口处生成唯一的logID,比如使用过滤器或拦截器,并将logID放入ThreadContext中。然后在日志输出格式中包含这个logID。但用户现在特别询问的是log4j2.xml的配置,所以重点应放在如何配置PatternLayout来包含logID,以及如何处理异步日志的问题。 在log4j2中,可以通过%X{logId}来引用ThreadContext中的logID。因此,在PatternLayout中添加这个变量是关键。但还需要考虑异步日志的情况,因为默认情况下,异步日志可能不会自动传递ThreadContext,导致logID丢失。需要配置AsyncLogger时指定ContextDataInjector,比如使用ReusableLogEventFactory,确保上下文数据被正确传递。 另外,用户可能需要跨线程传递logID,比如在使用线程池时,子线程需要继承父线程的logID。这时候可能需要配置isThreadContextMapInheritable参数,或者在代码中手动传递并设置logID。 还需要考虑Web应用的场景,比如使用Servlet过滤器在请求开始时生成logID,并在结束时清除。这部分虽然在之前的回答中提到了,但需要结合log4j2.xml的配置,确保过滤器正确设置logID,并且在日志输出中能够捕获到。 可能的配置步骤包括: 1. 在log4j2.xml的PatternLayout中添加%X{logId}。 2. 配置异步日志的ContextDataInjector,确保logID在异步环境下不丢失。 3. 使用过滤器或拦截器生成和清除logID。 4. 处理跨线程时的logID传递问题,可能需要使用InheritableThreadLocal或者手动传递。 同时,需要提醒用户注意线程池中的线程复用问题,确保每次任务执行前都清除旧的logID,并设置新的。此外,在微服务之间传递logID时,可能需要通过HTTP头或其他方式将logID传递到下游服务,并在每个服务的日志配置中同样包含logID。 最后,验证配置是否正确,可以通过生成logID并检查日志输出是否包含该ID,以及在不同线程和服务中是否保持一致。还要提供示例配置和代码片段,方便用户参考实现。</think>以下是关于在log4j2.xml中配置全链路logID的详细指南: # Log4j2全链路logID配置方案 ## 一、基础配置原理 $$ \text{日志链路追踪} = \text{ThreadContext存储} + \text{日志模板引用} + \text{上下文传递} $$ ## 二、核心配置步骤 ### 1. 日志模板配置 ```xml <Configuration> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %X{logId} %logger{36} - %msg%n"/> </Console> </Appenders> </Configuration> ``` *关键参数说明*: `%X{logId}` 会自动读取ThreadContext中名为"logId"的值 ### 2. 异步日志增强配置 ```xml <AsyncLogger name="com.yourpackage" level="info" includeLocation="true"> <AppenderRef ref="Console"/> <ContextDataInjector> org.apache.logging.log4j.core.impl.ReusableLogEventFactory </ContextDataInjector> </AsyncLogger> ``` ## 三、全链路传递实现 ### 1. HTTP请求入口配置(Servlet过滤器) ```java @WebFilter("/*") public class LogIdFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { String logId = UUID.randomUUID().toString(); ThreadContext.put("logId", logId); chain.doFilter(request, response); } finally { ThreadContext.clearAll(); } } } ``` ### 2. RPC调用上下文传递 ```java // 客户端发送前 rpcRequest.addHeader("X-Log-ID", ThreadContext.get("logId")); // 服务端接收时 String logId = rpcRequest.getHeader("X-Log-ID"); ThreadContext.put("logId", logId); ``` ## 四、多线程场景处理 ### 1. 线程池包装器 ```java public class LogAwareExecutor extends ThreadPoolExecutor { public void execute(Runnable task) { Map<String, String> context = ThreadContext.getImmutableContext(); super.execute(() -> { ThreadContext.clearAll(); ThreadContext.putAll(context); try { task.run(); } finally { ThreadContext.clearAll(); } }); } } ``` ### 2. CompletableFuture增强 ```java public static <T> CompletableFuture<T> withLogContext(Supplier<T> supplier) { Map<String, String> context = ThreadContext.getImmutableContext(); return CompletableFuture.supplyAsync(() -> { ThreadContext.putAll(context); try { return supplier.get(); } finally { ThreadContext.clearAll(); } }); } ``` ## 五、高级配置选项 ### 1. 强制继承模式 ```xml <Configuration> <Properties> <Property name="log4j2.isThreadContextMapInheritable">true</Property> </Properties> </Configuration> ``` ### 2. 日志脱敏配置 ```xml <PatternLayout pattern="%d{yyyy-MM-dd} %X{logId:--masked--} %msg%n"/> ``` ## 六、验证与调试 ### 1. 日志采样验证 ```java // 在关键节点添加验证日志 logger.info("logId验证标记: {}", ThreadContext.get("logId")); ``` ### 2. 诊断配置 ```bash # 启动时添加调试参数 -Dlog4j2.debug=true -Dlog4j2.disableThreadContext=true ``` ## 七、性能优化建议 1. **对象复用配置**: ```xml <Configuration reuseObjects="true"> <LogEventFactory class="org.apache.logging.log4j.core.impl.ReusableLogEventFactory"/> </Configuration> ``` 2. **上下文缓存策略**: ```java // 在应用启动时设置 System.setProperty( "log4j2.threadContextMap", "org.apache.logging.log4j.spi.CopyOnWriteThreadContextMap" ); ``` > **注意事项**: > 1. 当使用`%X{logId}`时如果值为空,日志会显示空白字符,建议添加默认值:`%X{logId:-N/A}` > 2. 异步日志与同步日志的上下文传递机制不同,需要特别注意`ReusableLogEventFactory`的配置 > 3. 跨JVM调用时需通过Header传递logId,推荐使用`X-Request-ID`作为标准字段
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值