Loading

[03] 日志体系

1. 体系概述

  • 日志接口
    • JCL:Apache 基金会所属的项目,是一套 Java 日志接口,之前叫 Jakarta Commons Logging,后更名为 Commons Logging,简称 JCL;
    • SLF4J:Simple Logging Facade for Java,缩写 Slf4j,是一套简易 Java 日志门面,只提供相关接口,和其他日志工具之间需要桥接。
  • 日志实现
    • JUL:JDK 中的日志工具,也称为 jdklog、jdk-logging,⾃ JDK 1.4 以来 sun 的官方提供;
    • Log4j:⾪属于 Apache 基金会的一套日志框架,现已不再维护;
    • Log4j2:Log4j 的升级版本,与 Log4j 变化很大,不兼容;
    • Logback:一个具体的日志实现框架,和 Slf4j 是同一个作者,性能很好;

2. 发展历程

(1)上古时代

在 JDK 1.3 及以前,Java 打日志依赖 System.out.println() / System.err.println() 或者 e.printStackTrace(),Debug 日志被写到 STDOUT 流,错误日志被写到 STDERR 流。这样打日志有一个非常大的缺陷,非常机械,无法定制,且日志粒度不够细分。

(2)开创先驱

Ceki Gülcü 于 2001 年发布了 Log4j,并将其捐献给了 Apache 软件基金会,成为 Apache 基金会的顶级项目。后来衍生支持 C, C++, C#, Perl, Python, Ruby 等语⾔。

Log4j 在设计上非常优秀,它定义的 Logger、Appender、Level 等概念对后续的 Java Log 框架有深远的影响,如今的很多日志框架基本沿用了这种思想。

Log4j 的性能是个问题,在 Logback 和 Log4j2 出来之后,2015 年 9 月,Apache 软件基金会宣布,Log4j 不再维护,建议所有相关项目升级到 Log4j2。

  • 依赖
    <dependency>
     <groupId>log4j</groupId>
     <artifactId>log4j</artifactId>
     <version>1.2.17</version>
    </dependency>
    <dependency>
     <groupId>log4j</groupId>
     <artifactId>apache-log4j-extras</artifactId>
     <version>1.2.17</version>
    </dependency>
    
  • 配置
    log4j.rootLogger=debug
    # console
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern= log4j:[%d{yyyy-MM-dd HH:mm:ssa}]:%p %l%m%n
    # dailyfile
    log4j.appender.dailyfile=org.apache.log4j.DailyRollingFileAppender
    log4j.appender.dailyfile.DatePattern='_'yyyy-MM-dd'.log'
    log4j.appender.dailyfile.File=./log4j.log
    log4j.appender.dailyfile.Append=true
    log4j.appender.dailyfile.Threshold=INFO
    log4j.appender.dailyfile.layout=org.apache.log4j.PatternLayout
    log4j.appender.dailyfile.layout.ConversionPattern=log4j:[%d{yyyy-MM-dd HH:mm:ssa}] [Thread: %t][ Class:%c >> Method: %l ]%n%p:%m%n
    
  • 代码
    import org.apache.log4j.Logger;
    public class Demo {
        Logger logger = Logger.getLogger(Demo.class);
        // ...
        logger.info("xxxxx");
        // ...
    }
    

(3)搞事情的 JUL

sun 公司在 JDK 1.4 版本后开始搞事情,增加了一个包为 java.util.logging,简称 JUL,用以对抗 Log4j ,但是却给开发造成了麻烦。相互引用的项目之间可能使用了不同的日志框架,经常将代码搞得一片混乱。

import java.util.logging.Logger;

public class Demo {
    Logger loggger = Logger.getLogger(Demo.class.getName());
    // ...
    logger.finest("xxxxx");
    // ...
}

日志配置的路径:$JAVA_HOME/jre/lib/logging.properties

JUL 功能远不如 Log4j 完善,⾃带的 Handlers 有限,性能和可用性上也一般,JUL 在 JDK 1.5 以后才有所提升。

(4)JCL 应用而生

从上面可以看出,JUL 的 API 与 Log4j 是完全不同的(参数只接受 String)。由于日志系统互相没有关联,彼此没有约定,不同⼈的代码使用不同日志,替换和统一也就变成了比较棘手的一件事。假如你的应用使用 Log4j,然后项目引用了一个其他团队的库,他们使用了 JUL,你的应用就得使用两个日志系统了,然后其他团队又使用了 simplelog …… 这个时候如果要调整日志的输出级别,用于跟踪某个信息,简直就是一场灾难。

那这个状况该如何解决呢?答案就是进行抽象,抽象出一个接口层,对每个日志实现都适配或者转接,这样这些提供给别人的库都直接使用抽象层即可 ,以后调用的时候就调用这些接口(面向接口思想)。

于是,JCL(Jakarta Commons Logging)应用而生,也就是 commons-logging-xx.jar 组件。JCL 只提供 log 接口,具体的实现则在运行时动态寻找。这样一来组件开发者只需要针对 JCL 接口开发,而调用组件的应用程序则可以在运行时搭配自己喜好的日志实践工具。

那接口下真实的日志是谁呢?参考下图:

JCL 会在 ClassLoader 中进行查找,如果能找到 Log4j 则默认使用 Log4j 实现,如果没有则使用 JUL 实现,再没有则使用 JCL 内部提供的 SimpleLog 实现。

依赖:

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>

使用:

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
// ...
Log log = LogFactory.getLog(Demo.class);
log.info("xxxxx");

JCL 缺点也很明显,一是效率较低,二是容易引发混乱,三是 JCL 的机制有很大的可能会引发内存泄露。

同时,JCL 的书写存在一个不太优雅的地方,典型的场景如下:

假如要输出一条 debug 日志,而一般情况下,生产环境 log 级别都会设到 info 或者以上,那这条 log 是不会被输出的。于是,在代码里就出现了:logger.debug("this is a debug info, message :" + msg);

这个有什么问题呢?虽然生产不会打出日志,但是这其中都会做一个字符串连接操作,然后生成一个新的字符串。如果这条语句在循环或者被调用很多次的函数中,就会多做很多无用的字符串连接,影响性能。

所以,JCL 推荐的写法如下:

if (logger.isDebugEnabled()) {
  logger.debug("this is a debug info , message :" + msg);
}

虽然解决了问题,但是这个代码实在看上去不怎么舒服 ...

(5)再起波澜

于是,针对以上情况,Log4j 的作者再次出手,他觉得 JCL 不好用,自己就又写了一个新的接口 API —— slf4j,并且为了追求更极致的性能,新增了一套日志的实现,就是 logback,一时间烽烟又起 ……

依赖:

<dependency>
 <groupId>org.slf4j</groupId>
 <artifactId>slf4j-api</artifactId>
 <version>1.7.30</version>
</dependency>
<!-- ······························ -->
<dependency>
 <groupId>ch.qos.logback</groupId>
 <artifactId>logback-core</artifactId>
 <version>1.2.3</version>
</dependency>
<dependency>
 <groupId>ch.qos.logback</groupId>
 <artifactId>logback-classic</artifactId>
 <version>1.2.3</version>
</dependency>

配置:

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
  <property name="logPattern" value="logback:[ %-5level] [%date{yyyy-MM-ddHH:mm:ss.SSS}] %logger{96} [%line] [%thread]- %msg%n"></property>
  <!-- 控制台的标准输出 -->
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <charset>UTF-8</charset>
      <pattern>${logPattern}</pattern>
    </encoder>
  </appender>
  <root level="DEBUG">
    <appender-ref ref="STDOUT"/>
  </root>
</configuration>

logback-core 提供基础抽象,logback-classic 提供日志实现,并且直接就是基于 slf4j API。所以 slf4j 配合 logback 来完成日志时,不需要像其他的日志框架一样提供适配器。

slf4j 本身并没有实际的日志输出能力,它底层还是需要去调用具体的日志框架 API,也就是它需要跟具体的日志框架结合使用。由于具体日志框架比较多,而且互相也大都不兼容,日志门面接口要想实现与任意日志框架结合就需要额外对应的桥接器(adaptation layer)

偷梁换柱:对上,application 照常调用自己选用的 logAPI;对下,修改 logAPI 的内部实现,让其再去调用 slf4jAPI,选择日志实现的切换。

有了新的 slf4j 后,上面的字符串拼接问题就被以下代码所取代,而 logback 也提供了更高级的特性,如异步 logger,Filter 等。

logger.debug("This is a debug info, message: {}", msg);

(6)再度青春

前面提到,Log4j 由 Apache 宣布 2015 年后不再维护。推荐大家升级到 Log4j2,虽然 Log4j2 沿袭了 Log4j 的思想,然而 Log4j2 和 Log4j 完全是两码事,并不兼容。

Log4j2 以性能著称,它比其前身 log4j1.x 提供了重大改进,同时类比 logback,它提供了 logback 中可用的许多改进,同时修复了 logback 架构中的一些固有问题。功能上,它有着和 logback 相同的基本操作,同时又有自己独特的部分,比如:插件式结构、配置文件优化、异步日志等。

  • 依赖
    <dependency>
     <groupId>org.apache.logging.log4j</groupId>
     <artifactId>log4j-api</artifactId>
     <version>2.13.0</version>
    </dependency>
    <dependency>
     <groupId>org.apache.logging.log4j</groupId>
     <artifactId>log4j-core</artifactId>
     <version>2.13.0</version>
    </dependency>
    <dependency>
     <groupId>com.lmax</groupId>
     <artifactId>disruptor</artifactId>
     <version>3.4.1</version>
    </dependency>
    
  • 配置
    <?xml version="1.0" encoding="UTF-8"?>
    <configuration status="info" monitorInterval="30">
      <Properties>
        <Property name="pattern">
            log4j2:[%-5p]:%d{YYYY-MM-dd HH:mm:ss} [%t]%c{1}:%L - %msg%n
        </Property>
      </Properties>
     <appenders>
       <!--console: 控制台输出的配置-->
       <Console name="Console" target="SYSTEM_OUT">
         <PatternLayout pattern="${pattern}"/>
       </Console>
     </appenders>
     <loggers>
       <logger name="org.springframework" level="INFO"></logger>
       <root level="info">
         <appender-ref ref="Console"/>
       </root>
     </loggers>
    </configuration>
    
  • 代码
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    // ...
    Logger logger = LogManager.getLogger(Demo.class);
    logger.debug("debug Msg");
    

3. 配置讲解

【日志级别】

  • trace:路径跟踪
  • debug:一般用于日常调式
  • info:打印重要信息
  • warn:给出警告
  • error:出现错误或问题

一个完整的日志组件都要具备日志级别的概念,每种日志组件级别定义不同,日常编码最经常用到的主流分级如上(低 → 高;每个日志组件的具体级别划分稍有不同,参考下文各章节)。

【日志组件】

  • appender:日志输出目的地,负责日志的输出 (输出到什么地方);
  • logger:日志记录器,负责收集处理日志记录 (如何处理日志);
  • layout:日志格式化,负责对输出的日志格式化(以什么形式展现)。

a. JUL

(1)配置文件位置

  • 默认情况下配置⽂件路径为 $JAVA_HOME\jre\lib\logging.properties
  • 可以指定配置文件
    static {
        System.setProperty("java.util.logging.config.file",
                Demo.class.getClassLoader().getResource("logging.properties").getPath());
    }
    

(2)级别

  • SEVERE(最高值)
  • WARNING
  • INFO
  • CONFIG
  • FINE
  • FINER
  • FINEST(最低值)
  • OFF,关闭日志
  • ALL,启用所有日志

(3)处理器

  • StreamHandler:日志记录写入 OutputStream;
  • ConsoleHandler:日志记录写入 System.err;
  • FileHandler:日志记录写入单个⽂件或⼀组滚动日志文件;
  • SocketHandler:日志记录写入远程 TCP 端⼝的处理程序;
  • MemoryHandler:缓冲内存中日志记录。

(4)格式化

  • SimpleFormatter:格式化为简短的日志记录摘要;
  • XMLFormatter:格式化为详细的 XML 结构信息;
  • 可自定义输出格式,继承抽象类 java.util.logging.Formatter 即可。

(5)如何修改输出级别?

############################################################
#  	Default Logging Configuration File
#
# You can use a different file by specifying a filename
# with the java.util.logging.config.file system property.
# For example java -Djava.util.logging.config.file=myfile
############################################################

############################################################
#  	Global properties
############################################################

# "handlers" specifies a comma separated list of log Handler
# classes.  These handlers will be installed during VM startup.
# Note that these classes must be on the system classpath.
# By default we only configure a ConsoleHandler, which will only
# show messages at the INFO and above levels.
handlers= java.util.logging.ConsoleHandler

# To also add the FileHandler, use the following line instead.
#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler

# Default global logging level.
# This specifies which kinds of events are logged across
# all loggers.  For any given facility this global level
# can be overriden by a facility specific level
# Note that the ConsoleHandler also has a separate level
# setting to limit messages printed to the console.
.level= FINE

############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
############################################################

# default file output is in user's home directory.
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter

# Limit the message that are printed on the console to INFO and above.
java.util.logging.ConsoleHandler.level = FINE
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

# Example to customize the SimpleFormatter output format
# to print one-line log message like this:
#     <level>: <log message> [<date/time>]
#
# java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n

############################################################
# Facility specific properties.
# Provides extra control for each logger.
############################################################

# For example, set the com.xyz.foo logger to only log SEVERE
# messages:
com.xyz.foo.level = SEVERE

b. Log4j

(1)配置文件

启动时,默认会寻找 sourceFolder 下的 log4j.xml;若没有,会寻找 log4j.properties。

(2)级别

  • FATAL(最高)
  • ERROR
  • WARN
  • INFO
  • DEBUG (最低)
  • OFF,关闭日志
  • ALL,启用所有日志

(3)处理器

  • org.apache.log4j.ConsoleAppender(控制台)
  • org.apache.log4j.FileAppender(文件)
  • org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)
  • org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生⼀个新的文件)
  • org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)

(4)格式化

  • org.apache.log4j.HTMLLayout(以 HTML 表格形式布局)
  • org.apache.log4j.PatternLayout(可以灵活地指定布局模式)
  • org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串)
  • org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等信息)

(5) example

# console
log4j.rootLogger=INFO,stdout,info,error
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[%-5p] [%d{yyyy-MM-dd HH:mm:ss}] [%l] %m%n
log4j.appender.stdout.Threshold = DEBUG
# info
log4j.appender.info=org.apache.log4j.RollingFileAppender
log4j.appender.info.Encoding=utf-8
log4j.appender.info.File=./logs/study-log.log
log4j.appender.info.layout=org.apache.log4j.PatternLayout
log4j.appender.info.layout.ConversionPattern=[%-5p] [%d{yyyy-MM-dd HH:mm:ss}] [%l] %m%n
log4j.appender.info.MaxFileSize=50MB
log4j.appender.info.MaxBackupIndex=20
log4j.appender.info.Threshold = INFO
log4j.appender.info.ImmediateFlush = TRUE
# error
log4j.appender.error=org.apache.log4j.RollingFileAppender
log4j.appender.error.Encoding=utf-8
log4j.appender.error.File=./logs/study-log.error
log4j.appender.error.layout=org.apache.log4j.PatternLayout
log4j.appender.error.layout.ConversionPattern=[%-5p] [%d{yyyy-MM-dd HH:mm:ss}] [%l] %m%n
log4j.appender.error.MaxFileSize=50MB
log4j.appender.error.MaxBackupIndex=20
log4j.appender.error.Threshold = error
log4j.appender.error.ImmediateFlush = TRUE
# other
log4j.logger.com.ljq.study.log=DEBUG
log4j.logger.com.alibaba.nacos.client.naming=error
log4j.logger.org.mybatis=DEBUG
# log4j.logger.java.sql.Connection=DEBUG
# log4j.logger.java.sql.Statement=DEBUG
# log4j.logger.java.sql.PreparedStatement=DEBUG

(6)语法说明

c. logback

(1)配置文件

  • Logback tries to find a file called logback-test.xml in the classpath.
  • If no such file is found, logback tries to find a file called logback.groovy in the classpath.
  • If no such file is found, it checks for the file logback.xml in the classpath..
  • If no such file is found, service-provider loading facility (introduced in JDK 1.6) is used to resolve the implementation of com.qos.logback.classic.spi.Configurator interface by looking up the file META-INF\services\ch.qos.logback.classic.spi.Configurator in the class path. Its contents should specify the fully qualified class name of the desired Configurator implementation.
  • If none of the above succeeds, logback configures itself automatically using the BasicConfigurator which will cause logging output to be directed to the console.

(2)级别

日志打印级别:ALL > TRACE > FATAL > DEBUG > INFO > WARN > ERROR > OFF

(3)处理器 Appender:Console、Rollingfile ...

(4)格式化 Layout:Xml、Pattern、Html 、 自定义 ...

(5)example

<?xml version="1.0" encoding="UTF-8"?>
<!-- 开启自动刷新配置 -->
<configuration scan="true" scanPeriod="3 seconds" debug="false">

  <!-- lOGGER PATTERN 根据个人喜好选择匹配 -->
  <property name="logPattern" value=
    "logback:[ %-5level] [%date{yyyy-MM-dd HH:mm:ss.SSS}] %logger{96} [%line] [%thread]- %msg%n">
  </property>

  <!-- 动态日志级别 -->
  <jmxConfigurator/>

  <!-- 控制台的标准输出 -->
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <charset>UTF-8</charset>
      <pattern>${logPattern}</pattern>
    </encoder>
  </appender>
  <!-- 滚动文件 -->
  <appender name="ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>DEBUG</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>
    <file>./logback.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>./logback.log.%d{yyyy-MM-dd}.zip</fileNamePattern>
      <!-- 最大保存时间 -->
      <maxHistory>2</maxHistory>
    </rollingPolicy>
    <encoder>
      <pattern>${logPattern}</pattern>
    </encoder>
  </appender>
  <!-- 数据库 -->
  <appender name="DB" class="ch.qos.logback.classic.db.DBAppender">
    <connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource">
      <driverClass>com.mysql.jdbc.Driver</driverClass>
      <url>jdbc:mysql://192.168.206.129:3306/log?useSSL=false</url>
      <user>root</user>
      <password>root</password>
    </connectionSource>
  </appender>
  <!-- 异步记录日志 -->
  <appender name="ASYNC_LOG" class="ch.qos.logback.classic.AsyncAppender">
    <!-- 不丢失日志.默认的,如果队列的 80% 已满则会丢弃 TRACT、DEBUG、INFO 级别的文志 -->
    <discardingThreshold>0</discardingThreshold>
    <!-- 更改默认的队列的深度,该值会影响性能.默认值为 256 -->
    <queueSize>3</queueSize>
    <!-- 异步记录ILoggingEvents,它仅充当事件分派器,因此必须引用另一个Appender才能执行任何有用的操作 -->
    <appender-ref ref="STDOUT"/>
  </appender>

  <!-- 日志的记录级别: 在定义后引用 APPENDER -->
  <root level="DEBUG">
    <!-- CONSOLE -->
    <appender-ref ref="STDOUT"/>
    <!-- ROLLING_FILE -->
    <appender-ref ref="ROLLING_FILE"/>
    <!-- ASYNC_LOG -->
    <appender-ref ref="ASYNC_LOG"/>
  </root>
</configuration>

AsyncAppender 参数配置:

d. JCL

(1)配置文件

首先在 classpath 下寻找 commons-logging.properties 文件。如果找到,则使用其中定义的 Log 实现类;如果找不到,则在查找是否已定义系统环境变量 org.apache.commons.logging.Log,找到则使用其定义的 Log 实现类;

查看 classpath 中是否有 Log4j 的包,如果发现,则自动使用 Log4j 作为日志实现类;否则,使用 JDK 自身的日志实现类(JDK1.4 以后才有日志实现类);否则,使用 commons-logging 自己提供的⼀个简单的日志实现类 SimpleLog;

(2)级别

JCL 有 5 个级别:trace < debug < info < warn < error

(3)commons-logging.properties 配置

# 指定日志对象
# org.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog
# org.apache.commons.logging.Log=org.apache.commons.logging.impl.Jdk14Logger
org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger

# 指定日志工厂
org.apache.commons.logging.LogFactory=org.apache.commons.logging.impl.LogFactoryImp

e. Log4j2

(1)配置文件

  • Log4j will inspect the log4j.configuration File system property and, if set, will attempt to load the configuration using the ConfigurationFactory that matches the file extension.
  • If no system property is set the YAML ConfigurationFactory will look for log4j2-test.yaml or log4j2-test.yml in the classpath.
  • If no such file is found the JSON ConfigurationFactory will look for log4j2-test.json or log4j2-test.jsn in the classpath.
  • If no such file is found the XML ConfigurationFactory will look for log4j2-test.xml in the classpath.
  • If a test file cannot be located the YAML ConfigurationFactory will look for log4j2.yaml or log4j2.yml on the classpath.
  • If a YAML file cannot be located the JSON ConfigurationFactory will look for log4j2.json or log4j2.jsn on the classpath.
  • If a JSON file cannot be located the XML ConfigurationFactory will try to locate log4j2.xml on the classpath.
  • If no configuration file could be located the DefaultConfiguration will be used. This will cause logging output to go to the console.

(2)级别

从低到高为:ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF

(3)处理器

处理器 说明
FileAppender 普通地输出到本地文件
KafkaAppender 输出到 Kafka
FlumeAppender 将多个不同源的日志汇集、集中到一处
JMSQueueAppender/JMSTopicAppender 与 JMS 相关的日志输出
RewriteAppender 对日志事件进行掩码或注入信息
RollingFileAppender 对日志文件进行封存(详细)
RoutingAppender 在输出地之间进行筛选路由
SMTPAppender 将 LogEvent 发送到指定邮件列表
SocketAppender 将 LogEvent 以普通格式发送到远程主机
SyslogAppender 将 LogEvent 以 RFC 5424 格式发送到远程主机
AsynchAppender 将一个 LogEvent 异步地写入多个不同输出地
ConsoleAppender 将 LogEvent 输出到命令行
FailoverAppender 维护一个队列,系统将尝试向队列中的 Appender 依次输出 LogEvent,直到有一个成功为止

(4)example

<?xml version="1.0" encoding="UTF-8"?>
<!-- 日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!-- status="WARN": 用于设置 log4j2 自身内部日志的信息输出级别,默认是 OFF -->
<!-- monitorInterval="30": 间隔秒数,自动检测配置文件的变更和重新配置本身 -->
<configuration status="info" monitorInterval="30">
  <Properties>
    <!-- 自定义一些常量,之后使用 ${变量名} 引用 -->
    <Property name="pattern">[%-5p]:%d{YYYY-MM-dd HH:mm:ss} [%t] %c{1}:%L - %msg%n />
  </Properties>
  <!-- Appenders: 定义输出内容/格式/方式,日志保存策略等;常用标签: console,File,RollingFile -->
  <appenders>
    <!-- Console: 控制台输出的配置 -->
    <Console name="Console" target="SYSTEM_OUT">
        <PatternLayout pattern="${pattern}"/>
    </Console>
    <!-- File: 同步输出日志到本地文件 -->
    <!-- append="false":根据其下日志策略,每次清空文件重新输出日志,可用于测试 -->
    <File name="File" fileName="./log4j2-file.log" append="false">
      <PatternLayout pattern="${pattern}"/>
    </File>
    <RollingFile name="RollingFile" fileName="./log4j2-rollingfile.log" 
                 filePattern="./$${date:yyyy-MM}/log4j2-%d{yyyy-MM-dd}-%i.log">
      <!--
        ThresholdFilter    日志输出过滤
          level="info"       日志级别
          onMatch="ACCEPT"   级别在 info 之上则接受
          onMismatch="DENY"  级别在 info 之下则拒绝
      -->
      <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>
      <PatternLayout pattern="${pattern}"/>
      <!-- Policies: 日志滚动策略-->
      <Policies>
        <!--
          TimeBasedTriggeringPolicy   时间滚动策略,默认0点产生新文件
            interval="6"                自定义文件滚动时间间隔,每隔6小时产生新文件
            modulate="true"             产生文件否以0点偏移时间,即6点/12点/18点/0点
        -->
        <TimeBasedTriggeringPolicy interval="6" modulate="true"/>
        <!-- SizeBasedTriggeringPolicy 文件大小滚动策略-->
        <SizeBasedTriggeringPolicy size="1 MB"/>
      </Policies>
      <!-- DefaultRolloverStrategy#max 属性若不设置,则默认为最多同一文件夹下 7 个文件 -->
      <DefaultRolloverStrategy max="20"/>
    </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>
    <!--
      AsyncLogger异步日志(三种日志模式:全异步日志、混合模式、同步日志)
      性能从高到底,线程越多效率越高,也可以避免日志卡死线程情况发生
      additivity="false":additivity 设置事件是否在 root-logger 输出
      为了避免重复输出,可以在 Logger 标签下设置 additivity 为 "false"
    -->
  <AsyncLogger name="AsyncLogger" level="trace" includeLocation="true" additivity="true">
    <appender-ref ref="Console"/>
  </AsyncLogger>
  <logger name="Kafka" additivity="false" level="debug">
    <appender-ref ref="Kafka"/>
    <appender-ref ref="Console"/>
  </logger>
  <!-- Root 节点用来指定项目的根日志,若没有单独指定 Logger,那么就会默认使用该 Root 日志输出 -->
  <root level="info">
    <appender-ref ref="Console"/>
    <!--<appender-ref ref="File"/>-->
    <!--<appender-ref ref="RollingFile"/>-->
    <!--<appender-ref ref="Kafka"/>-->
  </root>
  </loggers>
</configuration>

f. slf4j

(1)配置文件:具体日志输出内容取决于生效的具体日志实现

(2)级别:slf4j 日志级别有五种:ERROR、WARN、INFO、DEBUG、TRACE,级别从高到低

(3)多个桥接器的话,slf4j 怎么处理呢? 使用在 ClassPath 中较早出现的那个,如在 Maven 中,会使用在 pom.xml 中定义较靠前的桥接器

(4)slf 转其他日志

<dependencies>
    <!-- slf - jul -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-jdk14</artifactId>
        <version>1.7.30</version>
    </dependency>

    <!-- slf - log4j -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.30</version>
    </dependency>

    <!-- slf - log4j2 -->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <version>2.13.0</version>
    </dependency>

    <!-- slf - jul -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-jdk14</artifactId>
        <version>1.7.30</version>
    </dependency>

    <!-- slf - simplelog -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>1.7.30</version>
    </dependency>

    <!-- slf - logback -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-core</artifactId>
        <version>1.2.3</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
    </dependency>

    <!-- slf - jcl -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-jcl</artifactId>
        <version>1.7.30</version>
    </dependency>
</dependencies>

(5)桥接器会传递依赖到对应的下游日志组件:比如 slf4j-log4j12 会附带 log4j 的 jar 包依赖

(6)其他日志桥接到 slf4j

<!-- jul - slf -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jul-to-slf4j</artifactId>
    <version>1.7.30</version>
</dependency>
<!-- jcl - slf -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>1.7.30</version>
</dependency>
<!-- log4j - slf -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.30</version>
</dependency>
<!-- log4j2 - slf -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-to-slf4j</artifactId>
    <version>2.13.0</version>
</dependency>

(7)「日志环」演示: slf4j - log4j - slf4j

<!-- slf4j -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.30</version>
</dependency>
<!-- slf - log4j -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.30</version>
    <exclusions>
        <exclusion>
            <groupId>*</groupId>
            <artifactId>*</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!-- log4j -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>apache-log4j-extras</artifactId>
    <version>1.2.17</version>
</dependency>
<!-- log4j - slf -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.30</version>
</dependency>

报错结果(使用 slf 桥接和绑定的时候要留心,不要自相矛盾形成环):

4. 日志建议

a. 门面约束

使用门面,而不是具体实现。

使用 Log Facade 可以方便地切换具体的日志实现。而且,如果依赖多个项目,使用了不同的 Log Facade,还可以方便地通过 Adapter 转接到同⼀个实现上。如果依赖项目直接使用了多个不同的日志实现,会非常糟糕。

Log Facade,现在⼀般推荐使用 Log4j-API 或者 SLF4j,不推荐继续使用 JCL。

b. 单一原则

只添加⼀个日志实现。

项目中应该只使用一个具体的 Log Implementation,如果在依赖的项目中,使用的 Log Facade 不支持当前 Log Implementation,就添加合适的桥接器。

JUL 性能⼀般,Log4j 性能也有问题而且不再维护,建议使用 Logback 或者 Log4j2。

c. 依赖约束

日志实现坐标应该设置为 optional 并使用 Runtime Scope。

在项目中,Log Implementation 的依赖强烈建议设置为 Runtime Scope,并且设置为 optional。例如项目中使用了 SLF4J 作为 Log Facade,然后想使用 Log4j2 作为 Implementation,那么使用 Maven 添加依赖的时候这样设置:

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>${log4j.version}</version>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>
<dependency>
     <groupId>org.apache.logging.log4j</groupId>
     <artifactId>log4j-slf4j-impl</artifactId>
     <version>${log4j.version}</version>
     <scope>runtime</scope>
     <optional>true</optional>
</dependency>
  • 设为 optional,依赖不会传递,假设你是个 lib 项目,然后别的项目使用了你这个 lib,不会被引入不想要的 Log Implementation 依赖;
  • scope 设置为 Runtime,是为了防止开发人员在项目中直接使用 Log Implementation 中的类,强制约束开发人员使用 Facade 接口。

d. 避免传递

尽量用 exclusion 排除依赖的第三方库中的日志坐标。

同上一个话题,第三方库的开发者却未必会把具体的日志实现或者桥接器的依赖设置为 optional,然后你的项目就会被迫传递引入这些依赖,而这些日志实现未必是你想要的,比如他依赖了Log4j,你想使用 Logback,这时就很尴尬。另外,如果不同的第三方依赖使用了不同的桥接器和 Log 实现,极有可能会形成环。

这种情况下推荐的处理方法,是使用 exclude 来排除所有的这些 Log 实现和桥接器的依赖,只保留第三方库里面对 Log Facade 的依赖。

依赖 jstorm 会引入 Logback 和 log4j-over-slf4j,如果你在自己的项目中使用 Log4j 或其他 Log 实现的话,就需要加上 exclusion:

<dependency>
    <groupId>com.alibaba.jstorm</groupId>
    <artifactId>jstorm-core</artifactId>
    <version>2.1.1</version>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>log4j-over-slf4j</artifactId>
        </exclusion>
        <exclusion>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
        </exclusion>
    </exclusions>
</dependency>

e. 注意写法

避免为不会输出的 log 买单:log 库都可以灵活的设置输出级别,所以每⼀条程序中的 log,都是有可能不会被输出的。这时候要注意不要额外的付出代价。实例如下:

logger.debug("this is debug: " + message);
logger.debug("this is json msg: {}", toJson(message));

前面讲到,第 1 条的字符串拼接,即使日志级别高于 debug 不会打印,依然会做字符串连接操作;第 2 条虽然用了 SLF4J/Log4j2 中的懒求值方式,但是 toJson() 这个方法却是总会被调用并且开销更大。

推荐的写法如下:

// SLF4J/LOG4J2
logger.debug("this is debug:{}", message);
// LOG4J2
logger.debug("this is json msg: {}", () -> toJson(message));
// SLF4J/LOG4J2
if (logger.isDebugEnabled()) {
    logger.debug("this is debug: " + message);
}

f. 减少分析

输出的日志中尽量不要使用行号、函数名等信息。

原因是,为了获取语句所在的函数名或者行号,log 库的实现都是获取当前的 stacktrace,然后分析取出这些信息,而获取 stacktrace 的代价是很昂贵的。如果有很多的日志输出,就会占用大量的 CPU。在没有特殊需要的情况下,建议不要在日志中输出这些这些字段。

g. 精简至上

log 中尽量不要输出稀奇古怪的字符,这是个习惯和约束问题。有的人习惯用这种语句:

logger.debug("=====================================: {}", message);

输出了大量无关字符,虽然自己⼀时痛快,但是如果所有⼈都这样做的话,那 log 输出就没法看了!正确的做法是日志只输出必要信息,如果要过滤,后期使用 grep 来筛选,只查自己关系的日志。

5. 日志中间件

日志的生命周期一般分为采集、传输、存储、分析四个环节,每个步骤有可用的中间件和工具。

选用中间件时所关注的角度:性能、可靠性、插件支持程度、配置复杂度。

企业实战中,ELK 是成熟且广泛使用的方案;Logstash 因为性能弱于 Filebeat,并不直接运用于采集起点,一般使用 Filebeat。

进入 ELK 前,从经验性角度来说应放置 Kafka,一方面作为队列和缓冲,另一方面提供了统一的入口渠道。

*.log -> FileBeat -> Kafka -> Logstash -> Es -> Kibana

6. 日志查看命令

posted @ 2020-09-25 17:33  tree6x7  阅读(135)  评论(0编辑  收藏  举报