log4j2 日志保存至数据库

2023-09-16 15:02:40

概述

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-

更多推荐

【面试经典150 | 数组】多数元素

文章目录写在前面Tag题目来源题目解读解题思路方法一:哈希表方法二:排序方法三:摩尔投票法写在最后写在前面本专栏专注于分析与讲解【面试经典150】算法,两到三天更新一篇文章,欢迎催更……专栏内容以分析题目为主,并附带一些对于本题涉及到的数据结构等内容进行回顾与总结,文章结构大致如下,部分内容会有增删:Tag:介绍本题牵

淘宝问问:电商AI,重新定义购物体验

AI大模型进展的如火如荼,怎么少得了电商平台的参与,淘宝率先打响了第一枪。每一个软件都会有自己的Copilot,淘宝的就叫“淘宝问问”。用户可以在淘宝上使用“淘宝问问”来获取商品信息、价格、评价等,当前是内测版,虽有惊喜,但终究是刚刚发布内测,能力上还有待提升。淘宝问问通过语音、文字的方式进行交互,除基于通义千问的AI

Vue3 环境变量

文章目录前言一、环境变量简介二、自定义环境变量生产环境预览三、vite配置文件读取环境变量总结前言本文主要记录在项目中如何定义环境变量,达到不同环境中有不同的效果以及在vite配置文件中读取环境变量的方法。一、环境变量简介场景:各个环境下存在某些差异,比如请求地址不同,方便测试做的一些测试功能,这些在不同环境下都是不同

微信小程序如何在切换页面后原页面状态不变

在微信小程序中,如果要实现在切换页面后原页面状态不变,可以通过以下几种方式来实现:使用全局数据:可以将需要保持状态的数据存储在小程序的全局数据中,这样无论切换到哪个页面,都可以通过全局数据来获取之前保存的状态。//在app.js中定义全局数据App({globalData:{status:'default'}})在原页

如何在浏览器中导入Excel表格插件

如何在Vue框架中集成在线表格编辑器(designer)在Vue中集成在线表格编辑器:本节内容小编将为大家介绍Vue框架中如何集成在线表格编辑器和如何实现使用编辑器实现表格数据绑定。Vue集成在线表格编辑器和SpreadJS的方法相似,首先引入需要集成到Vue中的资源,其次使用styleInfo标签和designerI

pycharm安装jupyter,用德古拉主题,但是输入行全白了,看不清,怎么办?

问题描述今天换了以下pycharm主题,但是jupyter界面输入代码行太白了,白到看不清楚这行的字,更不知道写的是什么,写到哪了,这还是挺烦人的,其他都挺正常的。问题分析目前来看有两个原因:1、pycharm还没反应过来,重启下或许就好了(但是我重启好几次都没有解决)2、editor的问题,editor可能是本身就把

数据结构与算法:排序算法(1)

目录冒泡排序思想代码实现优化鸡尾酒排序优缺点适用场景快速排序介绍流程基准元素选择元素交换1.双边循环法使用流程代码实现2.单边循环法使用流程代码实现3.非递归实现排序在生活中无处不在,看似简单,背后却隐藏着多种多样的算法和思想;根据时间复杂度的不同,主流的排序算法可以分为三大类:1.时间复杂度为O(n^2)的排序算法冒

Prometheus+Consul 自助服务发现

Prometheus官网https://prometheus.io/download/Consul介绍Consul是基于GO语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册、服务发现和配置管理的功能。Consul提供服务注册/发现、健康检查、Key/Value存储、多数据中心和分布式一致性保证等功能。通过P

Qt/C++音视频开发55-加密保存到文件并解密播放

一、前言为了保证视频文件的安全性,有时候需要对保存的视频文件加密,然后播放的时候解密出来再播放,只有加密解密的秘钥一致时才能正常播放,用ffmpeg做视频文件的加密保存和解密播放比较简单,基于ffmpeg强大的字典参数设计,在avformat_write_header写入头部数据的时候,可以通过万能的av_dict_s

Redis的String常用命令

Redis基础知识不想key被更改,再key的后面加上nx.eg:127.0.0.1:6379>sets11OK127.0.0.1:6379>setss111OK127.0.0.1:6379>renamenxsss(integer)0--显示的结果为0,表示这个键在的时候,不可修改127.0.0.1:6379>判断命令

脑电相关临床试验及数据分析

临床试验设计作为一个医疗器械公司的开发–>算法–>项目–>产品,还是想在这里记录一下工作。直接开始吧临床试验的设计,主要分为20个部分,分别是封面一、申办者信息二、所有临床试验机构和研究者列表三、临床试验的目的和内容四、临床试验的背景资料五、产品特点、结构组成、工作原理与试验范围六、产品的适应症与禁忌症、注意事项七、总

热文推荐