当前位置: 首页 > news >正文

SpringBoot整合Log4j2进行日志记录异步写入日志文件

文章目录

  • 前言
  • 正文
    • 一、实现原理
    • 二、项目环境
    • 三、项目代码
      • 3.1 pom.xml
      • 3.2 LogIdFilter 过滤器
      • 3.3 TestController
      • 3.4 启动类
      • 3.5 log4j2.xml
    • 四、测试
      • 4.1 启动日志
      • 4.2 接口调用日志
  • 附录
    • 附1 线程封装调整
      • 1.1 MDCContextPreservingRunnable

前言

最近在看一些老项目,里边记录日志的方式有 Logback 和 Log4j2 这两种。

它们对于日志配置,异步策略方面都各有不同。

但是 Log4j2 可以和 “最快的单体队列” Disruptor 进行整合,从而达到异步情况下,性能的极大提升。当然,这种提升是建立在内存足够的情况下。

本文就 Log4j2 的使用进行整理记录。

正文

一、实现原理

  • DIsruptor 队列的原理说明
  • 线程池场景下,进行全链路logId传递时,需要使用 ThreadContext

二、项目环境

  • Java版本:Java 1.8
  • SpringBoot版本: 2.7.18
  • disruptor 版本:3.4.4
  • log4j starter版本:2.7.18

在这里插入图片描述

三、项目代码

3.1 pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.example</groupId><artifactId>springboot-log4j2-demo</artifactId><version>1.0-SNAPSHOT</version><packaging>jar</packaging><name>springboot-log4j2-demo</name><url>http://maven.apache.org</url><properties><java.version>1.8</java.version><java.compiler.source>${java.version}</java.compiler.source><java.compiler.target>${java.version}</java.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>2.7.18</spring-boot.version><lmax-disruptor.version>3.4.4</lmax-disruptor.version></properties><dependencies><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.34</version><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>${spring-boot.version}</version><!-- 排除默认的logback日志框架 --><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></exclusion></exclusions></dependency><!-- Log4j2 Starter --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-log4j2</artifactId><version>${spring-boot.version}</version></dependency><!-- Required for AsyncLoggers --><dependency><groupId>com.lmax</groupId><artifactId>disruptor</artifactId><version>${lmax-disruptor.version}</version></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><source>1.8</source><target>1.8</target><encoding>UTF-8</encoding></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>${spring-boot.version}</version><configuration><mainClass>org.pine.BootDemoApplication</mainClass><skip>true</skip></configuration><executions><execution><id>repackage</id><goals><goal>repackage</goal></goals></execution></executions></plugin></plugins></build>
</project>

3.2 LogIdFilter 过滤器

在这个过滤器中,对线程上下文设置和移除 logId 的值。演示使用UUID 生成,如果自己实际项目使用,则需要考虑从请求头中尝试获取,以及使用雪花算法计算一个唯一值。

package org.pine.filter;import org.apache.logging.log4j.ThreadContext;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import java.util.UUID;@Component
@Order(-1)
@WebFilter("/*")
public class LogIdFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {try {String logId = UUID.randomUUID().toString();// 线程上下文设置logIdThreadContext.put("logId", logId);chain.doFilter(request, response);} finally {//  清除线程上下文ThreadContext.clearAll();}}
}

3.3 TestController

这里定义一个rest接口,并使用线程池。打印日志,观察logId 的值。

package org.pine.controller;import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.ThreadContext;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;@RestController
@Slf4j
@RequestMapping("/test")
public class TestController {ExecutorService executorService = Executors.newFixedThreadPool(3);@RequestMapping("/hello")public String hello() {log.info("hello start");// 获取当前线程的ContextMapMap<String, String> contextMap = ThreadContext.getImmutableContext();for (int i = 0; i < 3; i++) { executorService.submit(() -> {// 在新线程中设置ContextMapThreadContext.putAll(contextMap);try {log.info("thread log ");} finally {log.info("thread log end");log.error("thread log error end");log.debug("thread log debug end");}});}log.info("hello end");return "hello";}
}

3.4 启动类

package org.pine;import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.ThreadContext;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@Slf4j
@SpringBootApplication
public class BootDemoApplication {public static void main(String[] args) {SpringApplication.run(BootDemoApplication.class, args);}
}

3.5 log4j2.xml

配置log4j2 对应的信息,包括日志格式,输出文件,异步输出等。

<?xml version="1.0" encoding="UTF-8"?><configuration status="INFO" shutdownHook="disable"><properties><!-- 设置属性logPath,指定日志文件的目录为 ./logs --><property name="log_path">./logs</property><!-- 设置日志格式 --><!-- 设置日志格式:时间(青色),线程(蓝色),日志ID(青色),日志级别(高亮默认),类名(黄色,限制长度最大36个字符),日志内容(高亮默认)--><!--%d{yyyy-MM-dd HH:mm:ss.SSS} 时间%t 线程%X{logId} 日志ID%-5level 日志级别%logger{60} 类名%msg 日志内容--><property name="console_log_pattern">%style{[%d{yyyy-MM-dd HH:mm:ss.SSS}]}{cyan} %style{[%t]}{blue} %style{[%X{logId}]}{cyan} %highlight{%-5level} %style{%logger{36}}{yellow} - %highlight{%msg} %n</property><property name="log_pattern">[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%t] [%X{logId}] %-5level %logger{36} - %msg%n</property><property name="log4j2.contextSelector">org.apache.logging.log4j.core.async.AsyncLoggerContextSelector</property><property name="log4j2.asyncQueueSize">262144</property><property name="log4j2.AsyncQueueFullPolicy">Block</property></properties><appenders><!-- 控制台输出 --><Console name="Console" target="SYSTEM_OUT"><PatternLayout pattern="${console_log_pattern}" /></Console><!-- 信息日志(滚动文件) --><RollingFile name="RollingFileInfo" fileName="${log_path}/info.log" filePattern="${log_path}/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log" immediateFlush="true" bufferSize="1024"><LevelRangeFilter minLevel="INFO" maxLevel="INFO" onMatch="ACCEPT" onMismatch="DENY"/><PatternLayout pattern="${log_pattern}" /><!--日志滚动策略:满足其中一种就会触发滚动TimeBasedTriggeringPolicy: 按时间滚动,默认为每天滚动,可以修改为按小时滚动,或者按天滚动SizeBasedTriggeringPolicy: 按文件大小滚动,默认为100MB滚动,可以修改为按M大小滚动,或者按G大小滚动--><Policies><TimeBasedTriggeringPolicy /><SizeBasedTriggeringPolicy size="512MB" /></Policies><DefaultRolloverStrategy max="20"><Delete basePath="${log_path}"><IfAccumulatedFileSize exceeds="5GB"/></Delete></DefaultRolloverStrategy></RollingFile><!-- 错误日志(滚动文件) --><RollingFile name="RollingFileError" fileName="${log_path}/error.log" filePattern="${log_path}/$${date:yyyy-MM}/error-%d{yyyy-MM-dd}-%i.log"><LevelRangeFilter minLevel="ERROR" maxLevel="ERROR" onMatch="ACCEPT" onMismatch="DENY"/><PatternLayout pattern="${log_pattern}"/><Policies><TimeBasedTriggeringPolicy /><SizeBasedTriggeringPolicy size="512MB" /></Policies></RollingFile><!-- 异步包装 --><Async name="AsyncRollingFileInfo"><AppenderRef ref="RollingFileInfo" /><AppenderRef ref="RollingFileError" /></Async></appenders><loggers><Root level="info"><AppenderRef ref="Console"/><AppenderRef ref="AsyncRollingFileInfo"/></Root><!-- 特定包使用异步 --><AsyncLogger name="org.pine" level="info" additivity="false"><AppenderRef ref="Console"/><AppenderRef ref="AsyncRollingFileInfo"/></AsyncLogger></loggers></configuration>

四、测试

4.1 启动日志

启动项目后,观察日志输出情况。
可以看到控制台:
在这里插入图片描述

日志文件中:
在这里插入图片描述

4.2 接口调用日志

访问地址:
GET http://localhost:8080/test/hello

观察日志如下:
在这里插入图片描述
可以看到,在方法内,线程内,单次请求的logId是相同的。

附录

附1 线程封装调整

封装线程,操作上下文的设置和清除。

1.1 MDCContextPreservingRunnable

package org.pine.logger;import org.apache.logging.log4j.ThreadContext;import java.util.Map;public class MDCContextPreservingRunnable implements Runnable {private final Runnable task;private final Map<String, String> context;public MDCContextPreservingRunnable(Runnable task) {this.task = task;this.context = ThreadContext.getImmutableContext();}@Overridepublic void run() {// 恢复MDC上下文try {ThreadContext.putAll(context);task.run();} finally {// 清理上下文(避免内存泄漏)ThreadContext.clearMap();}}
}

在使用时,可以使用封装的Runnable实现类:

@RequestMapping("/hello")public String hello() {log.info("hello start");for (int i = 0; i < 3; i++) {executorService.submit(new MDCContextPreservingRunnable(() -> {try {log.info("thread log {}", MDC.get("logId"));} finally {log.info("thread log end");log.error("thread log error end");log.debug("thread log debug end");}}));}log.info("hello end");return "hello";}

如此打印的日志也是一致的。


http://www.mrgr.cn/news/96096.html

相关文章:

  • 【windows搭建lvgl模拟环境(一)之VSCode】
  • 协作机械臂需要加安全墙吗? 安全墙 光栅 干涉区
  • docker中间件部署
  • TCP/IP的网络连接设备
  • 网络之数据链路层
  • 全文 - MLIR Toy Tutorial Chapter 1: Toy Language and AST
  • Linux网站搭建(新手必看)
  • XXL-Job 处理大数据量并发任务的解决方案及底层原理
  • SICAR 标准 KUKA 机器人标准功能块说明手册
  • 输出输入练习
  • MyBatis 语法不支持 having 节点
  • SQL语句---特殊查询
  • python中的面对对象
  • springboot在feign和线程池中使用TraceId日志链路追踪(最终版)-2
  • string 的接口
  • 【MySQL篇】DEPENDENT SUBQUERY(依赖性子查询)优化:从百秒到秒级响应的四种优化办法
  • mysql增、删、改和单表查询多表查询
  • C++ 异常
  • RAG(Retrieval-Augmented Generation)基建之PDF解析的“魔法”与“陷阱”
  • EF Core 执行原生SQL语句