基于log4j2搭建系统的日志记录框架

一、选型对比

需求:记录收银员的所有操作步骤,输出操作关键节点日志 多家门店每日收银操作

主要考量点: 异步日志、MDC上下文、jdbc写入支持、Filter

Log4j2 2.17.0 vs. Logback 比较表

特性/功能

Log4j2 2.17.0

Logback

开发者/支持

Apache Software Foundation

QOS.ch (Ceki Gülcü, Log4j 的最初开发者)

最新稳定版本

2.17.0

1.2.10(经典版),1.3.x(在发展中)

性能

高性能,特别是在异步日志记录上有很大的提升

性能优秀,但在异步日志记录方面略逊于 Log4j2

配置

支持 XML、JSON、YAML、Properties 文件

主要支持 XML 和 Groovy 配置

异步日志

提供多种异步日志实现(异步 Appender 和异步 Logger)

支持异步 Appender,但配置复杂度和灵活性不如 Log4j2

高级过滤

支持复杂的过滤器链,允许多级别和条件过滤

支持基础的过滤,但复杂度和灵活性不如 Log4j2

Log Layout

提供丰富的布局格式,支持自定义

提供标准布局,支持自定义

Lookups

支持基于变量的动态查找(环境变量、系统属性、MDC 等)

不支持类似 Log4j2 的 Lookups,但支持 MDC 和 NDC

插件架构

强大的插件机制,允许自定义 Appender、Layout、Filter 等

没有插件机制,但可以通过扩展类库实现自定义

Rolling Policy

支持基于时间、大小和复合的滚动策略

支持基于时间和大小的滚动策略,但配置灵活性略低

API 与 SPI

提供灵活且可扩展的 API 和 SPI,易于集成到自定义应用中

API 简单直接,适合大多数常见的日志需求

同步与异步模式

提供同步和异步的日志记录模式

提供同步模式,异步需要额外配置

GC 优化

低垃圾回收(GC)开销,特别是异步日志模式

较低的 GC 开销,但在高负载情况下可能表现不如 Log4j2

集成与兼容性

良好的兼容性,支持与老版本 Log4j1.x 集成

与 SLF4J 无缝集成,兼容性好,适用于许多框架

支持的输出目标

支持多种输出目标:文件、数据库、控制台、远程服务器等

支持常见的输出目标,但扩展性不如 Log4j2

性能监控和管理

内置性能监控和管理功能,支持 JMX

提供基础的 JMX 支持,监控功能不如 Log4j2 强大

内存消耗

一般来说,异步模式下的内存消耗更低

内存消耗合理,但高负载时可能略高于 Log4j2

线程上下文日志(MDC/NDC)

支持 MDC 和 NDC,可以传递上下文信息到日志记录

强力支持 MDC 和 NDC,适合需要上下文日志信息的应用

Logback 兼容性

通过 log4j-slf4j-impl 模块支持 Logback API 的兼容

本身即为 SLF4J 的实现,支持 SLF4J API 的直接使用

日志消息增强

支持消息格式化、参数化和国际化

提供基本的消息格式化和参数化支持

二、框架搭建

1.排除冲突,引入log4j2依赖

SLF4作为日志门面框架,logback和log4j2作为其具体的实现

springboot是默认选用兼容性更好的logback作为其实现的,因此需要排除相关的依赖

鉴于此,在引入日志框架依赖的时候要尽力避免,比如以下组合就不能同时出现:

•jcl-over-slf4j 和 slf4j-jcl

•log4j-over-slf4j 和 slf4j-log4j12

•jul-to-slf4j 和 slf4j-jdk14

<exclusions>
    <exclusion>
        <artifactId>logback-core</artifactId>
        <groupId>ch.qos.logback</groupId>
    </exclusion>
    <exclusion>
        <artifactId>logback-classic</artifactId>
        <groupId>ch.qos.logback</groupId>
    </exclusion>
    <exclusion>
        <artifactId>spring-boot-starter-logging</artifactId>
        <groupId>org.springframework.boot</groupId>
    </exclusion>
</exclusions>

确保slf4识别bind到log4j2作为其实现

引入log4j2相关依赖:

<dependencies>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.17.2</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <version>2.17.2</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>2.17.2</version>
    </dependency>
</dependencies>

2.配置文件

指定读取的配置文件

logging:
  config: classpath:log4j2.xml

log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration monitorInterval="5">

    <!-- 定义日志级别顺序 -->
    <!-- OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
    <!-- 定义变量 -->
    <Properties>
        <!-- 日志格式 -->
        <property name="LOG_PATTERN"
                  value="%clr{%d{yyyy-MM-dd HH:mm:ss.SSS}}{faint} %clr{%5p} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n%xwEx"/>
        <!-- 日志存储路径,不要使用相对路径 -->
        <property name="FILE_PATH" value="./logs"/>
        <property name="FILE_NAME" value="pos"/>
    </Properties>

    <!-- Appenders 配置 -->
    <Appenders>

        <!-- 控制台输出 -->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
        </Console>

        <!-- 文件输出,每次运行程序会自动清空 -->
        <File name="Filelog" fileName="${FILE_PATH}/test.log" append="false">
            <PatternLayout pattern="%d{yyyy.MM.dd 'at' HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n"/>
        </File>
        <!-- RollingFile 输出 INFO 级别及以下 -->
        <RollingFile name="RollingFileInfo"
                     fileName="${FILE_PATH}/info.log"
                     filePattern="${FILE_PATH}/${FILE_NAME}-INFO-%d{yyyy-MM-dd}_%i.log.gz">
            <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="20ms"/>
                <SizeBasedTriggeringPolicy size="10MB"/>
            </Policies>
            <DefaultRolloverStrategy max="15"/>
        </RollingFile>

        <!-- RollingFile 输出 WARN 级别及以下 -->
        <RollingFile name="RollingFileWarn"
                     fileName="${FILE_PATH}/warn.log"
                     filePattern="${FILE_PATH}/${FILE_NAME}-WARN-%d{yyyy-MM-dd}_%i.log.gz">
            <ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="20ms"/>
                <SizeBasedTriggeringPolicy size="10MB"/>
            </Policies>
            <DefaultRolloverStrategy max="15"/>
        </RollingFile>

        <!-- RollingFile 输出 ERROR 级别及以下 -->
        <RollingFile name="RollingFileError"
                     fileName="${FILE_PATH}/error.log"
                     filePattern="${FILE_PATH}/${FILE_NAME}-ERROR-%d{yyyy-MM-dd}_%i.log.gz">
            <ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="20ms"/>
                <SizeBasedTriggeringPolicy size="10MB"/>
            </Policies>
            <DefaultRolloverStrategy max="15"/>
        </RollingFile>


        <!-- JDBC Appender -->
        <JDBC name="databaseAppender" bufferSize="5" tableName="BVADM.BLOG_POS_LOG">
            <ConnectionFactory class="com.sundan.pos.conf.ConnectionFactory" method="getDatabaseConnection"/>
            <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_name" pattern="%M,%line"/>
            <Column name="message" pattern="%m"/>
            <Column name="exception" pattern="%ex{full}"/>
            <Column name="level1" pattern="%level"/>
            <Column name="time" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}"/>
            <Filters>
                <!-- 只接受 INFO 级别POS开头的日志 组合过滤入库-->
                <ThresholdFilter  level="INFO" onMatch="NEUTRAL" onMismatch="DENY"/>
               <RegexFilter regex=".*POS.*" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
        </JDBC>
        <!-- 添加自定义 Filter -->
        <!--        &lt;!&ndash;    这里其实可以结合mvc拦截器 在登录时对所有用户请求拦截 然后设置log4j2的的MDC线程变量 从而过滤出用户操作&ndash;&gt;-->
        <!--        <Filters>-->
        <!--            <ScriptFilter name="eventIdFilter" language="JavaScript">-->
        <!--                <Script>-->
        <!--                    <![CDATA[-->
        <!--                    // 获取 event_id 的值-->
        <!--                    var eventId = logEvent.getContextMap().get("id");-->
        <!--                    // 检查 event_id 是否为 1,并且日志级别为 INFO-->
        <!--                    if (eventId === "1" && logEvent.getLevel().equals(org.apache.logging.log4j.Level.INFO)) {-->
        <!--                        // 返回 ACCEPT 表示接受该日志事件-->
        <!--                        Filter.Result.ACCEPT;-->
        <!--                    } else {-->
        <!--                        // 返回 DENY 表示拒绝该日志事件-->
        <!--                        Filter.Result.DENY;-->
        <!--                    }-->
        <!--                ]]>-->
        <!--                </Script>-->
        <!--            </ScriptFilter>-->
        <!--        </Filters>-->

    </Appenders>

    <!-- Loggers 配置 -->
    <Loggers>
        <Logger name="org.mybatis" level="INFO" additivity="false">
            <AppenderRef ref="Console"/>
        </Logger>
        <Logger name="org.springframework" level="INFO" additivity="false">
            <AppenderRef ref="Console"/>
        </Logger>
        <!--用于记录特定包下的用户操作日志-->
        <Logger name="com.sundan.pos.*" level="INFO" additivity="false">
            <AppenderRef ref="databaseAppender"/>
        </Logger>


        <!-- Root Logger -->
        <Root level="INFO">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="Filelog"/>
            <AppenderRef ref="RollingFileInfo"/>
            <AppenderRef ref="RollingFileWarn"/>
            <AppenderRef ref="RollingFileError"/>
            <AppenderRef ref="databaseAppender"/>
        </Root>
    </Loggers>

</Configuration>

用于连接数据库的工具类

public class ConnectionFactory {
    private static final DataSource dataSource;
    private static final String DB_PROPERTIES_PATH = "/db.properties";

    static {
        Properties properties = new Properties();
        try (InputStream stream = ConnectionFactory.class.getResourceAsStream(DB_PROPERTIES_PATH)) {
            if (stream == null) {
                throw new ExceptionInInitializerError("Unable to find " + DB_PROPERTIES_PATH + " on classpath");
            }
            properties.load(stream);

            DruidDataSource druidDataSource = new DruidDataSource();
            druidDataSource.configFromPropety(properties);

            // 验证数据源配置是否正确,尝试获取连接
            try (Connection testConnection = druidDataSource.getConnection()) {
                if (testConnection != null) {
                    dataSource = druidDataSource;
                } else {
                    throw new ExceptionInInitializerError("DataSource configuration failed, cannot obtain a valid connection.");
                }
            }
        } catch (IOException | SQLException e) {
            throw new ExceptionInInitializerError("Failed to initialize DataSource: " + e.getMessage() + e);
        }
    }

    /**
     * 获取数据库连接。
     *
     * @return 数据库连接对象
     * @throws SQLException 连接数据库失败时抛出
     */
    public static Connection getDatabaseConnection() throws SQLException {
        return dataSource.getConnection();
    }
}

三、配置分析

1.控制台输出

PatternLayout用于设置控制台输出的格式,包括彩色日志,格式化日期之类的

<Console name="Console" target="SYSTEM_OUT">
                <PatternLayout pattern="${LOG_PATTERN}"/>
                <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
            </Console>

2.log文件滚动备份

Policies用来设置具体的备份策略

TimeBasedTriggeringPolicy代表你滚动log文件的时间间隔默认是1ms

SizeBasedTriggeringPolicy 滚动成压缩文件的大小

DefaultRolloverStrategy max="15" 最大备份数量为15个

filePattern=${FILE_PATH}/${FILE_NAME}-WARN-%d{yyyy-MM-dd}_%i.log.gz备份的日志名称

<RollingFile name="RollingFileWarn"
                         fileName="${FILE_PATH}/warn.log"
                         filePattern="${FILE_PATH}/${FILE_NAME}-WARN-%d{yyyy-MM-dd}_%i.log.gz">
                <ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY"/>
                <PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n"/>
                <Policies>
                    <TimeBasedTriggeringPolicy interval="1"/>
                    <SizeBasedTriggeringPolicy size="10MB"/>
                </Policies>
                <DefaultRolloverStrategy max="15"/>
            </RollingFile>

记录特定操作日志到jdbc

触发记录的缓冲条数 bufferSize="5"

表名 tableName="BVADM.BLOG_POS_LOG"

获取连接的类名方法名 ConnectionFactory class="com.sundan.pos.conf.ConnectionFactory"

method="getDatabaseConnection"

过滤的日志级别 ThresholdFilter

根据 日志信息过滤 RegexFilte

<!-- JDBC Appender -->
<JDBC name="databaseAppender" bufferSize="5" tableName="BVADM.BLOG_POS_LOG">
    <ConnectionFactory class="com.sundan.pos.conf.ConnectionFactory" method="getDatabaseConnection"/>
    <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_name" pattern="%M,%line"/>
    <Column name="message" pattern="%m"/>
    <Column name="exception" pattern="%ex{full}"/>
    <Column name="level1" pattern="%level"/>
    <Column name="time" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}"/>
    <Filters>
        <!-- 只接受 INFO 级别并且消息包含 "POS" 的日志 -->
        <ThresholdFilter level="INFO" onMatch="NEUTRAL" onMismatch="DENY"/>
        <RegexFilter regex=".*POS.*" onMatch="ACCEPT" onMismatch="DENY"/>
    </Filters>
</JDBC>

MDC线程上下文信息,使用map来记录key-value值,在配置文件中获取

可以在mvc拦截器中获取到当前用户、id并设置mdc值通过映射写入到数据库

db.properties 文件配置:

druid.url=jdbc:mysql://localhost:3306/bloglizi?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
druid.username=root
druid.password=123123
druid.driverClassName=com.mysql.cj.jdbc.Driver
druid.maxActive=10
druid.minIdle=5

四、TEST测试

请求:

@GetMapping("/test")
public String test() {
    System.out.println("====>");
    log.info("Received test request");
    log.trace("Received test request");
    log.warn("Received test request");
    log.error("Received test request");
    log.info("POS");
    log.trace("POS");
    log.warn("POS");
    log.error("POS");
    System.out.println("====>");
    return "Test successful";
}

结果:

  • 34
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值