log4j2 日志保存至数据库

文章目录

  • 概述
  • 一、springmvc工程
    • 1.创建数据库日志表
    • 2.log4j2.xml引入JDBCAppender
    • 3.定义日志管理类
    • 4.编写日志输出代码
    • 5.运行结果
    • 6.完整代码
  • 二、springboot工程
    • 1. 创建数据库日志表
    • 2.log4j2.xml引入JDBCAppender
    • 3.定义日志管理类
    • 4. 遗留问题
    • 5. 解决办法
    • 6. 完整代码

概述

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

官方Appenders提供了日志的多种输出方式实现。
log4j2 日志保存至数据库_第1张图片
下面我们以 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

	
<configuration status="off" monitorInterval="0">

	<properties>
		<property name="LOG_HOME">../logsproperty>
		<property name="PROJECT">springproperty>
		<property name="FORMAT">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%nproperty>
	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.运行结果

log4j2 日志保存至数据库_第2张图片
mysql 数据库日志数据如下log4j2 日志保存至数据库_第3张图片在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

		
		<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-

你可能感兴趣的:(系统架构,log4j,数据库)