现状

各个应用日志策略比较混乱且没有统一的标准,存在大量重复日志输出以及日志滚动策略配置不合理等现象。

示例,包括但不限于:

• additivity="false"导致部分日志无法传递给父Logger处理

• 无脑additivity="true"导致大量重复日志打印

• 滚动保留文件配置过大,单个文件大小太小,频繁归档日志打高CPU

• includeLocation默认为false,日志事件中不包含源代码文件名和行号等信息

• 滚动文件命名导致打爆磁盘(filePattern="${logPath}/error-%d{yyyy-MM-dd-HH:mm:ss}-%i.log")

最终目标:

• 部门内部形成良好的日志配置策略。

• 业务日志按类型分成2-3个日志文件,ERROR日志统一输出到erroor.log。

0、基础结构

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">

    <Properties>
        <!-- 配置属性 -->
    </Properties>

    <Appenders>
        <!-- 配置输出源 -->
    </Appenders>

    <Loggers>
        <!-- 配置Logger -->
    </Loggers>
</Configuration>

status应该记录到控制台的内部 Log4j 事件的级别。此属性的有效值为“ trace”,“ debug”,“ info”,“ warn”,“ error”和“ fatal”。

Log4j 会将有关初始化,过渡和其他内部操作的详细信息记录到状态 Logger 中

• trace:输出所有信息,包括调试、日志和错误信息。

• debug:输出调试、日志和错误信息。

• info:输出日志和错误信息。

• warn:只输出警告和错误信息。

• error:只输出错误信息。

• fatal:只输出致命错误信息。

• off:关闭所有日志输出。

log4j2 的启动级别只是控制日志的最低级别,即低于该级别的日志不会被打印(不影响业务日志)。

1、属性列表

<Properties>
    <Property name="logPath">/export/Logs/app.demo</Property>
</Properties>

<Properties> 标签用于定义可重用的配置属性,将这些属性定义在 <Properties> 标签中,可以在全局范围内使用${name}进行替换

2、输出源

<Appenders>
        <Console name="console" target="SYSTEM_OUT">
        <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}] %t %p %X{name} [%c(line\:%L)] - %m%n" />
        </Console>

          <RollingRandomAccessFile name="errorFile" 
                        immediateFlush="false" 
                        fileName="${logPath}/error.log"
            filePattern="${logPath}/error-%i.log">
            <Filters>
                  <!--只允许级别为error的日志通过-->
                <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
        <PatternLayout>
        <Pattern>[%d{yyyy-MM-dd HH:mm:ss}] %t %p %X{name} [%c(line\:%L)] - %m%n</Pattern>
        </PatternLayout>
        <Policies>
            <SizeBasedTriggeringPolicy size="50MB" />
        </Policies>
        <DefaultRolloverStrategy max="5" />
    </RollingRandomAccessFile>

    <RollingRandomAccessFile name="serviceFile"
            immediateFlush="false" fileName="${logPath}/service.log"
            filePattern="${logPath}/service-%i.log">
        <PatternLayout>
            <Pattern>[%d{yyyy-MM-dd HH:mm:ss}] %t %p %X{name} [%c(line\:%L)] - %m%n</Pattern>
        </PatternLayout>
        <Policies>
            <SizeBasedTriggeringPolicy size="300MB" />
        </Policies>
        <DefaultRolloverStrategy max="5" />
    </RollingRandomAccessFile>
</Appenders>

RollingRandomAccessFile 是 Log4j2 中的一个输出源,用于将日志信息输出到文件中,并支持自动轮换日志文件。在 RollingRandomAccessFile 的属性中,常用的有以下几个:

• fileName:表示输出到哪个文件,必填项

• filePattern:轮换文件的路径和名称格式,必填项。例如可以使用文件名模式实现按大小轮换:${logPath}/server-%i.log,其中%i是一个可选的整数计数器,从 1 开始递增。

• append:如果为 true,则事件追加到文件中。如果为 false,则在打开文件时覆盖文件。默认值为 true。

• SizeBasedTriggeringPolicy 是 Log4j2 中的一种归档触发策略,用于在不同阈值下定期将日志文件进行归档。当日志文件大小达到设定的阈值时,就会触发文件归档操作,将当前日志文件重命名并存档,并同时创建一个新的日志文件继续输出日志。另外一个策略是TimeBasedTriggeringPolicy(基于时间的触发策略,用于按照时间间隔对日志文件进行切割和滚动,默认情况下每天生成一个新的日志文件)

• name:输出源名称,可以任意设置。

• immediateFlush:默认true,当设置为 true 时,表示事件会立即刷新到输出源中;否则,Log4j2 会缓冲事件并定期刷新到输出源。这个缓冲可以提高性能,但也可能存在风险,例如如果程序崩溃,则可能会导致部分日志丢失。

滚动策略

1.基于时间的触发策略

基于时间的触发策略是以时间为触发条件来实现日志文件的自动切割和滚动。常见的时间触发策略有以下几种:

•TimeBasedTriggeringPolicy:按照时间间隔对日志文件进行切割和滚动,默认情况下每天生成一个新的日志文件。(modulate 属性是为了避免多个节点(例如多台服务器)同时触发日志滚动而导致文件命名冲突的问题。设置为 true 时,会将当前时间戳减去随机数量的单位时间(默认为 10 秒)来调整日志文件名。这样可以使不同节点生成的日志文件名尽可能均匀地分布在时间轴上,避免同一时刻由多个节点产生的日志文件重名。)

•CronTriggeringPolicy:基于 Cron 表达式的触发策略,通过指定表达式中的时间参数来控制日志切割和滚动的时间点,非常灵活。

2.基于文件大小的触发策略

基于文件大小的触发策略是在日志文件达到一定大小时对其进行切割和滚动,以防止单个日志文件过大而影响读取和管理。常见的文件大小触发策略有以下几种:

•SizeBasedTriggeringPolicy:基于日志文件大小的触发策略,可以指定日志文件的最大大小,当日志文件大小超过该值时会生成一个新的日志文件。

•CompositeTriggeringPolicy:通过同时指定多个触发条件来实现更加灵活的触发策略,例如可以同时基于时间和文件大小来触发日志滚动。(将多个TriggeringPolicy放到Policies中表示使用复合策略)

在实际应用中,我们可以根据自己的需求来选择合适的日志滚动策略,并设置好相应的参数,以便保证日志文件的可读性和易管理性。一些最佳实践包括但不限于:

•基于时间触发的策略可以设置为每天或每小时切割一次日志文件,避免单个日志文件过大而影响性能。

•建议同时使用基于时间和文件大小的触发策略,以防止因日志文件过大而造成软件崩溃或系统瘫痪。

•在设置文件大小触发策略时,需要按照实际业务需求来确定日志文件大小的上限,一般建议不要超过 1GB。

•注意配置日志文件的保留期限和数量,避免日志文件占满磁盘空间。

•一个合理的日志滚动策略配置:SUM(日志文件.log * max文件数) < 应用容器磁盘空间80%。

<Appenders>
    <RollingFile name="RollingFile"
                 fileName="logs/app.log"
                 filePattern="logs/$${date:yyyy-MM}/app-%d{yyyyMMdd-HH-mm}.log">
      <PatternLayout pattern="%d %-5p [%t] %C{2} (%F:%L) - %m%n"/>
      <Policies>
        <!-- 定义以时间为基础的触发策略,每半小时触发一次 -->
        <TimeBasedTriggeringPolicy interval="30" modulate="true" />
      </Policies>
    </RollingFile>
  </Appenders>
<Appenders>
    <RollingFile name="RollingFile" fileName="logs/app.log"
                 filePattern="logs/app-%d{yyyy-MM-dd}.log">
      <Policies>
        <!-- 定义以时间为基础的触发策略,每天触发一次 -->
        <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
      </Policies>
    </RollingFile>
  </Appenders>

Filters

亲测发现onMismatch="NEUTRAL"时由下一个filter处理,如果当前是最后一个,会匹配该级别以下的日志事件。(Filters过滤链从高级-低级写)

多个过滤条件一定要包含在<Filters>标签中,否则仅生效第一个过滤条件!!!

<Filters>
    <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
    <ThresholdFilter level="warn"  onMatch="DENY" onMismatch="NEUTRAL"/>
    <ThresholdFilter level="info"  onMatch="ACCEPT" onMismatch="NEUTRAL"/>
    <ThresholdFilter level="debug"  onMatch="DENY" onMismatch="DENY"/>
</Filters>

在<DefaultRolloverStrategy> 标签中,max 属性表示最多保留的日志文件数目。当达到这个数目后,较早的日志文件将会被删除或覆盖,以保证磁盘空间的合理利用。如果设置的过小,可能会导致日志信息丢失或存储不足;而设置过大,则可能会造成磁盘空间的浪费。因此,在实际应用中,应该根据业务需求和磁盘空间情况合理设置日志文件数目的限制。

includeLocation 是 Log4j2 日志框架中的一个属性,用于控制是否在日志事件中包含源代码文件名和行号等位置相关信息。当该属性设置为 true 时,Log4j2 将在异步线程中记录位置信息,以便在日志布局或自定义过滤器中使用。默认false。

queueSize子标签:指定异步队列的最大长度(日志事件条数)默认256,超出此长度的日志事件会被丢弃。例如:<queueSize value="1024"/>。

PatternLayout

%p:输出日志信息的优先级,即DEBUG,INFO,WARN,ERROR,FATAL。
%d:输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,如:%d{yyyy/MM/dd HH:mm:ss,SSS}。
%r:输出自应用程序启动到输出该log信息耗费的毫秒数。
%t:输出产生该日志事件的线程名。
%l:输出日志事件的发生位置,相当于%c.%M(%F:%L)的组合,包括类全名、方法、文件名以及在代码中的行数。例如:test.TestLog4j.main(TestLog4j.java:10)。
%c:输出日志信息所属的类目,通常就是所在类的全名。
%M:输出产生日志信息的方法名。
%F:输出日志消息产生时所在的文件名称。
%L::输出代码中的行号。
%m::输出代码中指定的具体日志信息。
%n:输出一个回车换行符,Windows平台为"\r\n",Unix平台为"\n"。
%x:输出和当前线程相关联的NDC(嵌套诊断环境),尤其用到像java servlets这样的多客户多线程的应用中。
%%:输出一个"%"字符。

另外,还可以在%与格式字符之间加上修饰符来控制其最小长度、最大长度、和文本的对齐方式。如:
c:指定输出category的名称,最小的长度是20,如果category的名称长度小于20的话,默认的情况下右对齐。
2)%-20c:"-"号表示左对齐。
3)%.30c:指定输出category的名称,最大的长度是30,如果category的名称长度大于30的话,就会将左边多出的字符截掉,但小于30的话也不会补空格。

3、Logger

<Loggers>
    <AsyncLogger name="com.xxx.service.impl" level="DEBUG"
                 additivity="true">
        <AppenderRef ref="serviceFile" />
    </AsyncLogger>

    <AsyncRoot name="global" level="DEBUG">
        <AppenderRef ref="globalFile" />
        <AppenderRef ref="errorFile" />
    </AsyncRoot>
</Loggers>

AsyncLogger 是 Log4j2 中的一种异步日志记录器,可以在高吞吐量场景下提高应用程序的性能。与传统的同步日志记录器相比,AsyncLogger 通过将日志事件异步地写入磁盘,从而避免了主线程的阻塞,提高了系统的响应能力和处理能力。

AsyncLogger 不会将日志位置(即源代码文件名和行号)信息传递到异步线程,如果需要在布局或自定义过滤器中使用位置信息,则需要设置 "includeLocation=true" 属性。此外,使用AsyncLogger 需要特别小心,因为使用不当可能会导致丢失日志事件或产生未预期的结果。

AsyncLogger 中的additivity 属性是一个布尔类型的值,用于控制日志事件是否传递给其父记录器。如果 additivity 属性设置为true,则日志事件将被传递给其父记录器,否则它将只在当前记录器中进行处理,并不会传递给其父记录器。

AsyncLogger 中的level 属性用于控制日志事件的级别。只有当某个日志事件的级别高于或等于指定的级别时,才会被记录并传递到异步线程中。

但是!!!

通过additivity传递的日志不管Logger配置的level。

注:还有一种使用全异步日志的方式,JVM启动参数中增加:

-DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

当配置AsyncLoggerContextSelector作为异步日志时,确保在Log4j xml配置中使用普通的 <root> 和 <logger> 元素。

原文解释如下:

When AsyncLoggerContextSelector is used to make all loggers asynchronous, make sure to use normal <root> and <logger> elements in the configuration. The AsyncLoggerContextSelector will ensure that all loggers are asynchronous, using a mechanism that is different from what happens when you configure <asyncRoot> or <asyncLogger>. The latter elements are intended for mixing async with sync loggers. If you use both mechanisms together you will end up with two background threads, where your application passes the log message to thread A, which passes the message to thread B, which then finally logs the message to disk. This works, but there will be an unnecessary step in the middle.

实践

<?xml version="1.0" encoding="UTF-8"?><Configuration status="warn" name="demo_app">

    <Properties>
        <Property name="logPath">/export/Logs/app.demo</Property>
    </Properties>

    <Appenders>
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}] %t %p %X{name} [%c(line\:%L)] - %m%n" />
        </Console>

        <!-- 全局app日志输出info、warn日志  -->
        <RollingRandomAccessFile name="appFile"
                                 immediateFlush="false" fileName="${logPath}/app.log"
                                 filePattern="${logPath}/app-%i.log">
            <Filters>
                <ThresholdFilter level="ERROR" onMatch="DENY" onMismatch="NEUTRAL"/>
                <ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
                <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
            <PatternLayout>
                <Pattern>[%d{yyyy-MM-dd HH:mm:ss}] %t %p %X{name} [%c(line\:%L)] - %m%n</Pattern>
            </PatternLayout>
            <Policies>
                <SizeBasedTriggeringPolicy size="300MB" />
            </Policies>
            <DefaultRolloverStrategy max="10" />
        </RollingRandomAccessFile>

        <!-- 全局error日志  -->
        <RollingRandomAccessFile name="errorFile"
                                  immediateFlush="false" fileName="${logPath}/error.log"
                                  filePattern="${logPath}/error-%i.log">
             <Filters>
                   <!--只允许级别为error的日志通过-->
                 <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
             </Filters>
             <PatternLayout>
                 <Pattern>[%d{yyyy-MM-dd HH:mm:ss}] %t %p %X{name} [%c(line\:%L)] - %m%n</Pattern>
             </PatternLayout>
             <Policies>
                 <SizeBasedTriggeringPolicy size="50MB" />
             </Policies>
             <DefaultRolloverStrategy max="5" />
         </RollingRandomAccessFile>

        <!-- 某个核心业务日志输出info -->
        <RollingRandomAccessFile name="serviceFile"
                                 immediateFlush="false" fileName="${logPath}/service.log"
                                 filePattern="${logPath}/service-%i.log">
            <Filters>
                <ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL"/>
                <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>            
            <PatternLayout>
                <Pattern>[%d{yyyy-MM-dd HH:mm:ss}] %t %p %X{name} [%c(line\:%L)] - %m%n</Pattern>
            </PatternLayout>
            <Policies>
                <SizeBasedTriggeringPolicy size="300MB" />
            </Policies>
            <DefaultRolloverStrategy max="5" />
        </RollingRandomAccessFile>         
    </Appenders>

    <Loggers>
        <AsyncLogger name="com.**.**.**" level="DEBUG"
                     additivity="false">
            <AppenderRef ref="serviceFile" />
            <AppenderRef ref="errorFile" />
        </AsyncLogger>

        <AsyncRoot name="global" level="error">
            <AppenderRef ref="appFile" />
            <AppenderRef ref="errorFile" />
        </AsyncRoot>
    </Loggers>
</Configuration>