Log4j2分析与实践-Appenders

    Appenders负责将日志事件传递到目的地。每个Appender必须实现Appender接口,大多数Appender都会继承AbstractAppender,而AbstractAppender添加了Lifecycle和Filterable支持。Lifecycle使得组件在配置加载完毕之后完成初始化操作,也使得在组件关闭时执行清理任务。Filterable使得组件可以拥有过滤器,在事件执行过程中欧冠过滤器可以发挥过滤作用。

    多数情况下,Appender将日志事件的格式化交给Layout来处理。某些Appender(记为A)里包装着其它Appender(记为B),这样的话A就可以修改日志事件、处理B中的错误、根据高级过滤标准将事件路由到下级Appender或类似功能,但不会直接对事件做最终的格式化。       Appender必须有一个名字,因为在Logger中需要通过名字来引用一个Appender。后文中提到的type字段,是对应到Java中的一种类型的,通常存在于Log4jCore包里。


AsyncAppender

    AsyncAppender可以引用多个其它Appender,然后将日志事件以一个单独的线程写入Appender。需要注意的是,在向那些Appender写入日志事件时,如出现了异常,那么这些异常是对应用隐藏的。AsyncAppender引用的Appender需要先配置,然后再配置AsyncAppender,这样做是为了在某些时候能够正常地关闭AsyncAppender。
    AsyncAppender默认采用java.util.concurrent.ArrayBlockingQueue,这个阻塞队列不需要外部库。多线程环境下需要特别注意了,阻塞队列极易受到锁竞争的影响,测试表明多线程下性能会降低,考虑使用无锁竞争的Async Loggers(后续文章会讲到)。
    在appender处理的速率跟不上日志请求速率时,设置log4j2.AsyncQueueFullPolicy和log4j2.DiscardThreshold系统属性来维持应用的吞吐量,当log4j2.AsyncQueueFullPolicy设置为Discard时,那么系统就会根据log4j2.DiscardThreshold的值来舍弃一些日志事件,log4j2.DiscardThreshold默认INFO、DEBUG和TRACE都会丢弃。
<?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>

    从Log4j2.7开始,可以指定自定义的BlockQueue或者TransferQueue实现类来作为AsyncAppender使用的队列,只需要在<Async/>元素中指定。

<Configuration name="LinkedTransferQueueExample">
  <Appenders>
    <List name="List"/>
    <Async name="Async" bufferSize="262144">
      <AppenderRef ref="List"/>
      <LinkedTransferQueue/>
    </Async>
  </Appenders>
  <Loggers>
    <Root>
      <AppenderRef ref="Async"/>
    </Root>
  </Loggers>
</Configuration>

CassandraAppender

    CassandraAppender可以将日志事件写到Apache Cassandra数据库中,keyspace和数据表需要提前定义,表的列在配置文件中映射。示例:
<Configuration name="CassandraAppenderTest">
  <Appenders>
    <Cassandra name="Cassandra" clusterName="Test Cluster" keyspace="test" table="logs" bufferSize="10" batched="true">
      <SocketAddress host="localhost" port="9042"/>
      <ColumnMapping name="id" pattern="%uuid{TIME}" type="java.util.UUID"/>
      <ColumnMapping name="timeid" literal="now()"/>
      <ColumnMapping name="message" pattern="%message"/>
      <ColumnMapping name="level" pattern="%level"/>
      <ColumnMapping name="marker" pattern="%marker"/>
      <ColumnMapping name="logger" pattern="%logger"/>
      <ColumnMapping name="timestamp" type="java.util.Date"/>
      <ColumnMapping name="mdc" type="org.apache.logging.log4j.spi.ThreadContextMap"/>
      <ColumnMapping name="ndc" type="org.apache.logging.log4j.spi.ThreadContextStack"/>
    </Cassandra>
  </Appenders>
  <Loggers>
    <Logger name="org.apache.logging.log4j.nosql.appender.cassandra" level="DEBUG">
      <AppenderRef ref="Cassandra"/>
    </Logger>
    <Root level="ERROR"/>
  </Loggers>
</Configuration>
    示例中用到的表结构:
CREATE TABLE logs (
    id timeuuid PRIMARY KEY,
    timeid timeuuid,
    message text,
    level text,
    marker text,
    logger text,
    timestamp timestamp,
    mdc map<text,text>,
    ndc list<text>
);


ConsoleAppender

    顾名思义,ConsoleAppender就是将日志事件写出到标准输出System.out或者System.err,默认为System.out。必须指定一个Layout来格式化LogEvent。
<?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>

FailoverAppender

    FailoverAppender里包装了一个Appender集合,如果第一个(或者设置为primary的)Appender失败了,那么自动使用第二个重试,以此类推,直到有一个Appender成功或者所有的Appender都失败。
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" name="MyApp" packages="">
  <Appenders>
    <RollingFile name="RollingFile" fileName="logs/app.log" filePattern="logs/app-%d{MM-dd-yyyy}.log.gz"
                 ignoreExceptions="false">
      <PatternLayout>
        <Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
      </PatternLayout>
      <TimeBasedTriggeringPolicy />
    </RollingFile>
    <Console name="STDOUT" target="SYSTEM_OUT" ignoreExceptions="false">
      <PatternLayout pattern="%m%n"/>
    </Console>
    <Failover name="Failover" primary="RollingFile">
      <Failovers>
        <AppenderRef ref="Console"/>
      </Failovers>
    </Failover>
  </Appenders>
  <Loggers>
    <Root level="error">
      <AppenderRef ref="Failover"/>
    </Root>
  </Loggers>
</Configuration>

FileAppender

    FileAppender是一个OutputStreamAppender,它将日志写到fileName指定的文件中。FileAppender实际是使用FileManager(继承自OutputStreamManager)执行文件的I/O。来自不同配置中的FileAppender可以被共享,若Manager可以访问的话,FileManager也可以被共享。比如,servlet容器中有两个web应用,如果它们的Log4j被相同的Classloader加载的话,那么它们就可以根据自身的配置安全地将日志写入相同的文件中。
<?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>

FlumeAppender

    这是一个可选的组件,以一个独立的jar包提供。
Apache Flume是一个稳定的分布式系统,用于从多个不同来源收集处理大量数据,再将这些数据转入集中的数据中心。FlumeAppender将日志事件作为序列化的Avro事件发送给Flume代理,供其消费。

JDBCAppender

    JDBCAppender支持使用标准的JDBC将日志事件写入关系数据库中,JDBC连接既可以配置为JNDI DataSource,也可以使用一个工厂方法。不管采用哪种方法,都需要使用连接池,否则性能会大大降低。

JMSAppender

    JMSAppender将日志事件发送到JMS目的地。在Log4j2.0中,这种appender被分为JMSQueueAppender和JMSTopicAppender,从Log4j2.1开始,这两种Appender被合并到JMSApender中了,不再区分队列和主题。然而,2.0中使用的<JMSQueue>和<JMSTopic>标签继续有效。

JPAAppender

    JPAAppender使用Java Persistence API 2.1将日志事件写入数据库表中。它需要在classpath上有API和provider的实现,也需要一个实体类,用其将数据持久化到表中。

KafkaAppender

    KafkaAppender可以将日志事件写入Apache Kafka主题中,它默认是同步的,也就是在Kafka服务器成功接受消息之前,它会阻塞,可以使用timout.ms属性来设置超时时间。可以使用AsyncAppender或者将syncSend设置为false来异步记录日志。
<?xml version="1.0" encoding="UTF-8"?>
  ...
  <Appenders>
    <Kafka name="Kafka" topic="log-test">
      <PatternLayout pattern="%date %message"/>
        <Property name="bootstrap.servers">localhost:9092</Property>
    </Kafka>
  </Appenders>

NoSQLAppender

    NoSQLAppender使用一个轻量级的provider接口将日志事件写入NoSQL数据库,当前Provider已有MongoDBhe和Apache CouchDB的实现,而自己实现一个provider也很容易。

OutputStreamAppender

    OutputStreamAppender是许多Appender的基类,比如File和Socket Appender,它们都将日志事件写入输出流。OutputStreamAppender不能直接配置,它支持立即冲刷和缓存,使用一个OutputStreamManager来处理I/O,允许输出流在多个配置中被Appender共享。

RandomAccessFileAppender

    RandomAccessFileAppender跟FileAppender类似,只是它总是使用缓冲(可以启用或禁用),它内部使用ByteBuffer+RandomAccessFile,而不是BufferedOutputStream。在实践中,启用缓存的RandomAccessFileAppender比FileAppender有着20%至200%的性能提升。     RandomAccessFileAppender使用一个RandomAccessFileManager来实际执行I/O。来自不同配置的RandomAccessFileAppender是不能共享的。而在Manager可被访问的情况下,RandomAccessFileManager是可以被共享的。比如一个Servlet容器里的两个应用可以拥有自己的配置,若它们的配置都是由同一个ClassLoader加载的,那么它们就可以安全地将日志写到相同的文件中。immediateFlush表示是否立即写入文件,默认为true,若要启用缓存功能,需要设置为false。可以设置缓存大小bufferSize,默认为256kb。
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" name="MyApp" packages="">
  <Appenders>
    <RandomAccessFile name="MyFile" fileName="logs/app.log" immediateFlush="false">
      <PatternLayout>
        <Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
      </PatternLayout>
    </RandomAccessFile>
  </Appenders>
  <Loggers>
    <Root level="error">
      <AppenderRef ref="MyFile"/>
    </Root>
  </Loggers>
</Configuration>

RewriteAppender

    RewriteAppender用于在其它Appender处理日志事件之前,对日志事件进行一些操作。可以用来擦除一些敏感信息(比如密码),或者向每个事件注入某些信息。RewriteAppender必须配置一个RewritePolicy。为了使得Appender可以正常关闭,RewriteAppender必须在其引用的Appender之后配置。
    RewritePolicy是一个接口,可以实现这个接口,并用于在日志事件传递给一个Appender时对其进行检查或修改。RewritePolicy只定义了一个方法rewrite(final LogEvent source),可以访问相同的事件,或者新创建一个事件。
    MapRewritePolicy会对包含MapMessage的日志事件进行操作,向其添加或更新Map中的元素。下面的示例表示向MapMessage中添加一个key和value。
<?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>
    <Rewrite name="rewrite">
      <AppenderRef ref="STDOUT"/>
      <MapRewritePolicy mode="Add">
        <KeyValuePair key="product" value="TestProduct"/>
      </MapRewritePolicy>
    </Rewrite>
  </Appenders>
  <Loggers>
    <Root level="error">
      <AppenderRef ref="Rewrite"/>
    </Root>
  </Loggers>
</Configuration>
    PropertiesRewritePolicy与MapRewritePolicy类似功能,也是向MapMessage中添加键值对。
<?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>
    <Rewrite name="rewrite">
      <AppenderRef ref="STDOUT"/>
      <PropertiesRewritePolicy>
        <Property name="user">${sys:user.name}</Property>
        <Property name="env">${sys:environment}</Property>
      </PropertiesRewritePolicy>
    </Rewrite>
  </Appenders>
  <Loggers>
    <Root level="error">
      <AppenderRef ref="Rewrite"/>
    </Root>
  </Loggers>
</Configuration>
    LoggerNameLevelRewritePolicy用于修改第三方的Logger日志级别,下面示例表示将com.foo.bar这个logger的Info日志改为DEBUG,将WARN日志改为INFO。


<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" name="MyApp">
  <Appenders>
    <Console name="STDOUT" target="SYSTEM_OUT">
      <PatternLayout pattern="%m%n"/>
    </Console>
    <Rewrite name="rewrite">
      <AppenderRef ref="STDOUT"/>
      <LoggerNameLevelRewritePolicy loggerName="com.foo.bar">
        <KeyValuePair key="INFO" value="DEBUG"/>
        <KeyValuePair key="WARN" value="INFO"/>
      </LoggerNameLevelRewritePolicy>
    </Rewrite>
  </Appenders>
  <Loggers>
    <Root level="error">
      <AppenderRef ref="Rewrite"/>
    </Root>
  </Loggers>
</Configuration>

RollingFileAppender

    RollingFileAppender应该是生产环境服务器用得最多的Appender类型了,它是一个OutputStreamAppender,它将日志写入fileName指定的日志文件中,并且根据TriggeringPolicy和RolloverPolicy进行日志切换。RollingFileAppender使用一个RollingFileManager(它继承自OutputStreamManager)实际操作文件I/O和切换。来自不同配置的RollingFileAppender不可共享,RollingFileManager在Manager可访问的情况下是可以共享的,同RandomAccessFileAppender理。
    RollingFileAppender需要一个TriggeringPolicy和一个RolloverStrategy,TriggeringPolicy决定是否执行切换日志,而RolloverStrategy决定如何切换日志。如果没有配置RolloverStrategy,那么就会使用默认的DefaultRolloverStrategy。从Log4j2.5开始,可以在DefaultRolloverStrategy中配置自定义的删除操作,从2.8开始,若没有配置fileName,会默认使用DirectWriteRolloverStrategy(切换日志文件时不会重命名之前的文件),而不是DefaultRolloverStrategy。RollingFileAppender不支持文件锁定。

Log4j2自带的TriggeringPolicy包含如下几种:

(1)CompositeTriggeringPolicy,用于同时配置多种Policy。
(2)CronTriggeringPolicy,定时切换日志。
(3)OnStartupTriggeringPolicy,启动应用时切换日志。
(4)SizeBasedTriggeringPolicy,根据日志文件大小来切换日志。
(5)TimeBasedTriggeringPolicy,根据时间来切换日志。
下面示例表示每天0点0分0秒时切换日志文件,并且只保留3天内生成的日志文件。

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info" name="Log4j2XMLConfig" strict="true">
    <Appenders>
        <Appender type="RollingFile" name="rollingAppender" fileName="logs/app.log"
                  filePattern="logs/app.%d{yyyyMMdd}.log" immediateFlush="false">
            <Layout type="PatternLayout" pattern="[%d] %m%n" />
            <Policies>
			    <!-- CronTriggeringPolicy用于定时切换日志。此例表示每天0点0分0秒切换日志文件,evaluateOnStartup表示启动的时候就检测是否需要切换 -->
                <CronTriggeringPolicy schedule="0 0 0 */1 * ?" evaluateOnStartup="true" />
            </Policies>
            <DefaultRolloverStrategy>
			    <!-- 删除3天前的日志 -->
                <Delete basePath="logs/" maxDepth="1">
                    <IfFileName glob="app.log*" />
                    <IfLastModified age="3d" />
                </Delete>
            </DefaultRolloverStrategy>
        </Appender>
    </Appenders>

    <Loggers>
        <Root level="info">
            <AppenderRef ref="accessAppender"/>
        </Root>
    </Loggers>
</Configuration>

RollingRandomAccessFileAppender

    RollingRandomAccessFileAppender和RollingFileAppender类似,不同的是它总是使用缓冲(可以启用或禁用)。


RoutingAppender

    RoutingAppender可以根据需要检测日志事件并将它们传递给其它Appender。目标Appender可以是提前配置的,也可以是动态生成的。RoutingAppender必须要在它所引用Appender之后进行配置。
    以下示例中,脚本将"ServiceWindows"作为Windows的默认路由,而将"ServiceOther"作为其它操作系统的路由。


<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" name="RoutingTest">
  <Appenders>
    <Routing name="Routing">
	  <!-- 相当于Routes元素的pattern属性,然后根据pattern的值去匹配Route中的key,再选择一个路由 -->
      <Script name="RoutingInit" language="JavaScript"><![CDATA[
        importPackage(java.lang);
        System.getProperty("os.name").search("Windows") > -1 ? "ServiceWindows" : "ServiceOther";]]>
      </Script>
      <Routes>
        <Route key="ServiceOther">
          <List name="List1" />
        </Route>
        <Route key="ServiceWindows">
          <List name="List2" />
        </Route>
      </Routes>
    </Routing>
  </Appenders>
  <Loggers>
    <Root level="error">
      <AppenderRef ref="Routing" />
    </Root>
  </Loggers>
</Configuration>

SMTPAppender

    SMTPAppender可以在指定日志事件发生时发送邮件,特别是错误日志。
    在邮件中发送的日志事件数目依赖于BufferSize的值,默认的配置将会在ERROR或者更高级别的日志事件发生时发送邮件,以下是配置示例:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" name="MyApp" packages="">
  <Appenders>
    <SMTP name="Mail" subject="Error Log" to="errors@logging.apache.org" from="test@logging.apache.org"
          smtpHost="localhost" smtpPort="25" bufferSize="50">
    </SMTP>
  </Appenders>
  <Loggers>
    <Root level="error">
      <AppenderRef ref="Mail"/>
    </Root>
  </Loggers>
</Configuration>

ScriptAppenderSelector

    ScriptAppenderSelector用于根据条件选择一个Appender。当配置构建好之后,ScriptAppenderSelector就会调用<Script>标签中的内容来获取一个Appender的名字,Log4j2然后根据这个名字在<AppenderSet>标签下选择并创建一个Appender,Log4j2只会构建选择的Appender,而会忽略<AppenderSet>下的其它Appender。
<Configuration status="WARN" name="ScriptAppenderSelectorExample">
  <Appenders>
    <ScriptAppenderSelector name="SelectIt">
      <Script language="JavaScript"><![CDATA[
        importPackage(java.lang);
        System.getProperty("os.name").search("Windows") > -1 ? "MyCustomWindowsAppender" : "MySyslogAppender";]]>
      </Script>
      <AppenderSet>
        <MyCustomWindowsAppender name="MyAppender" ... />
        <SyslogAppender name="MySyslog" ... />
      </AppenderSet>
    </ScriptAppenderSelector>
  </Appenders>
  <Loggers>
    <Root level="error">
      <AppenderRef ref="SelectIt" />
    </Root>
  </Loggers>
</Configuration>

SocketAppender

    SocketAppender是一种OutputStreamAppender,会通过一个主机名和端口号将输出写到远程目的地。可以使用TCP或者UDP以任何格式发送数据。默认的格式为发送一个已序列化的日志事件,Log4j2包含一个SocketServer,可以接收序列化的日志事件,然后通过服务器上的日志系统对它们进行路由。你也可以选择使用SSL进行安全传输。
TCP配置示例:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" name="MyApp" packages="">
  <Appenders>
    <Socket name="socket" host="localhost" port="9500">
      <SerializedLayout />
    </Socket>
  </Appenders>
  <Loggers>
    <Root level="error">
      <AppenderRef ref="socket"/>
    </Root>
  </Loggers>
</Configuration>
SSL配置示例:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" name="MyApp" packages="">
  <Appenders>
    <Socket name="socket" host="localhost" port="9500">
      <SerializedLayout />
      <SSL>
        <KeyStore location="log4j2-keystore.jks" password="changeme"/>
        <TrustStore location="truststore.jks" password="changeme"/>
      </SSL>
    </Socket>
  </Appenders>
  <Loggers>
    <Root level="error">
      <AppenderRef ref="socket"/>
    </Root>
  </Loggers>
</Configuration>

SyslogAppender

    SyslogAppender是一个SocketAppender,它会根据指定的主机名和端口号将输出写到远程目的地,格式可以是BSD Syslog或者RFC 5424。