如何优雅的使用 log4j 在多线程环境下记录详细的日志到数据库

如何优雅的使用 log4j 在多线程环境下记录详细的日志到数据库

在应用程序开发中,日志记录是一个关键的部分,尤其是在多线程环境下,能够记录每个任务的日志变得尤为重要。本文将介绍如何使用 Spring Boot 和 Log4j,将日志记录到数据库中,并且在多线程环境下使用 ThreadContext 来区分不同任务的日志。

创建日志表

需要在数据库中创建一个日志表 task_log

CREATE TABLE `task_log` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `task_id` int DEFAULT NULL,
  `logger` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  `level` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL,
  `message` text COLLATE utf8mb4_bin,
  `full_class_name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  `method_name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  `stack_trace` text COLLATE utf8mb4_bin,
  `timestamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) 

自定义 Log4j Appender

创建一个自定义的 Log4j Appender 类 TaskAppender

@Plugin(name = "TaskAppender", category = "Core", elementType = "appender", printObject = true)
public class TaskAppender extends AbstractAppender {

    private final String url;
    private final String username;
    private final String password;
    private Connection connection;

    protected TaskAppender(String name, Layout<? extends Serializable> layout, String url, String username, String password) {
        super(name, null, layout, true);
        this.url = url;
        this.username = username;
        this.password = password;
        initializeConnection();
    }

    private void initializeConnection() {
        try {
            connection = DriverManager.getConnection(url, username, password);
        } catch (SQLException e) {
            throw new AppenderLoggingException("Failed to connect to database", e);
        }
    }

    @Override
    public void append(LogEvent event) {

        String taskId = ThreadContext.get("taskId");


        if (taskId != null) {
            String sql = "INSERT INTO task_log (timestamp, level, logger, message, exception, task_id) VALUES (?, ?, ?, ?, ?, ?)";
            try (PreparedStatement statement = connection.prepareStatement(sql)) {
                statement.setTimestamp(1, new java.sql.Timestamp(event.getTimeMillis()));
                statement.setString(2, event.getLevel().name());
                statement.setString(3, event.getLoggerName());
                statement.setString(4, event.getMessage().getFormattedMessage());
                statement.setString(5, event.getThrown() != null ? event.getThrown().toString() : null);
                statement.setString(6, taskId);
                statement.executeUpdate();
            } catch (SQLException e) {
                throw new AppenderLoggingException("Failed to write log to database", e);
            }
        }
    }

    @PluginFactory
    public static TaskAppender createAppender(
            @PluginAttribute("name") String name,
            @PluginElement("Layout") Layout<? extends Serializable> layout,
            @PluginAttribute("url") String url,
            @PluginAttribute("username") String username,
            @PluginAttribute("password") String password) {
        if (name == null) {
            LOGGER.error("No name provided for TaskAppender");
            return null;
        }
        if (layout == null) {
            layout = PatternLayout.createDefaultLayout();
        }
        return new TaskAppender(name, layout, url, username, password);
    }
}
  1. 自定义 Appender: TaskAppender 是一个自定义的 Log4j2 Appender,将日志记录写入数据库。
  2. 数据库连接: 在构造函数中建立数据库连接,并在 append 方法中使用这个连接。
  3. 日志写入: 仅在 ThreadContext 中存在 taskId 时,将日志写入数据库表 task_log
  4. 插件创建: createAppender 方法用于从配置文件创建 TaskAppender 实例,允许在 Log4j2 配置中指定属性。

配置 Log4j

src/main/resources 目录下创建 log4j2.xml 配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="ConsoleAppender" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
        </Console>

        <TaskAppender name="DatabaseAppender"
                      url="jdbc:mysql://192.168.10.90:3306/task_log"
                      username="root"
                      password="701701">
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
        </TaskAppender>
    </Appenders>

    <Loggers>
        <Root level="info">
            <AppenderRef ref="ConsoleAppender"/>
            <AppenderRef ref="DatabaseAppender"/>
        </Root>
    </Loggers>
</Configuration>

src/main/resources 创建log4j2.component.properties配置文件:

# 设置为 true 的作用是启用 InheritableThreadLocal 机制,使线程上下文信息能够从父线程继承到子线程。
 log4j2.isThreadContextMapInheritable=true

请添加图片描述

参考文档:Log4j – Configuring Log4j 2 (apache.org)

多线程任务执行

  @GetMapping("/task/execute")
    public String executeTask(@RequestParam String taskId) {
        taskService.executeTask(taskId);
        return "Task with ID " + taskId + " is being executed.";
    }
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
import org.springframework.stereotype.Service;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Service
public class TaskService {

    private static final Logger logger = LogManager.getLogger(TaskService.class);
    private static final ExecutorService executor = Executors.newFixedThreadPool(3);

    public void executeTask(String taskId) {
        ThreadContext.put("taskId", taskId); // 设置线程上下文 taskId
        logger.info("任务ID: {} 开始执行", taskId);

        // 提交读取任务
        executor.submit(() -> {
            try {
                logger.info("任务ID: {} 读取开始", taskId);
                // 模拟读取任务
                Thread.sleep(1000);
                logger.info("任务ID: {} 读取完成", taskId);
            } catch (InterruptedException e) {
                logger.error("任务ID: {} 读取中断", taskId, e);
            } finally {
                ThreadContext.clearAll(); // 清除 taskId
            }
        });

        // 提交转换任务
        executor.submit(() -> {
            logger.info("任务ID: {} 转换开始", taskId);
            try {
                // 模拟转换任务,故意引发异常
                throw new RuntimeException("模拟异常");
            } catch (RuntimeException e) {
                logger.error("任务ID: {} 转换中断,发生异常", taskId, e);
            }
        });

        // 提交写入任务
        executor.submit(() -> {
            try {
                logger.info("任务ID: {} 写入开始", taskId);
                // 模拟写入任务
                Thread.sleep(1000);
                logger.info("任务ID: {} 写入完成", taskId);
            } catch (InterruptedException e) {
                logger.error("任务ID: {} 写入中断", taskId, e);
            } finally {
                ThreadContext.clearAll(); // 清除 taskId
            }
        });


        logger.info("任务ID: {} 所有任务已提交", taskId);
        try{
            int i = 10/0;
        }catch (Exception e){
            logger.error(e.getMessage(), e);
        }
    }

}

这段代码的主要作用:

  • 使用 ThreadContext 设置和清除每个任务的上下文信息
  • 日志记录包含任务的开始、结束和异常信息,确保每个任务的日志都能反映其独立的执行情况。

效果展示

发送请求http://localhost:8080/task/execute?taskId=1执行任务

查询taskId=1的日志 http://localhost:8080/taskLog/list?taskId=1
请添加图片描述

查询taskId=1 并且日志级别为error的日志 http://localhost:8080/taskLog/list?taskId=1&level=ERROR
请添加图片描述

拓展–多租户系统日志隔离

在多租户系统中,利用Log4j的ThreadContext功能可以带来很多便利和增强,特别是在日志记录和分析方面。每个请求都会与一个特定的租户关联。通过将租户ID存储在ThreadContext中,所有与该请求相关的日志条目都可以包含该租户的信息。这有助于在日志中明确区分和追踪不同租户的活动。

为不同租户生成单独的日志文件,可以提高日志管理的效率。在Log4j中,可以通过配置动态文件名,使日志记录自动根据ThreadContext中的租户ID写入不同的文件;能够有效地提高日志记录的清晰度和可追溯性,有助于运维人员和开发人员更好地管理和分析系统的运行状态。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值