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 {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
String logId = UUID.randomUUID().toString();
// 线程上下文设置logId
ThreadContext.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");
// 获取当前线程的ContextMap
Map<String, String> contextMap = ThreadContext.getImmutableContext();
for (int i = 0; i < 3; i++) {
executorService.submit(
() -> {
// 在新线程中设置ContextMap
ThreadContext.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();
}
@Override
public 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";
}
如此打印的日志也是一致的。