如何优雅的使用 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);
}
}
- 自定义 Appender:
TaskAppender
是一个自定义的 Log4j2Appender
,将日志记录写入数据库。 - 数据库连接: 在构造函数中建立数据库连接,并在
append
方法中使用这个连接。 - 日志写入: 仅在
ThreadContext
中存在taskId
时,将日志写入数据库表task_log
。 - 插件创建:
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写入不同的文件;能够有效地提高日志记录的清晰度和可追溯性,有助于运维人员和开发人员更好地管理和分析系统的运行状态。