vlambda博客
学习文章列表

Log4j2的正确使用姿势


首先,谈谈我对日志打印的感受:平时注意不到你,一出问题总有你!相信不少同学都遇到过因为日志输出不规范、线上日志错误配置、日志组件性能瓶颈等问题,造成的灾难级的事故。
Log4j2是Apache提供的一个功能强大的日志组件,提供方便的日志记录,本篇文章会从该组件的一些基本概念、基本用法、等方面和大家一块学习,弄清楚“日志到底该怎么配?各个配置项到底是啥含义?” (有多少同学的日志配置是直接copy的已有项目,秉着能打出日志就行的态度在使用🙂)

Log4j2官方网站

https://logging.apache.org/log4j/2.x/

Log4j2配置的一些基本概念
<?xml version="1.0" encoding="UTF-8"?> <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>

Log4j2简易配置:log4j2.xml

节点名称 含义
配置项
Configuration 配置的根节点,有两个子节点:Appenders和Loggers(表明可以定义多个Appender和Logger)。

该节点有两个配置项

status:用于指定log4j2本身的日志打印级别,日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出。

monitorinterval:用于指定log4j自动重新配置的监测间隔时间,单位是秒(s),最小是5s。

Appenders 负责将日志输出到目的地(这个目的地可以是控制台,文件,数据库,甚至是邮件),定义输出内容,输出格式,输出方式,日志保存策略等。

log4j2支持的appender种类非常多,完整的appender可通过官方网站进行查询。其中,比较常见的appender有:ConsoleAppender、FileAppender、RandomAccessFileAppender、RollingFileAppender、RollingRandomAccessFileAppender,另外,还提供异步的AsyncAppender,实现异步处理日志事件。

Loggers

LogEvent生产的源头,

只有定义了logger并引入的appender,appender才会生效。

常见的有两种:Root和Logger。

appender分类

http://logging.apache.org/log4j/2.x/manual/appenders.html

常见的appender的配置

  • ConsoleAppender

使用该appender可以将日志信息输出到控制台,典型的配置如下:

<?xml version="1.0" encoding="UTF-8"?><Configuration status="warn" name="MyApp" packages=""> <Appenders> <Console name="STDOUT" target="SYSTEM_OUT"> <PatternLayout pattern="%m%n"/> </Console> </Appenders> <Loggers> <Root level="error"> <AppenderRef ref="STDOUT"/> </Root> </Loggers></Configuration>
  • filter:设置过滤器。

  • layout:设置输出样式。

  • name:Appender的名字

  • target:输出方法,SYSTEM_OUT或者SYSTEM_ERR。默认SYSTEM_OUT。

  • FileAppender

FileAppender用于将LogEvent写入到一个文件中,该文件由fileName参数指定。

<?xml version="1.0" encoding="UTF-8"?><Configuration status="warn" name="MyApp" packages=""> <Appenders> <File name="MyFile" fileName="logs/app.log"> <PatternLayout> <Pattern>%d %p %c{1.} [%t] %m%n</Pattern> </PatternLayout> </File> </Appenders> <Loggers> <Root level="error"> <AppenderRef ref="MyFile"/> </Root> </Loggers></Configuration>
  • fileName:指定写入的log文件的名称。
  • append:指定是否是追加写入(append=true,默认情况),还是覆盖写入(append=false)。
  • layout:输出文件的名称格式。
  • bufferedIO:是否对数据进行缓冲到缓冲区满后再写入。测试显示,即使在启用immediateFlush的情况下,设置bufferedIO=true也能提高性能。
  • bufferSize:当bufferedIO=true时,缓冲区的大小,默认是8192bytes。
  • locking:是否对文件上锁,当有多个线程可能同时写该文件时需要考虑上锁(在《异常处理反模式》中就提到要把在一起的日志输出语句写到一句,而不是拆成几句来避免并发线程导致的日志语句之间的错位)。但对文件上锁会影响系统的性能,所以需要谨慎使用。默认值是false。
  • immediateFlush:是否立即将数据刷入磁盘,能够最大程度保证数据的完整性,但是对性能会有一定的影响,默认值为true。
  • RandomAccessFileAppender
FileAppender很相似,除了将BufferdOutputStream替换为了ByteBuffer+RandomAccessFile,因为使用了缓存,它的性能比FileAppender高20%~200%。

RandomAccessFile介绍

https://www.cnblogs.com/jyy599/p/12076662.html
<?xml version="1.0" encoding="UTF-8"?><Configuration status="warn" name="MyApp" packages=""> <Appenders> <RandomAccessFile name="MyFile" fileName="logs/app.log"> <PatternLayout> <Pattern>%d %p %c{1.} [%t] %m%n</Pattern> </PatternLayout> </RandomAccessFile> </Appenders> <Loggers> <Root level="error"> <AppenderRef ref="MyFile"/> </Root> </Loggers></Configuration>
大部分配置与FileAppender相同,见上文,主要区别在于bufferSize。
  • bufferSize:缓存空间大小,默认为256M。


  • RollingFileAppender

RollingFileAppender是一种OutputStreamAppender。它除了将日志输出到文件外,还会根据TriggeringPolicy和RolloverPolicy生成新的日志文件。

一个RollingFileAppender需要两个策略的支持,分别是TriggeringPolicy和RolloverPolicy。TriggeringPolicy定义何时应该生成新的日志文件而RolloverPolicy则决定如何生成新的日志文件。RollingFileAppender不支持文件锁。

如下配置:有两个trigger,表示每小时生成一个文件或者当文件大小达到250MB时生成一个新的文件;有一个RolloverPolicy,表示最多保持文件数量为10,达到最大值后,旧的文件将被删除。

<?xml version="1.0" encoding="UTF-8"?><Configuration status="warn" name="MyApp" packages=""> <Appenders> <RollingFile name="RollingFile" filePattern="logs/app-%d{yyyy-MM-dd-HH}-%i.log.gz"> <PatternLayout> <Pattern>%d %p %c{1.} [%t] %m%n</Pattern> </PatternLayout> <Policies> <CronTriggeringPolicy schedule="0 0 * * * ?"/> <SizeBasedTriggeringPolicy size="250 MB"/> </Policies>       <DefaultRolloverStrategy max="10"/> </RollingFile> </Appenders> <Loggers> <Root level="error"> <AppenderRef ref="RollingFile"/> </Root> </Loggers></Configuration>

  • fileName:指定写入的log文件的名称。
  • filePattern: 压缩日志文件的文件名的模式。 该模式的格式取决于所使用的RolloverPolicy。 DefaultRolloverPolicy将接受兼容SimpleDateFormat的日期/时间模式和/或者%i(代表整数计数器)。
  • append:指定是否是追加写入(append=true,默认情况),还是覆盖写入(append=false)。
  • layout:输出文件的名称格式。
  • bufferedIO:是否对数据进行缓冲到缓冲区满后再写入。测试显示,即使在启用immediateFlush的情况下,设置bufferedIO=true也能提高性能。
  • bufferSize:当bufferedIO=true时,缓冲区的大小,默认是8192bytes。

  • immediateFlush:是否立即将数据刷入磁盘,能够最大程度保证数据的完整性,但是对性能会有一定的影响,默认值为true。

  • policy:用于决定是否发生rollover的策略,类型为TriggeringPolicy。常见的policy如下:

CronTriggeringPolicy:基于cron时间表达式,决定是否需要rollover;
SizeBasedTriggeringPolicy:一旦文件大小达到指定大小后,就会发送rollover。
TimeBasedTriggeringPolicy:根据日期格式中最具体的时间单位来决定应该多久发生一次rollover。例如,在日期模式中小时为具体的时间单位,那么每4小时会发生4次rollover,默认值为1。
  • strategy:用于决定压缩文件的名称和路径,类型为RolloverStrategy。

默认的rollover strategy接受一个日期/时间模式和一个整数,其中这个整数,是RollingFileAppender本身指定的filePattern属性。如果date/time模式存在的话,它将会替换当前日期和时间的值。如果这个模式包含整数的话,它将会在每次发生rollover时,进行递增。如果模式同时包含date/time和整数,那么在模式中,整数会递增直到结果中的data/time模式发生改变。如果文件模式是以".gz", ".zip", ".bz2", ".deflate", ".pack200", or ".xz"结尾的,将会与后缀相匹配的压缩方案进行压缩文件。

详细的RollingFileAppender介绍参考

http://www.voidcn.com/article/p-kztvqvrr-re.html
  • RollingRandomAccessFileAppender

RollingRandomAccessFileAppender类似于标准的 RollingFileAppender, 除了它总是被缓冲(这不能被关闭),并且在内部它使用ByteBuffer + RandomAccessFile 而不是BufferedOutputStream。与RollingFileAppender相比,当“bufferedIO = true”,性能提升了20-200%。

  • AsyncAppender
将一个LogEvent异步地写入多个不同输出地。AsynchAppender维护了一个队列,队列中存放了需要异步发送的LogEvent。
<?xml version="1.0" encoding="UTF-8"?><Configuration status="warn" name="MyApp" packages=""> <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> <Loggers> <Root level="error"> <AppenderRef ref="Async"/> </Root> </Loggers></Configuration>
  • AppenderRef指定需要异步执行的appender的名称,上文中介绍的appender皆可。

  • blocking:指定是否对AsynchAppender的LogEvent队列上锁,如果blocking=true,那么在队列满员的情况下,新到达的LogEvent将等待,直到有空位。若blocking=false,那么在队列满员的情况下,将把新到的LogEvent转到error appender。默认为true。

  • shutdownTimeout:当线程被关闭时,需要等待多时毫秒,保证队列中的logEvent事件被刷入到磁盘上。默认值为0,代表无限期等待。

  • errorRef:当blocking=false时,LogEvent会转入该appender;当blocking=true时,如果队列已满,并且QueueFullPolicy定义为DefaultAsyncQueueFullPolicy,则LogEvent会转入该appender。

  • BlockingQueueFactory:可自定义队列,默认提供如下队列。

  • bufferSize:队列中LogEvent的个数,默认是1024,如果使用的是disruptor队列的话,该值需要是2的幂次方。当LogEvent数量超过定义值后,会根据定义的FullPolicy策略决定日志如何处理。

如流程图所示,首先会判断用户是否设置了blocking选项,默认是true,如果设置为        false,则Appender直接会ToErrorAppender,如果用户没有配置或者配置为true,则会按照一定的策略来处理这些消息。策略可以分为2种:

DefaultAsyncQueueFullPolicy:等待队列,转为同步操作策略。

DiscardingAsyncQueueFullPolicy:按照日志等级抛弃日志策略。

如何让上文中的FullPolicy配置生效呢?可以通过配置log4j2的系统参数,配置文件名称为:log4j2.component.properties
//如果使用了Disruptor队列,强烈建议配置该项,避免由于日志过多导致内存泄露//56 * 1024 = 57344,单位为bytesasyncLoggerRingBufferSize=57344
//以下如果两者都配置了,则第二种方式将会覆盖第一种方式的配置log4j2.AsyncQueueFullPolicy=Discard log4j2.AsyncQueueFullPolicy=Discard;log4j2.DiscardThreshold=INFO

常见的logger的配置

  • Root

Root节点用来指定项目的根日志,如果没有单独指定Logger,那么就会默认使用该Root日志输出。
<loggers> <!--过滤掉spring和mybatis的一些无用的DEBUG信息--> <!--Logger节点用来单独指定日志的形式,name为包路径,比如要为org.springframework包下所有日志指定为INFO级别等。--> <logger name="org.springframework" level="INFO"></logger> <logger name="org.mybatis" level="INFO"></logger> <!-- Root节点用来指定项目的根日志,如果没有单独指定Logger,那么就会默认使用该Root日志输出 --> <root level="all"> <appender-ref ref="Console"/> <appender-ref ref="RollingFileInfo"/> <appender-ref ref="RollingFileWarn"/> <appender-ref ref="RollingFileError"/> </root> <!--AsyncLogger :异步日志,LOG4J有三种日志模式,全异步日志,混合模式,同步日志,性能从高到底,线程越多效率越高,也可以避免日志卡死线程情况发生--> <!--additivity="false" : additivity设置事件是否在root logger输出,为了避免重复输出,可以在Logger 标签下设置additivity为”false”--> <AsyncLogger name="AsyncLogger" level="trace" includeLocation="true" additivity="false"> <appender-ref ref="RollingFileError"/> </AsyncLogger> </loggers>
  • level:日志输出级别,共有8个级别,按照从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF。

  • AppenderRef:Root的子节点,用来指定该日志输出到哪个Appender。

  • includeLocation:如果你的layouts或custom过滤器需要location信息,你需要在所有相关日志记录器(包括根日志记录器)的配置中设置“includeLocation=true”,默认为false.

  • Logger

Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。

  • level:日志输出级别,共有8个级别,按照从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF。不指定默认为ERROR。

  • AppenderRef:Root的子节点,用来指定该日志输出到哪个Appender。

  • name:用来指定该Logger所适用的类或者类所在的包全路径,继承自Root节点。一般是项目包名或者框架的包名,比如:com.jourwon,org.springframework。

  • additivity:这个属性的意思是需不需要打印此logger继承的父logger,如果是false则只打印当前logger;如果是true则继续打印上一层的logger,直到root。

  • includeLocation:如果你的layouts或custom过滤器需要location信息,你需要在所有相关日志记录器(包括根日志记录器)的配置中设置“includeLocation=true”,默认为false。

AsyncLogger

异步日志,Log4j2有三种日志模式,全异步日志,混合模式,同步日志,性能从高到底,线程越多效率越高,也可以避免日志卡死线程情况发生。
  • includeLocation:如果你的layouts或custom过滤器需要location信息,你需要在所有相关日志记录器(包括根日志记录器)的配置中设置“includeLocation=true”,默认为false。



Log4j2使用示例

jar包引入
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.30</version> </dependency>
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.30</version> </dependency>
log4j2文件配置
<?xml version="1.0" encoding="UTF-8"?><!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL --><!--status="WARN" :用于设置log4j2自身内部日志的信息输出级别,默认是OFF--><!--monitorInterval="30" :间隔秒数,自动检测配置文件的变更和重新配置本身--><configuration status="WARN" monitorInterval="30"> <Properties> <!--自定义一些常量,之后使用${变量名}引用--> <Property name="logFilePath">log</Property> <Property name="logFileName">test.log</Property> </Properties> <!--appenders:定义输出内容,输出格式,输出方式,日志保存策略等,常用其下三种标签[console,File,RollingFile]--> <appenders> <!--console :控制台输出的配置--> <console name="Console" target="SYSTEM_OUT"> <!--PatternLayout :输出日志的格式,LOG4J2定义了输出代码,详见第二部分--> <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/> </console> <!--File :同步输出日志到本地文件--> <!--append="false" :根据其下日志策略,每次清空文件重新输入日志,可用于测试--> <File name="log" fileName="${logFilePath}/${logFileName}" append="false"> <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/> </File> <!--SMTP :邮件发送日志--> <SMTP name="Mail" subject="****SaaS系统正式版异常信息" to="[email protected]" from="[email protected]" smtpUsername="[email protected]" smtpPassword="LENG****1234" smtpHost="mail.lengjing.info" smtpDebug="false" smtpPort="25" bufferSize="10"> <PatternLayout pattern="[%-5p]:%d{YYYY-MM-dd HH:mm:ss} [%t] %c{1}:%L - %msg%n" /> </SMTP> <!-- ${sys:user.home} :项目路径 --> <RollingFile name="RollingFileInfo" fileName="${sys:user.home}/logs/info.log" filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log"> <!--ThresholdFilter :日志输出过滤--> <!--level="info" :日志级别,onMatch="ACCEPT" :级别在info之上则接受,onMismatch="DENY" :级别在info之下则拒绝--> <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/> <!-- Policies :日志滚动策略--> <Policies> <!-- TimeBasedTriggeringPolicy :时间滚动策略,默认0点小时产生新的文件,interval="6" : 自定义文件滚动时间间隔,每隔6小时产生新文件, modulate="true" : 产生文件是否以0点偏移时间,即6点,12点,18点,0点--> <TimeBasedTriggeringPolicy interval="6" modulate="true"/> <!-- SizeBasedTriggeringPolicy :文件大小滚动策略--> <SizeBasedTriggeringPolicy size="100 MB"/> </Policies> <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件,这里设置了20 --> <DefaultRolloverStrategy max="20"/> </RollingFile>
<RollingFile name="RollingFileWarn" fileName="${sys:user.home}/logs/warn.log" filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log"> <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/> <Policies> <TimeBasedTriggeringPolicy/> <SizeBasedTriggeringPolicy size="100 MB"/> </Policies> </RollingFile> <RollingFile name="RollingFileError" fileName="${sys:user.home}/logs/error.log" filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/error-%d{yyyy-MM-dd}-%i.log"> <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/> <Policies> <TimeBasedTriggeringPolicy/> <SizeBasedTriggeringPolicy size="100 MB"/> </Policies> </RollingFile> </appenders> <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效--> <loggers> <!--过滤掉spring和mybatis的一些无用的DEBUG信息--> <!--Logger节点用来单独指定日志的形式,name为包路径,比如要为org.springframework包下所有日志指定为INFO级别等。--> <logger name="org.springframework" level="INFO"></logger> <logger name="org.mybatis" level="INFO"></logger> <!-- Root节点用来指定项目的根日志,如果没有单独指定Logger,那么就会默认使用该Root日志输出 --> <root level="all"> <appender-ref ref="Console"/> <appender-ref ref="RollingFileInfo"/> <appender-ref ref="RollingFileWarn"/> <appender-ref ref="RollingFileError"/> </root> <!--AsyncLogger :异步日志,LOG4J有三种日志模式,全异步日志,混合模式,同步日志,性能从高到底,线程越多效率越高,也可以避免日志卡死线程情况发生--> <!--additivity="false" : additivity设置事件是否在root logger输出,为了避免重复输出,可以在Logger 标签下设置additivity为”false”--> <AsyncLogger name="AsyncLogger" level="trace" includeLocation="true" additivity="false"> <appender-ref ref="RollingFileError"/> </AsyncLogger> </loggers></configuration>
日志打印

以AsyncLogger为例

package com.example.log4jtest.demo;
import org.junit.Test;import org.junit.runner.RunWith;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;
/** * Created by jwnba24_er on 2021/2/13. */@RunWith(SpringRunner.class)@SpringBootTest(classes = DemoApplication.class)public class LogTest { private static final Logger logger = LoggerFactory.getLogger("AsyncLogger"); @Test public void testLog(){ logger.error("===this is test log info==="); }}

2021-02-13 21:26:01.491 ERROR 38202 --- [           main] AsyncLogger                              : ===this is test log info===


日志规约
1.【强制】应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
import org.slf4j.Logger;import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);
2.【强制】日志文件推荐至少保存 15 天,因为有些异常具备以“周”为频次发生的特点。
3.【强制】应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:appName_logType_logName.log。
logType:日志类型,推荐分类有 stats/monitor/visit 等;
logName:日志描述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。
说明:推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。
4.【强制】对 trace/debug/info 级别的日志输出,必须使用条件输出形式或者使用占位符的方式。
说明:logger.debug("Processing trade with id: " + id + " and symbol: " + symbol); 如果日志级别是 warn,上述日志不会打印,但是会执行字符串拼接操作,如果 symbol 是对象,会执行 toString()方法,浪费了系统资源,执行了上述操作,最终日志却没有打印。
if (logger.isDebugEnabled()) {logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);}
logger.debug("Processing trade with id: {} and symbol : {} ", id, symbol);
5.【强制】避免重复打印日志,浪费磁盘空间,务必在 log4j.xml 中设置 additivity=false。
<logger name="com.taobao.dubbo.config" additivity="false">
6.【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字 throws 往上抛出。
logger.error(各类参数或者对象 toString + "_" + e.getMessage(), e);
7.【推荐】谨慎地记录日志。生产环境禁止输出 debug 日志;有选择地输出 info 日志;如果使用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。
说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
8.【推荐】可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。如非必要,请不要在此场景打出 error 级别,避免频繁报警。
说明:注意日志输出的级别,error 级别只记录系统逻辑出错、异常或者重要的错误信息。