Log4J2官方文档学习实践

摘要:Log4j是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

Log4J2有各种Appender,通过不同的方式去输出Log。log4j2.xml

例如一些XML标签的应用 :

  • <console> 输出到控制台 
  • <file>写道临时文件中 
  • <RollingFile> 滚动写入,自动归档 (文件到了100MB ,就会归档

支持Cassandra写到Cassandra数据库
支持Console控制台
支持Failover往一个地方写失败了就写入到另一个地方去
支持Flume大数据日志采集系统
支持JDBC写入关系型数据库
支持JMS写入外部消息系统
支持JPAJPA标准写入数据库
支持HTTP访问某一个HTTP接口
支持Kafka写入消息中间件
支持Mapped File内存映射文件

Async

Appender支持异步的Append,也就是Async模式。

他的工作原理就是接收一些引用定位到其他的appender,使用一个独立的线程去写日志,也就是完全异步化。

但是这里需要注意:写到其他的appender抛出的异常是直接不可见的,是隐藏起来的。

当然一般来说是配好了所有的Appender最后去做的一个配置。默认情况下Async会使用一个ArrayBlockingQueue。当我们的应用使用多线程去跑程序时必须对Async进行关注,他可能会导致锁的竞争,会导致性能的下降。

当我们需要在异步环境中提高打日志的性能,一般来说建议用lock-free Async Loggers,也就是无锁化异步Logger。倒也无需太关注AsyncAppender。

官方文档见:Log4j – Log4j 2 Appendershttps://logging.apache.org/log4j/2.x/manual/appenders.html#AsyncAppender

首先我们关注一下配置:

<?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>

如代码块所见:Root Logger里面只引用了一个Async,Async里面再去引用别人,这里是MyFile。

通过这种方式可以让我们把日志先放入ArrayBlockingQueue,再去往其他的Append里面写入。

当然里面也有一些配置:

AppenderRefAsyncAppend引用的一些其他的Append
blocking如果设置为true,队列满了就会阻塞住,这个其实就不太好了
shutdownTimeout关闭时的超时时间
BlockingQueueFactory

你可以自己定义一个Queue的工厂

不过这些也不用细究

文档里也说了正常来说也不建议使用,这个BlockingQueueFactory提供了几种queue的实现,比如

  • ArrayBlockingQueue:默认的实现
  • DisruptorBlockingQueue
  • JCToolsBlockingQueue
  • LinkedTransferQueue

但其实比较推荐的并不是用Async,而是异步的Logger


FailoverAppender

官方文档见:​​​​​​Log4j – Log4j 2 Appendershttps://logging.apache.org/log4j/2.x/manual/appenders.html#FailoverAppender

当我们使用FailoverAppender时,就可以先定义一个primary Appender,再定义一个secondary appenders,这个东西说实在还是挺有用的。

因为他会优先往primary里面打印,如果失败了再往secondary里面去写。那么如果我们后续有需求要把日志输入到ES中去,是不是就得用这个了?

到了那时我们primary就是打印进ES中,但是因为可能有一些不可预知的异常,比如网络通信导致写入失败了,那么肯定就得落盘到磁盘文件中去。

Parameter NameTypeDescription
filterFilter

一个过滤器,用于确定事件是否应由此 Appender 处理。

使用 CompositeFilter 可以使用多个过滤器。

primaryString要使用的主要 Appender 的名称。
failoversString[]要使用的辅助 Appender 的名称。
nameStringThe name of the Appender.

retryIntervalSeconds

[重试间隔]

integer默认60,如果primary失败了那么不会进行重试.
ignoreExceptionsboolean

默认值为 true,会导致遇到的异常时被记录到内部,然后被忽略。

当设置为 false 时,异常将被传播给调用者。

targetStringEither "SYSTEM_OUT" or "SYSTEM_ERR". The default is "SYSTEM_ERR".

官方配置示例:

<?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>

我们可以看到在示例中直接引用了Failover,Failover定义了滚动文件。

如果失败了就可以配置控制台输出。


KafkaAppender

这个其实我们可以关注一下,把日志往Kafka里面去打。对应着我们可以开一个后台的服务,比如统一日志中心。日志中心里面就包含了Kafka以及对应的消费组件。消费组件负责把Kafka里的日志写入到ES中去,其实这也是一个不错的思路。然后我们在基于ES做一个对应的日志查询系统就可以了。

这个配置相对简单,就不一一赘述了。你可以配置一个Kafka的Topic,指定一个Key进行partition分区,确保同一个服务有序的分发到同一个分区中去。

配置syncSend默认为true,这个是为了同步的去发送以保证写Kafka出错就能反馈回来。

还有就是properties,我们需要设置的就是bootstrap.servers这样的一个地址

官方示例:

<?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

当然我们也可以把日志直接写到ES这种NoSQL数据库里,没必要走什么Kafka了。他会用一个比较轻量级的接口,目前接口对MongoDB和CouchDB都有实现。如果说我们需要自定义provider其实也非常简单。这个其实还是比较值的学习的。

这里的配置最重要的就是定义一个自己的provider(NoSqlProvider),我们对ES的定义直接写在这个配置就可以了。

文档上直接说了推荐我们阅读一下MongoDB和CounchDB的源码作为一个指导【To create your own custom provider, read the JavaDoc for the NoSQLProviderNoSQLConnection, and NoSQLObject classes】,学习一下日志是如何直接写入NoSQL数据库中去的。

当然,如果这样做相对来说要麻烦一些,如果用Kafka来中转就会简单很多

{
    "level": "WARN",
    "loggerName": "com.example.application.MyClass",
    "message": "Something happened that you might want to know about.",
    "source": {
        "className": "com.example.application.MyClass",
        "methodName": "exampleMethod",
        "fileName": "MyClass.java",
        "lineNumber": 81
    },
    "marker": {
        "name": "SomeMarker",
        "parent" {
            "name": "SomeParentMarker"
        }
    },
    "threadName": "Thread-1",
    "millis": 1368844166761,
    "date": "2013-05-18T02:29:26.761Z",
    "thrown": {
        "type": "java.sql.SQLException",
        "message": "Could not insert record. Connection lost.",
        "stackTrace": [
                { "className": "org.example.sql.driver.PreparedStatement$1", "methodName": "responder", "fileName": "PreparedStatement.java", "lineNumber": 1049 },
                { "className": "org.example.sql.driver.PreparedStatement", "methodName": "executeUpdate", "fileName": "PreparedStatement.java", "lineNumber": 738 },
                { "className": "com.example.application.MyClass", "methodName": "exampleMethod", "fileName": "MyClass.java", "lineNumber": 81 },
                { "className": "com.example.application.MainClass", "methodName": "main", "fileName": "MainClass.java", "lineNumber": 52 }
        ],
        "cause": {
            "type": "java.io.IOException",
            "message": "Connection lost.",
            "stackTrace": [
                { "className": "java.nio.channels.SocketChannel", "methodName": "write", "fileName": null, "lineNumber": -1 },
                { "className": "org.example.sql.driver.PreparedStatement$1", "methodName": "responder", "fileName": "PreparedStatement.java", "lineNumber": 1032 },
                { "className": "org.example.sql.driver.PreparedStatement", "methodName": "executeUpdate", "fileName": "PreparedStatement.java", "lineNumber": 738 },
                { "className": "com.example.application.MyClass", "methodName": "exampleMethod", "fileName": "MyClass.java", "lineNumber": 81 },
                { "className": "com.example.application.MainClass", "methodName": "main", "fileName": "MainClass.java", "lineNumber": 52 }
            ]
        }
    },
    "contextMap": {
        "ID": "86c3a497-4e67-4eed-9d6a-2e5797324d7b",
        "username": "JohnDoe"
    },
    "contextStack": [
        "topItem",
        "anotherItem",
        "bottomItem"
    ]
}

RollingFileAppender

这个还是挺常用的,RollingFile其实也算是输出流的一种,往磁盘文件里面输出嘛。

如果触发了TriggeringPolicy就会将文件进行归档。如果触发了RolloverPolicy就会把文件进行清理。所以说RollingFileAppender会使用RollingFileManager去执行磁盘文件的IO以及执行rollover开辟新文件删除旧文件。当RolloverFileAppender不能被共享的时候,RollingFileManager是可以被共享的。这个其实是说两个应用部署在一个tomcat的时候是可以共享的,当然这个你可以不用管他。😀

RolloverFileAppender可以设置两种policy:TriggeringPolicy和RolloverPolicy。TriggeringPolicy会决定在写文件的时候何时Rollove滚出来一个新的日志文件。当我们没有设置Rollove的话默认会使用DefaultRolloverSrategy。

从log4j-2.5以后我们可以在DefaultRollover里面去配置一个自定义的日志文件的删除行为。从2.8开始会默认用DirectWriteRolloverStrategy。

Parameter NameTypeDescription
appendboolean是往文件里追加而不是重写
bufferedIOboolean设置为true就会开启一个buffer。buffer如果被写满了就往磁盘里进行溢写
bufferSizeintWhen bufferedIO is true, this is the buffer size, the default is 8192 bytes.
createOnDemandboolean如果有需要的话就会自己去创建文件
filterFilterA Filter to determine if the event should be handled by this Appender. More than one Filter may be used by using a CompositeFilter.
fileNameStringThe name of the file to write to. If the file, or any of its parent directories, do not exist, they will be created.
filePatternStringThe pattern of the file name of the archived log file. The format of the pattern is dependent on the RolloverPolicy that is used. The DefaultRolloverPolicy will accept both a date/time pattern compatible with SimpleDateFormat and/or a %i which represents an integer counter. The pattern also supports interpolation at runtime so any of the Lookups (such as the DateLookup) can be included in the pattern.
immediateFlushboolean

如果为true那么每次写文件都会被flush,每次都会写到磁盘文件中去。只有在使用同步Loggers中才生效,并且只有对于一批数据才会执行flush

layout【布局】LayoutThe Layout to use to format the LogEvent. If no layout is supplied the default pattern layout of "%m%n" will be used.
nameStringThe name of the Appender.
policyTriggeringPolicyThe policy to use to determine if a rollover should occur.
strategyRolloverStrategyThe strategy to use to determine the name and location of the archive file.
ignoreExceptionsbooleanThe default is true, causing exceptions encountered while appending events to be internally logged and then ignored. When set to false exceptions will be propagated to the caller, instead. You must set this to false when wrapping this Appender in a FailoverAppender.
filePermissionsString

File attribute permissions in POSIX format to apply whenever the file is created.

Underlying files system shall support POSIX file attribute view.

Examples: rw------- or rw-rw-rw- etc...

fileOwnerString

File owner to define whenever the file is created.

Changing file's owner may be restricted for security reason and Operation not permitted IOException thrown. Only processes with an effective user ID equal to the user ID of the file or with appropriate privileges may change the ownership of a file if _POSIX_CHOWN_RESTRICTED is in effect for path.

Underlying files system shall support file owner attribute view.

fileGroupString

File group to define whenever the file is created.

Underlying files system shall support POSIX file attribute view.

<?xml version="1.0" encoding="UTF-8"?>
<!-- monitorInterval:log4j2会自动检测配置文件的变化,如果变化了就重新加载,自动检测的时间间隔 -->
<configuration monitorInterval="30">
    <!-- 我们可以配置很多种不同的appender,有的是在控制台打印,有的是打印文件,甚至可能是打印不同的文件 -->
    <appenders>
        <!-- 打印到控制台的appender -->
        <console name="ConsoleAppender" target="SYSTEM_OUT">
            <PatternLayout pattern="[%d{HH:mm:ss.SSS}] [%p] - %l - %m%n"/>
        </console>

        <!-- 临时打印到文件的日志信息,特别适合测试环境的日志打印,每次系统重启都会清空文件 -->
        <File name="TestFileAppender" fileName="/usr/local/demo-log4j2/test.log" append="false">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
        </File>

        <!-- rolling,一个日志文件存满了以后,可以让他自动归档到指定目录下,也可以保留几个文件,超过文件数量就清理掉 -->
        <RollingFile name="InfoRollingFileAppender"
                     fileName="/usr/local/demo-log4j2/logs/info.log"
                     filePattern="/usr/local/demo-log4j2/logs/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log">
            <ThresholdFilter level="info" 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="WarnRollingFileAppender"
                     fileName="/usr/local/demo-log4j2/logs/warn.log"
                     filePattern="/usr/local/demo-log4j2/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>
            <DefaultRolloverStrategy max="7" />
        </RollingFile>

        <RollingFile name="ErrorRollingFileAppender"
                     fileName="/usr/local/demo-log4j2/logs/error.log"
                     filePattern="/usr/local/demo-log4j2/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日志记录组件 -->
    <loggers>
        <root level="all">
            <appender-ref ref="ConsoleAppender"/>
            <appender-ref ref="InfoRollingFileAppender"/>
            <appender-ref ref="WarnRollingFileAppender"/>
            <appender-ref ref="ErrorRollingFileAppender"/>
        </root>
    </loggers>
</configuration>

Log4j2的layouts布局


Log4j – Log4j 2 Layoutshttps://logging.apache.org/log4j/2.x/manual/layouts.html#PatternLayout

Pattern Layout是我们比较常用的一种布局方式。

例如如下示例

<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>

C{precision} 或者 class{precision} 就是打印class的类

d{pattern} 或者 date{pattern} 就是 打印时间

PatternExample
%d{DEFAULT}2012-11-02 14:34:02,123
%d{DEFAULT_MICROS}2012-11-02 14:34:02,123456

其他的大家可以参考文档去做,在这里就不细究了。


Log4j2的filter过滤

filter其实本质上就是定义appender过滤出自己感兴趣的log,比如符合warn这个级别的日志就接受,不符合就拒绝。

<ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>

filter的种类也是繁多,比如BurstFilter。它可以控制你打log的频率,甚至可以用它来进行限流😂

但其实我们一般用ThresholdFilter就足够了。


简要设计一个日志中心系统

根据上述文章所言,具体的技术我们聊的差不多了。那么在真是的生产环境中肯定有各种各样的问题的。比如:如果系统部署太多服务器了,日志怎么看呢?

正常来说:系统部署服务器少的时候也就算了。但是一旦系统部署太多服务器那么我们一定需要一个日志中心的基础设施。这个基础设施我们就可以基于ElasticSearch来构建。

我们现在知道:通过Log4j2打印日志,日志会首先写入内存队列disruptor里的,他是无锁化的。拥有相对应的后台线程,从disruptor里消费这条内存日志。通过我们配置的各个appender,写入到磁盘文件就可以了。但是现在我们希望日志一律不要进磁盘文件,因为服务器太多记录磁盘文件已经没有意义了。

这时我们就需要用log4j2比较特殊的appender:对接MQ的Appender,把日志直接异步化推送到Kafka里去。之后我们通过自己的日志中心组件写一个kafka的消费服务,持续的消费日志将日志写入ElasticSearch。对用于搜索日志的关键字段建立索引,后续我们可以基于ES开发一个可视化的搜索界面,基于我们固化好的一些搜索条件可以进行日志的搜索。

那么系统日志怎么往ES里面打?又怎么搜呢?

搜索日志呢:时间范围查询一定时最重要的一点。功能模块应该以一个标识区分,那么这个也算一个检索点。有些时候我们还需要对整个请求的生命周期关注,在这里可以设计一个链路id。

这个链路id是用ThreadLocal实现的,当请求打入时去查询某一个工具类里的ThreadLocal里面看看有没有值。如果没有值就生成一个比如说UUID,如果有值就取出这个UUID作为链路ID。当我们在这个请求级别的调用中遇到打印日志的代码,都会使用这个UUID。

打印日志:时间戳、功能名称、链路id、日志具体内容。

想明白了检索字段和日志的数据结构,我们就可以设计整条日志中心的链路了。

首先在对log4j2做配置的时候,就可以使用KafkaAppender这个组件了。在这个Appender里面配置topic、key(因为kafka进入同一个分区里的消息是有顺序的,如果有序会更好一点。当然其实无序也无所谓,因为我们搜索日志大概率还是根据时间范围或者关键词检索的)、syncSend(同步发送就可以)官方的例子就不错:

<?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>

  <Loggers>
    <Root level="DEBUG">
      <AppenderRef ref="Kafka"/>
    </Root>
    <Logger name="org.apache.kafka" level="INFO" />
  </Loggers>

从kafka里面消费到的日志如何建立ES索引?

比如往Kafka里面推送日志的结构是这样的:时间戳##功能模块名称##业务Id##链路ID。

我们在日志中心获取的日志结构也是这个,此时就可以通过##进行切割。把这些字段封装成document写入ES中。但是我们一般建议:ES里仅仅存放搜索索引就可以了,尽可能去减少ES里存储的数据,这样ES可以充分利用服务器里的os cache的内存来存放索引数据,可以加快ES后续搜索的性能。

当然:如果我们设计的日志中心大量的使用时间戳检索而没有关键词搜索的话可以参考上述建议。继续往下:具体的日志content我们就可以放入HBase里了。首先建立一张hbase表,rowkey就是ES里的自动生成的ducument的id。列族和列就可以存放logContent,这样我们后续就可以通过kv形式根据document id 读取对应的日志内容了。

到此为止日志索引的建立和日志内容的存储就全都完成了。 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值