当前位置: 首页 > 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 {
    @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";
    }

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

相关文章:

  • 《深度剖析Android 12 SystemUI锁屏通知布局亮屏流程:从源码到实现》
  • 0323-B树、B+树
  • Mybatis-plus配置动态多数据源
  • Linux系统之yum本地仓库创建
  • EMC知识学习一
  • 【android】补充
  • Jenkins 配置python项目和allure
  • 【HTML 基础教程】HTML 元素
  • 逼用户升级Win11,微软开始给Win10限速
  • 使用 langchain_deepseek 实现自然语言转数据库查询SQL
  • LXC 容器技术简介
  • rbpf虚拟机-验证器(verifier)
  • iOS:GCD信号量、同步、异步的使用方法
  • Browserlist 使用指南:应对浏览器兼容性问题的解决方案
  • golang-互斥锁-mutex-源码阅读笔记
  • Maven工具学习使用(四)——仓库
  • 双工通信:WebSocket服务
  • Flink 常用及优化参数
  • 【NLP 49、提示工程 prompt engineering】
  • 海外紧固件市场格局与发展趋势研究报
  • 银行网站 设计方案/校园推广方案
  • 网络营销营销型网站建设/百度引流平台
  • 网站 云建站/泰安短视频seo
  • 东营考试信息网/快速seo软件
  • 程序员做网站如何赚钱/永久免费跨境浏览app
  • 内网是怎么做网站的/小红书推广方式