一个在生产环境里运行的程序如果没有日志是很让维护者提心吊胆的,有太多杂乱又无意义的日志也是令人伤神。程序出现问题时候,从日志里如果发现不了问题可能的原因是很令人受挫的。本文想讨论的是如何在Java程序里写好日志。
一般来说日志分为两种:业务日志和异常日志,使用日志我们希望能达到以下目标:
1、对程序运行情况的记录和监控;
2、在必要时可详细了解程序内部的运行状态;
3、对系统性能的影响尽量小;
java里常见的日志库有java.util.logging(JDKlog)、Apache log4j、log4j2、logback、slf4j等等,这么多的日志框架里如何选择。
首先需要梳理的是日志接口,以及日志接口对应的实现。然后再考虑如何选择使用哪个日志框架。
下图说明了几个日志框架的关系:
上述关系图,大致就是两个 API 接口 + 4个实现框架。我们应用服务调用日志框架,只需遵循 API 接口规范,配置调用即可。
Log4j
Log4j : 是apache下一个功能非常丰富的java日志库实现,Log4j应该是出现比较早而且最受欢迎的java日志组件,它是基于java的开源的日志组件。Log4j的功能非常强大,通过Log4j可以把日志输出到控制台、文件、用户界面。也可以输出到操作系统的事件记录器和一些系统常驻进程。值得一提的是:Log4j可
以允许你非常便捷地自定义日志格式和日志等级,可以帮助开发人员全方位的掌控自己的日志信息是一个日志开源框架。
这个是最先出现流行的日志框架。
Java.util.logging
Log4j 流行之时,java自身也不忘在这方面发力,于是java 1.4 之后,java原生库也写了一套日志框架 即是 Java.util.logging。
此时,也有很多厂家的日志框架在共同竞争,各自为营,相互兼容性很差。导致应用软件API 在相互调用受阻。于是,apache 就站出来,设计了一套公共接口 – commons-logging ,来兼容各方面的日志框架,便于实际开发进程;
Commons-logging(jcl)
1、 在log4j , java.util.logging 等问世之后,相对于日志输出就简化很多,但是由于没有统一规范,开发者调用这些框架也是很麻烦,相互兼容性也很差(不同厂家的方法各不一样),同时对日志框架迭代更新维护带来不确定性,于是 apache 为规范日志输出,定义一套标准日志API接口 ---- commons-logging API 接口。开发者,只需关注接口,而不需看重实现细节,极大提供效率。
2、尽管 commons-logging(jcl) 也为众多日志实现库提供了统一的接口,作用和slf4j类似。它允许运行时绑定任意的日志库。但commons-loggins对log4j和java.util.logging的配置问题兼容性不太好,还会遇到类加载问题。所以当时log4j的作者CEKI又创作了 SLF4j API 接口。
SLF4j:
1、它是基于API的java日志框架,slf4j提供了简单统一的日志记录的接口,开发者在配置部署时只需要是吸纳这个接口就能是实现日志功能。它自身并没有提供具体的日志解决方案,它是负责服务于各种各样的日志系统,允许用户在部署应用上使用自己常用的日志框架。也就是说,SLF4j是一个抽象层,它提供了众多的适配器能是配合其他所有开源日志框架。
2、为了考虑其他项目会使用大量的第三方库,而第三方库使用的日志框架又各不相同,不同的日志框架又需要不同的配置,不同配置就会导致日志输出到不同的位置。所以我们就需要一个可以将日志level、日志输出等统一管理,而slf4j的适配器又对各种日志都实现了接管,接管后就可以统一配置这些第三方库中使用的日志。
logback
对比 Log4j的优势:
1、更快的实现:Logback的内核重写了,在一些关键执行路径上性能提升10倍以上。而且logback不仅性能提升了,初始化内存加载也更小了。
2、非常充分的测试:Logback经过了几年,数不清小时的测试。Logback的测试完全不同级别的。
3、Logback-classic非常自然实现了SLF4j:Logback-classic实现了SLF4j。在使用SLF4j中,你都感觉不到logback-classic。而且因为logback-classic非常自然地实现了slf4j , 所 以切换到log4j或者其他,非常容易,只需要提供成另一个jar包就OK,根本不需要去动那些通过SLF4JAPI实现的代码。
4、非常充分的文档 官方网站有两百多页的文档。
5、自动重新加载配置文件,当配置文件修改了,Logback-classic能自动重新加载配置文件。扫描过程快且安全,它并不需要另外创建一个扫描线程。这个技术充分保证了应用程序能跑得很欢在JEE环境里面。
6、Lilith是log事件的观察者,和log4j的chainsaw类似。而Lilith还能处理大数量的log数据 。
7、谨慎的模式和非常友好的恢复,在谨慎模式下,多个FileAppender实例跑在多个JVM下,能够安全地写道同一个日志文件。RollingFileAppender会有些限制。Logback的FileAppender和它的子类包括 RollingFileAppender能够非常友好地从I/O异常中恢复。
8、配置文件可以处理不同的情况,开发人员经常需要判断不同的Logback配置文件在不同的环境下(开发,测试,生产)。而这些配置文件仅仅只有一些很小的不同,可以通过,和来实现,这样一个配置文件就可以适应多个环境。
9、Filters(过滤器)有些时候,需要诊断一个问题,需要打出日志。在log4j,只有降低日志级别,不过这样会打出大量的日志,会影响应用性能。在Logback,你可以继续 保持那个日志级别而除掉某种特殊情况,如alice这个用户登录,她的日志将打在DEBUG级别而其他用户可以继续打在WARN级别。要实现这个功能只需加4行XML配置。可以参考MDCFIlter 。
10、SiftingAppender(一个非常多功能的Appender):它可以用来分割日志文件根据任何一个给定的运行参数。如,SiftingAppender能够区别日志事件跟进用户的Session,然后每个用户会有一个日志文件。
11、自动压缩已经打出来的log:RollingFileAppender在产生新文件的时候,会自动压缩已经打出来的日志文件。压缩是个异步过程,所以甚至对于大的日志文件,在压缩过程中应用不会受任何影响。
12、堆栈树带有包版本:Logback在打出堆栈树日志时,会带上包的数据。
13、自动去除旧的日志文件:通过设置TimeBasedRollingPolicy或者SizeAndTimeBasedFNATP的maxHistory属性,你可以控制已经产生日志文件的最大数量。如果设置maxHistory 12,那那些log文件超过12个月的都会被自动移除。
Log4j2
Log4j2 的特性:
1、API分离:Log4j的API与实现是分开的,从而使应用程序开发人员可以清楚地了解他们可以使用哪些类和方法,同时确保向前的兼容性。这允许Log4j团队以兼容的方式安全地改进实施。
2、性能提升:Log4j 2包含基于LMAX Disruptor库的下一代异步记录器。在多线程方案中,与Log4j 1.x和Logback相比,异步Logger的吞吐量高18倍,延迟降低了几个数量级。
3、支持多种API:Log4j 2 API将提供最佳性能,而Log4j 2提供对Log4j 1.2,SLF4J,Commons Logging和java.util.logging(JUL)API的支持。
4、避免锁定:编码为Log4j 2 API的应用程序始终可以选择使用任何SLF4J兼容库作为其Log4j-to-slf4j适配器的记录器实现。利用jdk1.5并发的特性,减少了死锁的发生;
5、自动重载配置:与Logback一样的是,Log4j 2可以在修改后自动重新加载其配置。与Logback不同的是,它在进行重新配置时不会丢失日志事件。丢数据这种情况少,可以用来做审计功能。而且自身内部报的exception会被发现,但是logback和log4j不会。
6、进阶筛选:与Logback一样,Log4j 2支持基于上下文数据,标记,正则表达式和Log事件中的其他组件进行过滤。可以指定过滤以将所有事件应用到所有事件,然后再传递给Logger或事件通过Appender。此外,过滤器还可以与Loggers关联。与Logback不同,您可以在任何这些情况下使用通用的Filter类。
7、插件架构:Log4j使用插件模式来配置组件。这样,您无需编写代码即可创建和配置Appender,Layout,Pattern Converter等。Log4j自动识别插件,并在配置引用它们时使用它们。
8、物业支持:您可以在配置中引用属性,Log4j将直接替换它们,或者Log4j将它们传递给将动态解析它们的基础组件。属性来自配置文件中定义的值,系统属性,环境变量,ThreadContext映射以及事件中存在的数据。用户可以通过添加自己的查找插件来进一步自定义属性提供程序。
9、Java 8 Lambda支持:以前,如果构建日志消息的成本很高,则通常会在构建消息之前显式检查是否启用了请求的日志级别。在Java 8上运行的客户端代码可以受益于Log4j的lambda支持。如果未启用请求的日志级别,由于Log4j不会评估lambda表达式,因此可以用更少的代码获得相同的效果。
10、自定义日志级别:在Log4j 2中,可以通过代码或配置轻松定义自定义日志级别。不需要子类。
11、无垃圾:在稳态日志记录期间,Log4j 2 在独立应用程序中是无垃圾的,而在Web应用程序中是低垃圾的。这样可以减少垃圾收集器上的压力,并可以提供更好的响应时间性能。【拥有号称能够减少 JVM 垃圾回收停顿时间的 Garbage-free(无垃圾模式)】
12、与应用服务器集成:版本2.10.0引入了模块log4j-appserver,以改善与Apache Tomcat和Eclipse Jetty的集成。
13、启用云:2.12.0版引入了对通过Lookup访问Docker容器信息以及通过Spring Cloud Configuration访问和更新Log4j配置的支持。
日志框架大战随着 SLF4j 的一统天下而落下帷幕,但 SLF4j 仅仅是接口,实现方面, logback 与 log4j2 仍然难分高下,接下来聊聊,日志框架实现到底是该选择 Log4j2 还是 Logback。这篇文章我们将从功能、API 设计、可扩展性、性能四个方面展开讨论。
配置文件方面,Log4j 提供了更多的配置文件配置方式,Log4j2 支持 properties、YAML、JSON、XML四种,Logback 则支持 XML 与 groovy 两种方式;
Appender 方面,两者均支持自定义扩展 Appender ,Log4j2 并且提供了几乎全部场景的 Appender,文件、控制台、JDBC、NoSql、MQ、Syslog、Socket、SMTP等,Logback提供 Appender 略少于 Log4j2,提供了文件、控制台、数据库、Syslog、Socket、SMTP等,动态化输出方面,Log4j2 提供了ScriptAppenderSelector,Logback 则提供了 Evaluator 与 SiftingAppender(两者均可以用于判断并选择对应的 Appender);
独有特性方面,Logback 支持 Receivers, 可以接收其他 Logback 的 Socket Appender 输出,Logbak 还拥有 logback-access 模块,可以用来与 Servlet容器(如 Tomcat 和 Jetty)集成,提供 http 访问日志功能;Log4j2 则拥有号称能够减少 JVM 垃圾回收停顿时间的 Garbage-free(无垃圾模式),Log4j2 API 支持使用 Java 8 lambda,SLF4j 则在 2.0 版本提供流式(Fluent)API 同时支持 lambda;
API 设计及可扩展性
如前文所说,SLF4j 则在 2.0 版本提供流式(Fluent)API ,届时Logback将会原生实现(理论上会比动态转译过去要好),而 Log4j2 并没有提供支持。扩展方面,Logback 采用配置文件中直接写对应实现(class=“ch.qos.logback.core.rolling.RollingFileAppender”)来自定义实现扩展,Log4j2 采用插件机制,无需配置,但比较复杂,个人认为 Logback 反而清晰一些。
性能
对于高并发,大量日志输出,Log4j2 确实有一定优势。因为其内部的队列使用的是disruptor。异步输出性能提升不少,是实话!
但是,对于日志框架应用,我们要求可能并不需要那么高,即对于一般的日志输出,Logback 与 Log4j2 性能方面差不多;
具体参考: https://www.jianshu.com/p/359b14067b9e,http://www.cainiaoxueyuan.com/bc/17731.html
总结
Logback 使用更简单、Log4j2 功能更强大,如果不是深度使用,两者并不会有太大差别,并且在使用SLF4j的时候可以无缝切换。建议,不必纠结选型,按照偏好选择即可。
第1步: 对于一般 Maven 项目,引入依赖
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-coreartifactId>
<version>1.2.3version>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
<version>1.2.3version>
dependency>
第2步:导入 logback.xml 即是logback日志输出配置文件,如下
<configuration debug="true">
<property name="LOG_HOME" value="D:/log/" />
<property name="APP_NAME" value="lolo"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{64}[%line] - %msg%nPattern>
layout>
appender>
<appender name="DATELOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${LOG_HOME}${APP_NAME}.logFile>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>infolevel>
<onMatch>ACCEPTonMatch>
<onMismatch>DENYonMismatch>
filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<FileNamePattern>${LOG_HOME}/${APP_NAME}.%i.bakFileNamePattern>
<MinIndex>1MinIndex>
<MaxIndex>30MaxIndex>
rollingPolicy>
<triggeringPolicy
class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>100MBMaxFileSize>
triggeringPolicy>
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{64}[%line] - %msg%nPattern>
layout>
appender>
<appender name="DATELOG_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${LOG_HOME}${APP_NAME}.error.logFile>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERRORlevel>
<onMatch>ACCEPTonMatch>
<onMismatch>DENYonMismatch>
filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy"><FileNamePattern>${LOG_HOME}/${APP_NAME}.error.%i.bakFileNamePattern>
<MinIndex>1MinIndex>
<MaxIndex>30MaxIndex>
rollingPolicy>
<triggeringPolicy
class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>100MBMaxFileSize>
triggeringPolicy>
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{64}[%line] - %msg%nPattern>
layout>
appender>
<logger name="com.jieshun.jscp.p" level="DEBUG" additivity="false" >
<appender-ref ref="CONSOLE" />
<appender-ref ref="DATELOG" />
<appender-ref ref="DATELOG_ERROR" />
logger>
<root level="INFO" >
<appender-ref ref="CONSOLE" />
<appender-ref ref="DATELOG" />
<appender-ref ref="DATELOG_ERROR" />
root>
configuration>
第3步:再代码调用
1) 可以使用 lomback 的@SLf4j 注解,即可直接使用默认的 log 对象;
@Slf4j
public class TestDemo {
@Test
public void testDateStr2TimeStamp() throws Exception{
String dateStr = "2020-07-13 11:16:00";
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = formatter.parse(dateStr);
log.info(" 2020-07-13 11:16:00 to times = {}",date.getTime());
// System.out.println(date.getTime());
}
}
注意调用 @Slf4j 注解时,需要引入 lomback 依赖!
<dependency> <groupId>org.projectlombokgroupId> <artifactId>lombokartifactId> <version>1.18.12version> <scope>providedscope> dependency>
2)不使用 @Slf4j 注解,那么在类的首行就要自定义一个日志输出对象
public class TestDemo {
// 使用 static 修饰,独立类对象之外,为所有类的对象共用
private static Logger logger = LoggerFactory.getLogger(TestDemo.class); // TestDemo => Person
@Test
public void testDateStr2TimeStamp() throws Exception{
String dateStr = "2020-07-13 11:16:00";
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = formatter.parse(dateStr);
logger.info(" 2020-07-13 11:16:00 to times = {}",date.getTime());
// System.out.println(date.getTime());
}
}
========== 输出的结果 ==========
14:03:09,386 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration.
14:03:09,387 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@36d4b5c - Registering current configuration as safe fallback point
2020-07-24 14:03:09.394 [main] INFO com.lolo.entity.Person[51] - 2020-07-13 11:16:00 to times = 1594610160000
【注意】
- Logger logger 对象对应的类为:
org.slf4j.Logger
; 在我们输入 Logger 可能有一堆相同类名对象,请注意使用org.slf4j.Logger
。因为前面已讲过,Slf4j 一统天下。使用此接口,方便后续任意日志框架移植和切换;- LoggerFactory.getLogger(TestDemo.class); 中的 class 类,应为日志所在类的名称,不要随意书写。若像上面代码写成 Person.class ,那么日志输出就成了
... com.lolo.entity.Person[51] ...
,导致我们来看日志定位不明朗不准确;
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<timestamp key="bySecond" datePattern="yyyyMMdd'T'HHmmss"/>
<contextName>${bySecond}contextName>
<property name="LOG_HOME" value="D:/log/" />
<property name="APP_NAME" value="lolo"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{16}[%line] - %msg%npattern>
encoder>
appender>
<appender name="DATELOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${LOG_HOME}/${APP_NAME}.logFile>
<append>trueappend>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>infolevel>
<onMatch>ACCEPTonMatch>
<onMismatch>DENYonMismatch>
filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<FileNamePattern>${LOG_HOME}/${APP_NAME}.%i.bakFileNamePattern>
<MinIndex>1MinIndex>
<MaxIndex>30MaxIndex>
rollingPolicy>
<triggeringPolicy
class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>100MBMaxFileSize>
triggeringPolicy>
<prudent>falseprudent>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}[%line] - %msg%npattern>
encoder>
appender>
<appender name="DATELOG_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${LOG_HOME}${APP_NAME}.error.logFile>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERRORlevel>
<onMatch>ACCEPTonMatch>
<onMismatch>DENYonMismatch>
filter>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<FileNamePattern>${LOG_HOME}/${APP_NAME}.error.%i.bakFileNamePattern>
<MinIndex>1MinIndex>
<MaxIndex>30MaxIndex>
rollingPolicy>
<triggeringPolicy
class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>100MBMaxFileSize>
triggeringPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}[%line] - %msg%npattern>
encoder>
appender>
<logger name="com.jieshun.jscp.p" level="DEBUG" additivity="false" >
<appender-ref ref="CONSOLE" />
<appender-ref ref="DATELOG" />
<appender-ref ref="DATELOG_ERROR" />
logger>
<root level="INFO" >
<appender-ref ref="CONSOLE" />
<appender-ref ref="DATELOG" />
<appender-ref ref="DATELOG_ERROR" />
root>
configuration>
第1步: 引入依赖。
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-slf4j-implartifactId>
<version>2.12.1version>
dependency>
<dependency>
<groupId>com.lmaxgroupId>
<artifactId>disruptorartifactId>
<version>3.4.2version>
dependency>
1、一般需要 log4j-core, log4j-api, slf4j-api jar包。下面的 log4j-slf4j-impl 涵盖所有,因为依赖传递,包含了log4j-core, log4j-api, slf4j-api 。若jar 包引入又问题,将会报如下错:
# 1. 错误如下 SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". SLF4J: Defaulting to no-operation (NOP) logger implementation SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details. # 2. 因为sl4j和log4j的不兼容导致的,具体处理方案如下: 1)若能重新引入依赖,那参考上面引入一个高版本的 log4j-slf4j-impl即可。 2)若不想上面对应的操作,那就试探引入 对应 slf4j-log4j 版本桥接包,尽让版本高些即可;
2、disruptor 的版本不能太低,否则会报:java.lang.NoSuchMethodError: com.lmax.disruptor.dsl.Disruptor.
3.、在依赖包时,默认会到mvn中央仓库去拿数据。但是那可能很慢,导致更新失败或拉不到jar。此时,把 repositories 配置到 aliyun 仓库即可<repositories> <repository> <id>aliyun-repoid> <name>nexus aliyunname> <url>http://maven.aliyun.com/nexus/content/groups/public/url> repository> repositories>
<Configuration status="info" monitorInterval="30">
<Properties>
<Property name="baseLogDir">logsProperty>
<Property name="pattern">%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %level %c{64}[%L] - %msg%nProperty>
Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout charset="UTF-8">
<Pattern>${pattern}Pattern>
PatternLayout>
Console>
<RollingRandomAccessFile name="APPINFO_APPENDER" fileName="${baseLogDir}/appinfo.log"
filePattern="${baseLogDir}/appinfo.log.%d{yyyyMMddHH}.%i.gz">
<PatternLayout>
<Pattern>${pattern}Pattern>
PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="500MB" />
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
Policies>
<Filters>
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY" />
Filters>
<DefaultRolloverStrategy max="20">
<Delete basePath="${baseLogDir}" maxDepth="1">
<IfFileName glob="*.gz" />
<IfLastModified age="3d" />
Delete>
DefaultRolloverStrategy>
RollingRandomAccessFile>
Appenders>
<Loggers>
<AsyncRoot level="info">
<AppenderRef ref="Console" />
AsyncRoot>
<AsyncLogger name="com.lolo" level="info" includeLocation="false" additivity="false">
<AppenderRef ref="APPINFO_APPENDER" />
AsyncLogger>
Loggers>
Configuration>
简易日志配置如上。若我们需要对日志分类管理及存储: ThresholdFilter 配置作适当修改即可!
<Configuration status="INFO" monitorInterval="30">
<Properties>
<Property name="baseLogDir">logsProperty>
<Property name="pattern">%d{yyyyMMdd-HHmmss.SSS} [%level] %c{1} - %msg%nProperty>
Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout>
<Pattern>${pattern}Pattern>
PatternLayout>
Console>
<RollingRandomAccessFile name="SYS_APPENDER" fileName="${baseLogDir}/server.log"
filePattern="${baseLogDir}/server.log.%d{yyyyMMddHH}.%i.gz">
<PatternLayout>
<Pattern>${pattern}Pattern>
PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="200MB" />
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
Policies>
<Filters>
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY" />
Filters>
<DefaultRolloverStrategy max="6">
<Delete basePath="${baseLogDir}" maxDepth="1">
<IfFileName glob="*.gz" />
<IfLastModified age="1d" />
Delete>
DefaultRolloverStrategy>
RollingRandomAccessFile>
<RollingRandomAccessFile name="APPINFO_APPENDER" fileName="${baseLogDir}/appinfo.log"
filePattern="${baseLogDir}/appinfo.log.%d{yyyyMMddHH}.%i.gz">
<PatternLayout>
<Pattern>${pattern}Pattern>
PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="500MB" />
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
Policies>
<Filters>
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL" />
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY" />
Filters>
<DefaultRolloverStrategy max="20">
<Delete basePath="${baseLogDir}" maxDepth="1">
<IfFileName glob="*.gz" />
<IfLastModified age="3d" />
Delete>
DefaultRolloverStrategy>
RollingRandomAccessFile>
<RollingRandomAccessFile name="APPERROR_APPENDER" fileName="${baseLogDir}/apperror.log"
filePattern="${baseLogDir}/apperror.log.%d{yyyyMMddHH}.%i.gz">
<PatternLayout>
<Pattern>${pattern}Pattern>
PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="500MB" />
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
Policies>
<Filters>
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
Filters>
<DefaultRolloverStrategy max="10">
<Delete basePath="${baseLogDir}" maxDepth="1">
<IfFileName glob="*.gz" />
<IfLastModified age="3d" />
Delete>
DefaultRolloverStrategy>
RollingRandomAccessFile>
Appenders>
<Loggers>
<AsyncRoot level="WARN">
<AppenderRef ref="Console" />
<AppenderRef ref="SYS_APPENDER" />
AsyncRoot>
<AsyncLogger name="com.lolo" level="INFO" includeLocation="false" additivity="false">
<AppenderRef ref="APPINFO_APPENDER" />
<AppenderRef ref="APPERROR_APPENDER" />
AsyncLogger>
Loggers>
Configuration>
log4j2提供了AsyncAppender和AsyncLogger以及全局异步,开启方式如下
# 全局异步 jvm 参数配置
-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
# 修正disruptor等待策略 4个策略: Block - Timeout - Sleep - Yield
-Dlog4j2.asyncLoggerWaitStrategy=Block
日志模式使用注意事项:
- 如果使用异步,建议使用AsyncLogger实现而不是AsyncAppender
- 如果使用同步,AsyncLogger、AsyncAppender和全局异步只能使用一种,不可以同时配置AsyncAppender和AsyncLogger,或者配置了异步的情况下启用了全局异步
log4j2提供了基于文件大小的滚动策略和基于时间的滚动策略,也可以二者并用,这里给出基于大小的滚动策略配置和基于大小/时间双滚动策略配置。
<RollingRandomAccessFile name="APPINFO_APPENDER" fileName="${baseLogDir}/appinfo.log"
filePattern="${baseLogDir}/appinfo.log.%i.gz">
<PatternLayout>
<Pattern>${pattern}Pattern>
PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="500MB" />
Policies>
<Filters>
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL" />
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY" />
Filters>
<DefaultRolloverStrategy max="20"/>
RollingRandomAccessFile>
<RollingRandomAccessFile name="APPINFO_APPENDER" fileName="${baseLogDir}/appinfo.log"
filePattern="${baseLogDir}/appinfo.log.%d{yyyyMMddHH}.%i.gz">
<PatternLayout>
<Pattern>${pattern}Pattern>
PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="500MB" />
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
Policies>
<Filters>
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL" />
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY" />
Filters>
<DefaultRolloverStrategy max="20">
<Delete basePath="${baseLogDir}" maxDepth="1">
<IfFileName glob="*.gz" />
<IfLastModified age="3d" />
Delete>
DefaultRolloverStrategy>
RollingRandomAccessFile>
注意:控制总的日志留存时间的机制,需要log4j-2.5及以上的版本支持,如上面的 2.12版本!
相关测试验证: 同步性能最差(这里就不多说了),异步全局异步的性能接近异步appender的10倍,同样是异步实现的,为何性能有如此大的差距?
参考:disruptor 算法
log4j2的日志使用了disruptor,其内部使用了基于ringbuffer的环形队列,并且也有生产者消费者的概念。在消费者等待消息事件(也就是日志消息)时,其内部有一处等待策略的配置,配置项可以是Block/Timeout/Sleep/Yield,默认Timeout,不同策略的含义如下:
log4j2默认策略是Timeout,在实际测试中,我们尝试测试出不同策略下的cpu占用和延迟时间情况,但测试结果并没有明显的数据对比,因此这里仅供参考,应用如果修改,需要结合场景做全面的测试。例如如果发现cpu占用较高,可以尝试修改为Block或者其他策略并测试观察。
# 修改disruptor wait策略的方法为(以修改为Block为例)
-Dlog4j2.asyncLoggerWaitStrategy=Block
// 不推荐方式
logger.debug("this is log4j2, current time:" + System.currentTimeMillis());
// 推荐使用占位符
logger.debug("this is log4j2, current time:{}", System.currentTimeMillis());
上面两行的代码功能相同,但是前一句在每次执行时,无论我们的日志级别是不是debug即以上,每次都会生成一个新的字符串,字符串的字面值是前缀加上系统当前时间。即使我们的日志级别配置成warn,该句也会产生一个新的字符串
后一句,当我们日志级别是debug或者小于debug的时候,才会真的创建一个完整的字符串,否则内存中只会有包含了占位符的唯一一个字符串
如果这种情况非常多,那么直接拼接字符串的方式对于内存的浪费就非常明显了。若这时候使用占位符的方式,可以明显的改善字符串的生成数量。当然也不是说任何地方都要使用占位符,因为占位符拼接成字符串,也是有开销的,起码要遍历占位符后面的参数。因此一般建议: