log4j2 日志保存至数据库

概述

Apache Log4j 2是对Log4j的升级,它比其前身Log4j 1.x提供了重大改进,并提供了Logback中可用的许多改进,同时修复了Logback架构中的一些问题。是目前最优秀的Java日志框架,没有之一。

官方Appenders提供了日志的多种输出方式实现。
在这里插入图片描述
下面我们以 JDBCAppender 为例来说明如何在项目中实现系统日志保存到数据库。

一、springmvc工程

1.创建数据库日志表

CREATE TABLE IF NOT EXISTS boot_log ( 
  `id` bigint NOT NULL AUTO_INCREMENT,
  `event_id` varchar(50) ,
  `event_date` datetime ,
  `thread` varchar(255) ,
  `class` varchar(255) ,
  `function` varchar(255) ,
  `message` varchar(255) ,
  `exception` text,
  `level` varchar(255) ,
  `time` datetime,
   PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2.log4j2.xml引入JDBCAppender

	<?xml version="1.0" encoding="UTF-8"?>
<configuration status="off" monitorInterval="0">

	<properties>
		<property name="LOG_HOME">../logs</property>
		<property name="PROJECT">spring</property>
		<property name="FORMAT">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</property>
	</properties>

	<appenders>
		<console name="Console" target="system_out">
			<patternLayout pattern="${FORMAT}" />
		</console>

		<JDBC name="databaseAppender" bufferSize="20" tableName="boot_log">
			<ConnectionFactory class="com.fly.core.log.LogPoolManager" method="getConnection" />
			<Column name="event_id" pattern="%X{id}" />
			<Column name="event_date" isEventTimestamp="true" />
			<Column name="thread" pattern="%t %x" />
			<Column name="class" pattern="%C" />
		 	<Column name="`function`" pattern="%M" />
			<Column name="message" pattern="%m" />
			<Column name="exception" pattern="%ex{full}" />
			<Column name="level" pattern="%level" />
			<Column name="time" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}" />
		</JDBC>
	</appenders>

	<loggers>
		<logger name="org.springframework" level="INFO" />
		<root level="INFO">
			<appender-ref ref="Console" />
			<appender-ref ref="databaseAppender" />
		</root>
	</loggers>
</configuration>

3.定义日志管理类

LogPoolManager.java

/**
 * 
 * 日志数据库数据源
 * 
 * @author 00fly
 * @version [版本号, 2023年3月27日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
public final class LogPoolManager
{
    private LogPoolManager()
    {
        super();
    }
    
    /**
     * getConnection
     * 
     * @return
     * @throws SQLException
     * @see [类、类#方法、类#成员]
     */
    public static Connection getConnection()
        throws SQLException
    {
        // TODO: mvc工程下使用此写法,可行,boot工程不行
        DataSource dataSource = SpringContextUtils.getBean(DataSource.class);
        Assert.notNull(dataSource, "dataSource is null");
        return dataSource.getConnection();
    }
}

4.编写日志输出代码

@Slf4j
@Component
@Configuration
public class ScheduleJob
{
    @Value("${welcome.message:hello, 00fly in java!}")
    private String welcome;
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    @Scheduled(cron = "0/10 * 7-20 * * ?")
    public void run()
    {
        log.info("---- {}", welcome);
        long count = jdbcTemplate.queryForObject("select count(*) from boot_log", Long.class);
        log.info("------------ boot_log count: {} ----------", count);
        if (count > 100)
        {
            log.info("###### truncate table boot_log ######");
            jdbcTemplate.execute("truncate table boot_log");
        }
    }
    
    @Bean
    public ScheduledExecutorService scheduledExecutorService()
    {
        // return Executors.newScheduledThreadPool(5);
        return new ScheduledThreadPoolExecutor(5, new CustomizableThreadFactory("schedule-pool-"));
    }
}

5.运行结果

在这里插入图片描述
mysql 数据库日志数据如下在这里插入图片描述在log4j2.xml中设置了 bufferSize=“20”,这边日志容量达到20才执行一次批量保存。

6.完整代码

https://gitee.com/00fly/java-code-frame/tree/master/springmvc-dbutils

二、springboot工程

1. 创建数据库日志表

CREATE TABLE IF NOT EXISTS boot_log ( 
  `id`  bigint NOT NULL AUTO_INCREMENT ,
  `event_id` varchar(50) ,
  `event_date` datetime ,
  `thread` varchar(255) ,
  `class` varchar(255) ,
  `function` varchar(255) ,
  `message` varchar(255) ,
  `exception` text,
  `level` varchar(255) ,
  `time` datetime,
PRIMARY KEY (id)
);

2.log4j2.xml引入JDBCAppender

		<!-- bufferSize 没起作用,待排查 -->
		<JDBC name="databaseAppender" bufferSize="20" tableName="boot_log">
			<ConnectionFactory class="com.fly.core.log.LogPoolManager" method="getConnection" />
			<Column name="event_id" pattern="%X{id}" />
			<Column name="event_date" isEventTimestamp="true" />
			<Column name="thread" pattern="%t %x" />
			<Column name="class" pattern="%C" />
			<Column name="`function`" pattern="%M" />
			<Column name="message" pattern="%m" />
			<Column name="exception" pattern="%ex{full}" />
			<Column name="level" pattern="%level" />
			<Column name="time" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}" />
		</JDBC>

3.定义日志管理类


/**
 * 
 * 日志数据库数据源
 * 
 * @author 00fly
 * @version [版本号, 2023年3月27日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
public final class LogPoolManager
{
    private static DataSource dataSource;
    
    private LogPoolManager()
    {
        super();
    }
    
    /**
     * boot启动时指定的外部配置文件位置
     */
    private static String configLocation;
    
    public static void setConfigLocation(String configLocation)
    {
        LogPoolManager.configLocation = configLocation;
    }
    
    /**
     * 不能静态初始化 DataSource,否则无法加载外部配置文件
     */
    public static synchronized void init()
    {
        try
        {
            // 加载外部配置文件
            if (StringUtils.isNotBlank(configLocation))
            {
                File file = new File(configLocation);
                String text = FileUtils.readFileToString(file, StandardCharsets.UTF_8.toString());
                Properties props = YamlUtils.yamlToProperties(text);
                dataSource = DataSourceBuilder.create()
                    .type(DruidDataSource.class)
                    .url(props.getProperty("spring.datasource.url"))
                    .username(props.getProperty("spring.datasource.username"))
                    .password(props.getProperty("spring.datasource.password"))
                    .build();
            }
            else
            {
                // TODO: 数据源通过spring.profiles.active指定或docker-compose环境变量注入,怎么改写下面的逻辑?
                Resource resource = new ClassPathResource("application.yml");
                String text = IOUtils.toString(resource.getURL(), StandardCharsets.UTF_8.toString());
                boolean dev = StringUtils.contains(text, "dev");
                Properties properties = PropertiesLoaderUtils.loadProperties(new ClassPathResource(dev ? "jdbc-h2.properties" : "jdbc-mysql.properties"));
                dataSource = DruidDataSourceFactory.createDataSource(properties);
            }
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
    
    /**
     * getConnection
     * 
     * @return
     * @throws SQLException
     * @see [类、类#方法、类#成员]
     */
    public static Connection getConnection()
        throws SQLException
    {
        if (dataSource == null)
        {
            init();
        }
        Assert.notNull(dataSource, "dataSource can not be null");
        return dataSource.getConnection();
    }
}

4. 遗留问题

工程中log4j2组件的初始化一般早于springboot工程,这里采用log4j2.xml引入JDBCAppender,故LogPoolManager无法获取springboot管理的DataSource, 大家网上搜到的demo大部分采用写死数据库连接参数的形式,不利于维护。

上面采用的读取数据库配置文件的方式,在以下场景会导致无法读取正确的数据库配置,日志无法保存的问题:

  1. 数据源通过命令行 spring.profiles.active指定环境注入
    如:
       java -jar -Dspring.profiles.active=dev springboot-hello.jar --spring.config.location=./application-other.yml
       java -jar springboot-hello.jar --spring.profiles.active=dev --spring.config.location=./application-other.yml
  1. 数据源通过docker-compose编排文件环境变量注入
    如:
services:
  hello:
    image: registry.cn-shanghai.aliyuncs.com/00fly/springboot-hello-swagger2:1.0.0
    container_name: hello-random
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 200M
        reservations:
          memory: 180M
    ports:
    - 8080:8082
    entrypoint: 'sh wait-for.sh 172.88.88.11:3306 -- java -jar /app.jar'
    environment:
      JAVA_OPTS: -server -Xms200m -Xmx200m -Djava.security.egd=file:/dev/./urandom
      SPRING_DATASOURCE_URL: jdbc:mysql://172.88.88.11:3306/hello?useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
      SPRING_DATASOURCE_DRIVERCLASSNAME: com.mysql.cj.jdbc.Driver
      SPRING_DATASOURCE_USERNAME: root
      SPRING_DATASOURCE_PASSWORD: root123
    restart: on-failure
    logging:
      driver: json-file
      options:
        max-size: 5m
        max-file: '1'

5. 解决办法

将第2部的log4j2.xml引入JDBCAppender改写为使用javaConfig方式。

Log4j2Configuration.java


@Component
public class Log4j2Configuration implements ApplicationListener<ContextRefreshedEvent>
{
    private final DataSource dataSource;
    
    public Log4j2Configuration(DataSource dataSource)
    {
        this.dataSource = dataSource;
    }
    
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent)
    {
        final LoggerContext ctx = LoggerContext.getContext(false);
        final ColumnConfig[] cc =
            {ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("event_id").setPattern("%X{id}").setUnicode(false).build(),
                ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("event_date").setEventTimestamp(true).setUnicode(false).build(),
                ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("thread").setPattern("%t %x").setUnicode(false).build(),
                ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("class").setPattern("%C").setUnicode(false).build(),
                ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("`function`").setPattern("%M").setUnicode(false).build(),
                ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("message").setPattern("%m").setUnicode(false).build(),
                ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("exception").setPattern("%ex{full}").setUnicode(false).build(),
                ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("level").setPattern("%level").setUnicode(false).build(),
                ColumnConfig.newBuilder()
                    .setConfiguration(ctx.getConfiguration())
                    .setName("time")
                    .setPattern("%d{yyyy-MM-dd HH:mm:ss.SSS}")
                    .setUnicode(false)
                    .build()};
        
        // 配置appender
        final Appender appender = JdbcAppender.newBuilder()
            .setName("databaseAppender")
            .setIgnoreExceptions(false)
            .setConnectionSource(new ConnectionFactory(dataSource))
            .setTableName("boot_log")
            .setColumnConfigs(cc)
            .setColumnMappings(new ColumnMapping[0])
            .build();
        appender.start();
        
        ctx.getConfiguration().addAppender(appender);
        
        // 指定哪些logger输出的日志保存在mysql中
        ctx.getConfiguration().getLoggerConfig("com.fly.core.log.job").addAppender(appender, Level.INFO, null);
        ctx.updateLoggers();
    }
}

ConnectionFactory.java


public class ConnectionFactory extends AbstractConnectionSource
{
    private final DataSource dataSource;
    
    public ConnectionFactory(DataSource dataSource)
    {
        Assert.notNull(dataSource, "dataSource can not be null");
        this.dataSource = dataSource;
    }
    
    @Override
    public Connection getConnection()
        throws SQLException
    {
        return dataSource.getConnection();
    }
}

6. 完整代码

改造前代码:
https://gitee.com/00fly/effict-side/tree/master/springboot-hello

改造后javaConfig代码:
https://gitee.com/00fly/effict-side/tree/master/springboot-hello-swagger2

有任何问题和建议,都可以向我提问讨论,大家一起进步,谢谢!

-over-

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,针对您的问题,我可以给您提供以下思路: 使用log4j日志信息保存数据库,需要进行以下步骤: 1. 创建数据库表,用于存储日志信息。例如: ```sql CREATE TABLE `log` ( `id` int(11) NOT NULL AUTO_INCREMENT, `logger` varchar(255) DEFAULT NULL, `level` varchar(50) DEFAULT NULL, `message` text, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` 2. 创建Java项目,导入log4j和mysql的依赖。 3. 在log4j.properties文件中配置log4j的输出方式为JDBCAppender。例如: ```properties log4j.rootLogger=debug, db log4j.appender.db=org.apache.log4j.jdbc.JDBCAppender log4j.appender.db.URL=jdbc:mysql://localhost:3306/test log4j.appender.db.driver=com.mysql.jdbc.Driver log4j.appender.db.user=root log4j.appender.db.password=root log4j.appender.db.sql=INSERT INTO log(logger, level, message) VALUES('%c', '%p', '%m') ``` 其中,URL、driver、user、password是连接数据库的参数,sql是插入日志信息的SQL语句。 4. 在Java代码中使用log4j输出日志信息。例如: ```java import org.apache.log4j.Logger; public class TestLog { private static final Logger logger = Logger.getLogger(TestLog.class); public static void main(String[] args) { logger.debug("debug message"); logger.info("info message"); logger.warn("warn message"); logger.error("error message"); logger.fatal("fatal message"); } } ``` 运行程序后,log4j会自动将日志信息插入到数据库表中。 以上就是使用log4j日志信息保存数据库的一般思路,希望对您有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值