Java项目主流日志解决方案介绍【Log4j2+Slf4j+Lombok】
在大型系统中,日志是一个很重要的部分,线上问题的排查很大程度上依赖日志。记录日志的过程,大体上可以分成三个步骤:
在程序中对原始日志信息进行采集
对采集下来的日志信息进行格式化
将格式化好的日志信息写入目的地
Log4j2 的架构也自然是按照这个来的,Log4j2 中有三个重要的组件分别实现了这些功能,分别是 Logger
、Layout
和Appender
,日志数据流向图如下:
Log4j2的使用:
Log4j2 使用很简单,以 Maven 为例,只需要在 pom.xml
中添加:
<dependencies>
<!-- log4j2的使用-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.12.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.12.1</version>
</dependency>
</dependencies>
然后就可以在 Java 代码中直接使用:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.text.SimpleDateFormat;
import java.util.Timer;
import java.util.TimerTask;
public class Log4j2Test {
private static final Logger logger = LogManager.getLogger(Log4j2Test.class);
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
logger.error("Hello World --by Liu, Test01:测试日志1 ");
new Timer("timer").schedule(new TimerTask() {
public void run() {
logger.error("Hello World --by Liu, Test01:测试日志1 ,系统时间 "+ sdf.format(System.currentTimeMillis()) );
}
},1000,1000);
}
}
配置详解:
通过
ConfigurationFactory
使用编程的方式进行配置通过配置文件配置,支持
XML
、JSON
、YAML
和properties
等文件类型
Configuration
Confirguration 是配置文件根元素,每个配置文件有且仅有一个。如果不使用配置文件使用默认配置,以下配置与默认配置等价:
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="error">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
每一个配置都至少包含一个 Appender
和 Logger
。status
属性用于配置 Log4j2 内部的的日志级别。
Log4j2 可以自动检测配置文件的变化,monitorInterval
属性可以配置自动检测配置文件的时间,这个属性最小的值为 5,代表 5 秒检测一次。
在加入依赖完成后,我们开始Lo4j2进行配置,这里重点关注log4j2的配置文件的读取位置
说明:log4j2默认读取配置文件的位置如下
(1)非Maven项目,src目录下的log4j2.xml
(2)Maven项目,resouces目录下的log4j2.xml
本文中log4j2的配置文件内容具体如下:
<Configuration status="debug" name="defaultLogConfig"
packages="">
<properties>
<property name="LOG_HOME">D:\export\Logs\log.demo</property>
<property name="LOG_LEVEL">INFO</property>
<property name="patternlayout">
[%t] %d{yyyy-MM-dd HH:mm:ss SSS} %-5level %logger{1} %L %M - %msg%xEx%n
</property>
<!-- 入库模块WORKER日志 -->
<property name="jdx-cis-inbound">${LOG_HOME}/jdx-cis-inbound.log</property>
</properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true">
<PatternLayout pattern="${patternlayout}"/>
</Console>
<!--
name: 输出端的名字
fileName: 指定当前日志文件的位置和文件名称
filePattern: 指定当发生自动封存日志时,文件的转移和重命名规则
这个filePatten结合下面的TimeBasedTriggeringPolicy一起使用,可以实现控制日志按天生成文件.
自动封存日志的策略可以设置时间策略和文件大小策略(见下面的Policies配置)
时间策略:
文件名_%d{yyyy-MM-dd}_%i.log 这里%d表示自动封存日志的单位是天
如果下面的TimeBasedTriggeringPolicy的interval设为1,
表示每天自动封存日志一次;那么就是一天生成一个文件。
文件大小策略:
如果你设置了SizeBasedTriggeringPolicy的size的话,
超过了这个size就会再生成一个文件,这里的%i用来区分的
-->
<!-- 入库模块日志-->
<RollingFile name="InboundRollingFile" fileName="${jdx-cis-inbound}"
filePattern="${LOG_HOME}/jdx-cis-inbound-%d{yyyy-MM-dd}-%i.log">
<PatternLayout pattern="${patternlayout}"/>
<Policies>
<!-- 根据上图:filePattern的设置,每隔一天生成一个日志文件。-->
<TimeBasedTriggeringPolicy/>
<!--如果果今天的文件大小到了设定的size,则会新生成一个文件,上面的%i就表示今天的第几个文件-->
<SizeBasedTriggeringPolicy size="20 MB"/>
</Policies>
<!--
DefaultRolloverStrategy属性如不设置,
则默认为最多同一文件夹下7个文件,这里设置了50
-->
<DefaultRolloverStrategy max="50"/>
</RollingFile>
</Appenders>
<Loggers>
<!-- 这里引用上面定义的输出端,千万不要漏了。 -->
<!-- 入库模块日志,此处猪样,配置不同的代码包输出的对应的路径下 -->
<AsyncLogger name="com.yuewei.log4j.ws" level="${LOG_LEVEL}" additivity="true" >
<AppenderRef ref="InboundRollingFile"/>
</AsyncLogger>
<Root level="INFO">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
Logger
Logger 元素用于配置日志记录器。Logger 有两种形式,Logger
和 Root
,两个区别在于 Root 没有 name
属性并且不支持 additivity
属性。
假如我们要获取一个特定类的某个级别的日志。调高日志的级别无法完成这样的需求。一种可行的方式是在 Loggers 中添加一个 Logger,配置方式如下:
<Loggers>
<Logger name="cn.rayjun.Log4j2HelloWorld" level="trace">
<AppenderRef ref="Console"/>
</Logger>
<Root level="error">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
这样配置,除了 cn.rayjun.Log4j2HelloWorld
类会打印出 trace
级别的日志,其他就只会打印出 error
级别的日志,打印结果如下:
19:59:06.957 [main] TRACE cn.rayjun.Log4j2HelloWorld - Enter
19:59:06.957 [main] TRACE cn.rayjun.Log4j2HelloWorld - Enter
19:59:06.959 [main] ERROR cn.rayjun.Log4j2HelloWorld - Hello world
19:59:06.959 [main] ERROR cn.rayjun.Log4j2HelloWorld - Hello world
但是上面打印的日志有个问题,信息都被打印了两遍,为了解决这个问题,需要添加 additivity
参数并且置为 false,如下配置:
<Loggers>
<Logger name="cn.rayjun.Log4j2HelloWorld" level="trace" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Root level="error">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
默认情况下 Logger 都是同步的,但是也有异步的实现,Root 的异步版本是 AsyncRoot
,Logger 的异步版本是 AsyncLogger
,异步 Logger 依赖外部队列 LMAX Disruptor。
使用异步 Logger 需要加上外部依赖:
<!-- 使用异步日志增加如下的配置-->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.2</version>
</dependency>
使用异步 Logger:
<AsyncRoot level="error">
<AppenderRef ref="Console"/>
</AsyncRoot>
<AsyncLogger name="com.yuewei.log4j.ws" level="${LOG_LEVEL}" additivity="true" >
<AppenderRef ref="InboundRollingFile"/>
</AsyncLogger>
Logger 在具体使用中需要声明一个 Logger 对象,官方推荐将 Logger 声明为一个静态变量,可以提高日志记录的性能:
private static final Logger logger = LogManager.getLogger(Log4j2Test.class);
Appender 配置:
Appender
负责将日志分发到相应的目的地。也就是说 Appender
决定日志以何种方式展示,上面使用到的就是 ConsoleAppender
,这个 Appender 会将日志直接打印到控制台。同时还支持将日志输出到文件
、数据库
、消息队列
。
FileAppender
基本配置如下:
<File name="MyFile" fileName="logs/app.log">
<PatternLayout>
<Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
</PatternLayout>
</File>
如果想要把日志记录到数据库中,那就使用JDBCAppender
, 基本配置如下:
<JDBC name="databaseAppender" tableName="dbo.application_log">
<DataSource jndiName="java:/comp/env/jdbc/LoggingDataSource" />
<Column name="eventDate" isEventTimestamp="true" />
<Column name="level" pattern="%level" />
<Column name="logger" pattern="%logger" />
<Column name="message" pattern="%message" />
<Column name="exception" pattern="%ex{full}" />
</JDBC>
默认情况下 Appender 都是同步的,就是说日志产生的时候就会进行处理。但是有时候会从程序性能的角度进行考虑,生成的日志不会立即进行刷盘或者进行传输,而是在一个合适的时间集中进行处理,配置方式如下,异步 Appender 使用 ArrayBlockingQueue
作为队列,与异步 Logger 不同,异步 Appender 不需要外部依赖,但是官方推荐使用异步 Logger 而不是异步 Appender:
<Appenders>
<File name="MyFile" fileName="logs/app.log">
<PatternLayout>
<Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
</PatternLayout>
</File>
<Async name="Async">
<AppenderRef ref="MyFile"/>
</Async>
</Appenders>
把日志保存为文件是一个常用的操作,保存在文件中的日志以追加方式写入,但是单个文件不可能无限增大,也不可能手工来分割日志文件,所以需要通过自动的方式来分割日志。这就需要使用 RollingFileAppender
,通过设定日志分割的条件。分割的条件可以从两个方面进行设定,以时间频率或者日志文件的大小来触发日志的分割。
<RollingFile name="InboundRollingFile" fileName="${jdx-cis-inbound}"
filePattern="${LOG_HOME}/jdx-cis-inbound-%d{yyyy-MM-dd}-%i.log">
<PatternLayout pattern="${patternlayout}"/>
<Policies>
<!-- 根据上图:filePattern的设置,每隔一天生成一个日志文件。-->
<TimeBasedTriggeringPolicy/>
<!--如果果今天的文件大小到了设定的size,则会新生成一个文件,上面的%i就表示今天的第几个文件-->
<SizeBasedTriggeringPolicy size="20 MB"/>
</Policies>
<!--
DefaultRolloverStrategy属性如不设置,
则默认为最多同一文件夹下7个文件,这里设置了50
-->
<DefaultRolloverStrategy max="50"/>
</RollingFile>
TimeBasedTriggeringPolicy
会与 filePattern 的配置相匹配,如果 filePattern 是 {yyyy-MM-dd HH-mm}
,最小的时间粒度是分钟,那么就会每隔一分钟生成一个文件,如果改成 {yy-MM-dd HH}
,最小时间粒度是小时,那么就会每隔一个小时生成一个文件。SizeBasedTriggeringPolicy
表示设定日志的大小,上面的配置是指日志大小到 20M 后开始生成新的日志文件。
RollingRandomAccessFileAppender
与 RollingFileAppender
在功能上基本一致,但是底层的实现有所区别,RollingFileAppender 底层是 BufferedOutputStream
,RollingRandomAccessFileAppender 底层是使用 ByteBuffer
+ RandomAccessFile
,性能上有了很大的提升。
Layouts:
Appender 会使用 Layout
来对日志进行格式化。Lo4j1 和 Logback 中的 Layout 会把日志转成 String,而在 Log4j2 中使用的则是 byte 数组,这是从性能的角度进行的优化。
Layout 配置支持多种方式。用的最多的方式就是 PatternLayout
,就是通过正则表达式来格式化日志,应用的也最多,基本配置如下:
<PatternLayout>
<Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
</PatternLayout>
或者采用本文案例中的方式。
%d
表示时间,默认情况下表示打印完整时间戳2012-11-02 14:34:02,123
,可以调整%d
后面的参数来调整输出的时间格式%p
表示输出日志的等级,可以使用%highlight{%p}
来高亮显示日志级别%c
用来输出类名,默认输出的是完整的包名和类名,%c{1.}
输出包名的首字母和完整类名%t
表示线程名称%m
表示日志内容,%M
表示方法名称%n
表示换行符%L
表示打印日志的代码行数。
Logger 上如果加上了 includeLocation 后,日志性能会下降的很厉害,如果日志的位置信息不是必要的,就不需要加上
配合 Slf4j 使用:
Simple Logging Facade for Java(SLF4J)用作各种日志框架(例如 java.util.logging,logback,log4j)的简单外观或抽象,允许最终用户在部署时插入所需的日志框架。
slf4j 就是众多日志接口的集合,他不负责具体的日志实现,只在编译时负责寻找合适的日志系统进行绑定
Log4j2 和logbook是具体的日志框架的实现。
使用 Slf4j
非常简单,只需要在项目中加入以下依赖:
<!-- slfg4j的使用-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.12.1</version>
</dependency>
然后就可以在代码中使用了:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Slf4jDemo {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(Slf4jDemo.class);
logger.info("Slf4j log info");
}
}
配合lombok的使用:
在pom文件中引入lombok的依赖:
<!--lombok的使用-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>
代码示例如下:
import lombok.extern.slf4j.Slf4j;
import java.text.SimpleDateFormat;
import java.util.Timer;
import java.util.TimerTask;
@Slf4j
public class Test01 {
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//在类中引入注解@Slf4j后就可以使用,使用时固定的记录器是log
log.error("Hello World --by Liu, Test01:测试日志1 ");
new Timer("timer").schedule(new TimerTask() {
@Override
public void run() {
log.error("Hello World --by Liu, Test01:测试日志1 ,系统时间 "+ sdf.format(System.currentTimeMillis()) );
log.error("参数1:{},参数2:{}","lombok时间",sdf.format(System.currentTimeMillis()));
}
},1000,1000);
}
小结:
输出日志时,建议采用Sl4j作为日志的门面,方便后续切换日志框架。
参考资料:
https://juejin.cn/post/6844903990619013134
https://logging.apache.org/log4j/2.x/manual/index.html