一、选型对比
需求:记录收银员的所有操作步骤,输出操作关键节点日志 多家门店每日收银操作
主要考量点: 异步日志、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 -->
<!-- <!– 这里其实可以结合mvc拦截器 在登录时对所有用户请求拦截 然后设置log4j2的的MDC线程变量 从而过滤出用户操作–>-->
<!-- <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";
}
结果: