Java 日志框架

1,422 阅读20分钟

什么是日志


日志的作用就是在测试、生产环境没有 Debug 调试工具时开发、测试人员定位问题的手段。日志打得好,就能根据日志的轨迹快速定位并解决线上问题,反之,日志输出不好不能定位到问题不说反而会影响系统的性能。 优秀的项目都是能根据日志定位问题的,而不是在线调试,或者半天找不到有用的日志而抓狂…

发展史


image.png

log4j: 1996年早期,欧洲安全电子市场项目组E.U.SEMPER需要一个(Tracing API)来准中程序。经过不断的完善,这个API终于成为一个十分受欢迎的Java日志软件包,即Log4j,并被转换成C/C++/C#/Perl/Python等不同的语言版本,后来Log4j成为Apache基金会项目中的一员,期间Log4j近乎成了Java社区的日志标准。据说Apache基金会还曾经建议sun引入Log4j到java的标准库中,但Sun拒绝了。

JUL: 原来Sun也有自己的盘算,不就一个日志嘛,我自己也搞一个,2002年2月JDK1.4发布,Sun推出了自己的日志标准库JUL(Java Util Logging),其实是照着Log4j抄的,而且还没抄好,还是在JDK1.5以后性能和可用性才有所提升

JCL: 同年,Apache推出了Jakarta Commons Logging,Jakarta在这里指的是一个组织,而不是印度的首都雅加达,Jakarta,一个早期的Apache开源项目,用于管理各个Java子项目,诸如Tomcat, Ant, Maven, Struts, JMeter, Velocity, JMeter, Commons等。2011年12月,在所有子项目都被迁移为独立项目后,Jakarta名称就不再使用了。JCL诞生的初衷是因为Java自身的一些包用了JUL,而Log4j用户使用的有很多,那么JCL就是提供一套API来实现不同Logger之间的切换。JCL只是定义了一套日志接口(其内部也提供一个Simple Log的简单实现),支持运行时动态加载日志组件的实现,也就是说,在你应用代码里,只需调用Commons Logging的接口,底层实现可以是Log4j,也可以是Java Util Logging。最后更新时间是2014.7.9

SLF4J: 2006年,Ceki Gülcü(log4j1.x的作者)不适应Apache的工作方式,离开了Apache。然后先后创建了SLF4J(日志门面接口,类似于Commons Logging)和Logback(SLF4J的实现)两个项目,并回瑞典创建了QOS公司,SLF4J(Simple Logging Facade For Java)简单日志门面,和JCL功能类似,但JCL有一个致命的缺点就是算法复杂,出现问题难以排除,而SLF4J的诞生就是为了解决JCL的缺点。QOS官网上是这样描述Logback的:The Generic,Reliable Fast&Flexible Logging Framework(一个通用,可靠,快速且灵活的日志框架)。

Logback: Ceki巨佬觉得市场上的日志标准库都是间接实现Slf4j接口,也就是说每次都需要配合桥接包,因此在2006年,Ceki巨佬基于Slf4j接口撸出了Logback日志标准库,做为Slf4j接口的默认实现,Logback也十分给力,在功能完整度和性能上超越了所有已有的日志标准库。

Log4j2: Apache在2012年重写了log4j1.x,并成立了log4j2项目,2014年发布了log4j2的通用版本,Log4j2有着和Logback相同的功能,但又有自己单用的功能,比如:插件式结构、配置文件优化、异步日志等。Log4j2是Log4j的升级,它比其前身Log4j 1.x提供了重大改进,并提供了Logback中可用的许多改进,同时修复了Logback架构中的一些固有问题。

常用日志框架


System.out和System.err

2001年以前,Java是没有日志库的,打印日志全凭System.out和System.err,我人都傻了,十分离谱

image.png

缺点下:

  • 产生大量的IO操作 同时在生产环境中 无法合理的控制是否需要输出
  • 输出的内容不能保存到文件
  • 只打印在控制台,打印完就过去了,也就是说除非你一直盯着程序跑
  • 无法定制化,且日志粒度不够细

Log4j

背景

此时名为Ceki的巨佬站出来,说你这个不好用,我这个好用,接着在2001年掏出了Log4j,用起来也确实比System系列香,Log4j一度成为业内日志标准

image.png

介绍

Log4j是Apache下的一款开源的日志框架,通过在项目中使用 Log4J,我们可以控制日志信息输出到控 制台、文件、甚至是数据库中。我们可以控制每一条日志的输出格式,通过定义日志的输出级别,可以 更灵活的控制日志的输出过程。方便项目的调试

官方网站: logging.apache.org/log4j/1.2/

使用

入门实验

  1. 添加依赖
<dependencies>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

2.代码测试

public class Log4jTest {
    @Test
    public void testQuick() throws Exception { // 初始化系统配置,不需要配置文件数据
        // 初始化系统配置,不需要配置文件数等
        BasicConfigurator.configure();
        // 创建日志记录器对象
        Logger logger = Logger.getLogger(Log4jTest.class);
        // 日志记录输出
        logger.info("hello log4j");
        
        // 日志级别
        logger.fatal("fatal"); // 严重错误,一般会造成系统崩溃和终止运行
        logger.error("error"); // 错误信息,但不会影响系统运行
        logger.warn("warn"); // 警告信息,可能会发生问题
        logger.info("info"); // 程序运行信息,数据库的连接、网络、IO操作等
        logger.debug("debug");// 调试信息,一般在开发阶段使用,记录程序的变量、参
        logger.trace("trace");// 追踪信息,记录程序的所有流程信息
    } 
}

日志级别

  • 每个Logger都被了一个日志级别(log level),用来控制日志信息的输出。日志级别从高到低分 为:
 fatal 指出每个严重的错误事件将会导致应用程序的退出
 error 指出虽然发生错误事件,但仍然不影响系统的继续运行。
 warn 表明会出现潜在的错误情形。
 info 一般和在粗粒度级别上,强调应用程序的运行全程。
 debug 一般用于细粒度级别上,对调试应用程序非常有帮助。
 trace 是程序追踪,可以用于输出程序运行中的变量,显示执行的流程。
  • 还有两个特殊的级别:
 OFF,可用来关闭日志记录。
 ALL,启用所有消息的日志记录。

注:一般只使用4个级别,优先级从高到低为 ERROR > WARN > INFO > DEBUG

Log4j组件

Log4J 主要由 Loggers (日志记录器)、Appenders(输出端)和 Layout(日志格式化器)组成。其中 Loggers 控制日志的输出级别与日志是否输出;Appenders 指定日志的输出方式(输出到控制台、文件 等);Layout 控制日志信息的输出格式

image.png

Loggers (日志记录器)

日志记录器,负责收集处理日志记录,实例的命名就是类“XX”的full quailied name(类的全限定名), Logger的名字大小写敏感,其命名有继承机制:例如:name为org.apache.commons的logger会继承 name为org.apache的logger

Log4J中有一个特殊的logger叫做“root”,他是所有logger的根,也就意味着其他所有的logger都会直接 或者间接地继承自root。root logger可以用Logger.getRootLogger()方法获取

配置根Logger,其语法为:

log4j.rootLogger = [ level ] , appenderName, appenderName, ...

例子:

log4j.rootLogger=info, stdout, log, errorlog
log4j.Logger=search,Test

Appenders(输出端)

Appender 用来指定日志输出到哪个地方,可以同时指定日志的输出目的地。Log4j 常用的输出目的地 有以下几种:

输出端类型作用
ConsoleAppender将日志输出到控制台
FileAppender将日志输出到文件中
DailyRollingFileAppender将日志输出到一个日志文件,并且每天输出到一个新的文件
RollingFileAppender将日志信息输出到一个日志文件,并且指定文件的尺寸,当文件大小达到指定尺寸时,会自动把文件改名,同时产生一个新的文件
JDBCAppender把日志信息保存到数据库中

配置日志信息输出目的地Appender,其语法为:

log4j.appender.appenderName = fully.qualified.name.of.appender.class
log4j.appender.appenderName.option1 = value1
log4j.appender.appenderName.option = valueN

例子:

log4j.rootLogger=info, stdout, log, errorlog
log4j.Logger=search,Test
###Console ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern =  %d{ABSOLUTE} [ %t ] [ %p ]:%L - %m%n
### Log ### 
log4j.appender.log = org.apache.log4j.DailyRollingFileAppender
log4j.appender.log.File = log/log.log
log4j.appender.log.Append = true
log4j.appender.log.Threshold = INFO
log4j.appender.log.DatePattern='.'yyyy-MM-dd
log4j.appender.log.layout = org.apache.log4j.PatternLayout
log4j.appender.log.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t ] %m%n

详细配置可参考:wiki.jikexueyuan.com/project/log…

Layout(日志格式化器)

布局器 Layouts用于控制日志输出内容的格式,让我们可以使用各种需要的格式输出日志。Log4j常用的Layouts

格式化器类型作用
HTMLLayout格式化日志输出为HTML表格形式
SimpleLayout简单的日志输出格式化,打印的日志格式为(info - message)
PatternLayout最强大的格式化期,可以根据自定义格式输出日志,如果没有指定转换格式,就是用默认的转换格式

在 log4j.properties 配置文件中,我们定义了日志输出级别与输出端,在输出端中分别配置日志的输出 格式:

* log4j 采用类似 C 语言的 printf 函数的打印格式格式化日志信息,具体的占位符及其含义如下:
        %m 输出代码中指定的日志信息
        %p 输出优先级,及 DEBUG、INFO 等
        %n 换行符(Windows平台的换行符为 "\n",Unix 平台为 "\n")
        %r 输出自应用启动到输出该 log 信息耗费的毫秒数
        %c 输出打印语句所属的类的全名
        %t 输出产生该日志的线程全名
        %d 输出服务器当前时间,默认为 ISO8601,也可以指定格式,如:%d{yyyy年MM月dd日
        HH:mm:ss}
        %l 输出日志时间发生的位置,包括类名、线程、及在代码中的行数。如:
        Test.main(Test.java:10)
        %F 输出日志消息产生时所在的文件名称
        %L 输出代码中的行号 %% 输出一个 "%" 字符
* 可以在 % 与字符之间加上修饰符来控制最小宽度、最大宽度和文本的对其方式。如:
        %5c 输出category名称,最小宽度是5,category<5,默认的情况下右对齐
        %-5c 输出category名称,最小宽度是5,category<5,"-"号指定左对齐,会有空格
        %.5c 输出category名称,最大宽度是5,category>5,就会将左边多出的字符截掉,<5不
        会有空格
        %20.30c category名称<20补空格,并且右对齐,>30字符,就从左边交远销出的字符截掉

自定义Logger

# RootLogger配置
log4j.rootLogger = trace,console
# 自定义Logger com.lm 包下的logger类型
log4j.logger.com.lm = info,file 
log4j.logger.org.lm = error
@Test
public void testCustomLogger() throws Exception {
    // 自定义 com.lm
    Logger logger1 = Logger.getLogger(Log4jTest.class);
    logger1.info("info");
    
    // 自定义 org.lm
    Logger logger2 = Logger.getLogger(Logger.class);
}

JUL(java util logging)

背景

JUL全称Java util Logging是java原生的日志框架,使用时不需要另外引用第三方类库,相对其他日志框架使用方便,学习简单,能够在小型应用中灵活使用

介绍

image.png

  • Loggers:被称为记录器,应用程序通过获取Logger对象,调用其API来来发布日志信息。Logger 通常时应用程序访问日志系统的入口程序。
  • Appenders:也被称为Handlers,每个Logger都会关联一组Handlers,Logger会将日志交给关联 Handlers处理,由Handlers负责将日志做记录。Handlers在此是一个抽象,其具体的实现决定了 日志记录的位置可以是控制台、文件、网络上的其他日志服务或操作系统日志等。
  • Layouts:也被称为Formatters,它负责对日志事件中的数据进行转换和格式化。Layouts决定了 数据在一条日志记录中的最终形式。
  • Level:每条日志消息都有一个关联的日志级别。该级别粗略指导了日志消息的重要性和紧迫,可以将Level和Loggers,Appenders做关联以便于我们过滤消息。
  • Filters:过滤器,根据需要定制哪些信息会被记录,哪些信息会被放过。

总结一下:

用户使用Logger来进行日志记录,Logger持有若干个Handler,日志的输出操作是由Handler完成的。 在Handler在输出日志前,会经过Filter的过滤,判断哪些日志级别过滤放行哪些拦截,Handler会将日 志内容输出到指定位置(日志文件、控制台等)。Handler在输出日志时会使用Layout,将输出内容进 行排版

使用

入门实验

不需要特殊引入什么jar,jdk 原生支持

public class JULTest {
    //快速入门
    @Test
    public void testQuick(){
        //1.获取日志记录器对象
        Logger logger = Logger.getLogger("com.lm.JULTest");
        //2.日志记录输出
        logger.info("hello jul");

        //通用方法进行日志记录
        logger.log(Level.INFO,"info msg");

        //通过占位符方式输出变量值
        String name = "lm";
        Integer age = 13;
        logger.log(Level.INFO,"用户信息:{0},{1}",new Object[]{name,age});
    }
}

日志级别

java.util.logging.Level中定义了日志的级别:
        SEVERE(最高值)
        WARNING
        INFO (默认级别)
        CONFIG
        FINE
        FINER
        FINEST(最低值)
还有两个特殊的级别:
        OFF,可用来关闭日志记录。
        ALL,启用所有消息的日志记录。

虽然我们测试了7个日志级别但是默认只实现info以上的级别

@Test
public void testLogLevel(){
    //1.获取日志对象
    Logger logger = Logger.getLogger("com.com.lm.JULTest");
    //2.日志记录输出
    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
}
结果:
severe
warning
info

自定义日志级别控制

@Test
public void testLogConfig() throws IOException {
    // 1.获取日志记录器对象
    Logger logger = Logger.getLogger("com.com.lm.JULTest");

    // 关闭系统默认配置
    logger.setUseParentHandlers(false);

    // 自定义配置日志级别
    // 创建ConsolHandler
    ConsoleHandler consoleHandler = new ConsoleHandler();

    // 创建简单格式转换对象
    SimpleFormatter simpleFormatter = new SimpleFormatter();

    // 进行关联
    consoleHandler.setFormatter(simpleFormatter);
    logger.addHandler(consoleHandler);

    // 设置日志具体级别
    logger.setLevel(Level.ALL);
    consoleHandler.setLevel(Level.ALL);

    // 场景FileHandler 文件输出
    FileHandler fileHandler = new FileHandler("F:/logs/jul.log");

    // 进行关联
    fileHandler.setFormatter(simpleFormatter);
    logger.addHandler(fileHandler);

    // 2.日志记录输出
    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
}
结果:
所有日志都打印

Logger之间的父子关系

JUL中Logger之间存在父子关系,这种父子关系通过树状结构存储,JUL在初始化时会创建一个顶层 RootLogger作为所有Logger父Logger,存储上作为树状结构的根节点。并父子关系通过路径来关联

// Logger对象父子关系
@Test
public void testLogParent() throws Exception{

    Logger logger1 = Logger.getLogger("com.lm.App");
    Logger logger2 = Logger.getLogger("com");

    // 测试
    System.out.println(logger1.getParent() == logger2);
    // 所有日志记录器的顶级父元素 LogManager$RootLogger,name ""
    System.out.println("logger2 Parent:"+logger2.getParent() + ",name:"+ logger2.getParent().getName());

    // 关闭系统默认配置
    logger2.setUseParentHandlers(false);

    // 设置loggger2的默认级别
    // 创建ConsolHandler对象
    ConsoleHandler consoleHandler = new ConsoleHandler();

    // 创建简单格式转换对象
    SimpleFormatter simpleFormatter = new SimpleFormatter();

    // 进行关联
    consoleHandler.setFormatter(simpleFormatter);
    logger2.addHandler(consoleHandler);

    // 设置日志具体级别
    logger2.setLevel(Level.ALL);
    consoleHandler.setLevel(Level.ALL);

    logger1.severe("severe");
    logger1.warning("warning");
    logger1.info("info");
    logger1.config("config");
    logger1.fine("fine");
    logger1.finer("finer");
    logger1.finest("finest");
}

结果:logger1继承了logger2的配置

true
logger2 Parent:java.util.logging.LogManager$RootLogger@6f94fa3e,name:
五月 24, 2021 12:36:51 下午 com.lm.App main
严重: severe
五月 24, 2021 12:36:51 下午 com.lm.App main
警告: warning
五月 24, 2021 12:36:51 下午 com.lm.App main
信息: info
五月 24, 2021 12:36:51 下午 com.lm.App main
配置: config
五月 24, 2021 12:36:51 下午 com.lm.App main
详细: fine
五月 24, 2021 12:36:51 下午 com.lm.App main
较详细: finer
五月 24, 2021 12:36:51 下午 com.lm.App main
非常详细: finest

日志的配置文件

默认配置文件路径$JAVAHOME\jre\lib\logging.properties

@Test
public void testProperties() throws IOException {
    // 读取自定义配置文件
    InputStream in = JULDemo01.class.getClassLoader().getResourceAsStream("logging.properties");
    // 获取日志管理器对象
    LogManager logManager = LogManager.getLogManager();
    // 通过日志管理器加载配置文件
    logManager.readConfiguration(in);

    Logger logger = Logger.getLogger("com.lm.JULDemo01");

    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
}

配置文件:

# RootLogger 顶级父元素指定的默认处理器为:ConsoleHandler
handlers= java.util.logging.ConsoleHandler
# RootLogger 顶级父元素默认的日志级别为ALL
.level= INFO

## 自定义Logger
com.itheima.handlers = java.util.logging.FileHandler
# 自定义Logger日志等级
com.itheima.level = INFO
# 忽略父日志设置
com.itheima.useParentHandlers = false

# 向控制台输出 handler 对象
# 指定 handler 对象的日志级别
java.util.logging.ConsoleHandler.level = INFO
# 指定 handler 对象的日志消息格式对象
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# 指定 handler 对象的字符集
java.util.logging.ConsoleHandler.encoding = UTF-8


## 文件处理器
# 输出日志级别
java.util.logging.FileHandler.level=INFO
# 向日志文件输出的 handler 对象
# 指定日志文件路径 F:/logs/java%u.log
java.util.logging.FileHandler.pattern = F:/logs/java%u.log
# 指定日志文件内容大小(50000字节)
java.util.logging.FileHandler.limit = 50000
# 指定日志文件数量
java.util.logging.FileHandler.count = 1
# 指定 hanlder 对象日志消息格式对象
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# 指定以追加方式添加日志内容
java.util.logging.FileHandler.append=true

# 指定日志消息格式
java.util.logging.SimpleFormatter.format = %1$tc %2$s%n%4$s: %5$s%6$s%n

日志原理解析

  1. 初始化LogManager
    1. LogManager加载logging.properties配置
    2. 添加Logger到LogManager
  2. 从单例LogManager获取Logger
  3. 设置级别Level,并指定日志记录LogRecord
  4. Filter提供了日志级别之外更细粒度的控制
  5. Handler是用来处理日志输出位置
  6. Formatter是用来格式化LogRecord的

JCL(Jakarta Commons Logging)

介绍

Jakarta Commons Logging (JCL)提供的是一个日志(Log)接口(interface),同时兼顾轻量级和不依赖于具体的日志实现工具。 它提供给中间件/日志工具开发者一个简单的日志操作抽象,允许程序开发人员使用不同的具体日志实现工具。用户被假定已熟悉某种日志实现工具的更高级别的细节。JCL提供的接口,对其它一些日志工具,包括Log4J, Avalon LogKit, and JDK 1.4等,进行了简单的包装,此接口更接近于Log4J和LogKit的实现

image.png

于是JUL刚出来不久,2002年8月Apache推出了JCL(Jakarta Commons Logging),也就是日志抽象层,支持运行时动态加载日志组件的实现,当然也提供一个默认实现Simple Log(在ClassLoader中进行查找,如果能找到Log4j则默认使用log4j实现,如果没有则使用JUL 实现,再没有则使用JCL内部提供的Simple Log实现)

image.png

为什么要使用日志门面?

  1. 面向接口开发,不再依赖具体的实现类。减少代码的耦合
  2. 项目通过导入不同的日志实现类,可以灵活的切换日志框架
  3. 统一API,方便开发者学习和使用
  4. 统一配置便于项目日志的管理

使用

入门实验

1、增加commons-logging依赖

<!--引入common-logging-->
<dependency>
   <groupId>commons-logging</groupId>
   <artifactId>commons-logging</artifactId>
   <version>1.2</version>
</dependency>

2、使用commons-logging得LogFactory获取日志实例(使用JUL)

public class App {
    public static void main(String[] args) {
        Log logger1 = LogFactory.getLog(App.class);
        logger1.info("lm.test");
    }
}
结果:
五月 24, 2021 1:05:34 下午 com.lm.App main
信息: lm.test

3、增加log4j 依赖

<!-- log4j -->
<dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  <version>1.2.17</version>
</dependency>

4、使用log4j 打印(日志样式可以通过log4j.properties改变)

public class App {
    public static void main(String[] args) {
        Log logger1 = LogFactory.getLog(App.class);
        logger1.info("lm.test");
    }
}
结果:
log4j:WARN No appenders could be found for logger (com.lm.App).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.

原理分析

  1. 通过LogFactory动态加载Log实现类 image.png
  2. 日志门面支持的日志实现数组
private static final String[] classesToDiscover =
    new String[]{"org.apache.commons.logging.impl.Log4JLogger",
                 "org.apache.commons.logging.impl.Jdk14Logger",
                 "org.apache.commons.logging.impl.Jdk13LumberjackLogger",
                 "org.apache.commons.logging.impl.SimpleLog"};
  1. 获取具体的日志实现
for(int i = 0; i < classesToDiscover.length && result == null; ++i) {
    result = this.createLogFromClass(classesToDiscover[i], logCategory,true); 
}
 

SLF4J( Simple Logging Facade for Java)

背景

SLF4J: 2006年,Ceki Gülcü(log4j1.x的作者)不适应Apache的工作方式,离开了Apache。然后先后创建了SLF4J(日志门面接口,类似于Commons Logging)和Logback(SLF4J的实现)两个项目,并回瑞典创建了QOS公司,SLF4J(Simple Logging Facade For Java)简单日志门面,和JCL功能类似,但JCL有一个致命的缺点就是算法复杂,出现问题难以排除,而SLF4J的诞生就是为了解决JCL的缺点。QOS官网上是这样描述Logback的:The Generic,Reliable Fast&Flexible Logging Framework(一个通用,可靠,快速且灵活的日志框架)

官方网站: www.slf4j.org/

image.png

由于Slf4j出来的较晚,光有一个接口,没有实现的日志库也很蛋疼,如JUL和Log4j都是没有实现Slf4j,就算开发者想用Slf4j也用不了

image.png 这时候巨佬Ceki发话了,Sum和Apache这两个组织不来实现我的接口没关系,我来实现就好了,只有魔法才能打败魔法。后面巨佬Ceki提供了一系列的桥接包来帮助Slf4j接口与其他日志库建立关系,这种方式称为桥接设计模式。

image.png

image.png

介绍

SLF4J(The Simple Logging Facade for Java) 是一个用于日志系统的简单Facade,允许最终用户在部署其应用时使用其所希望的日志框架,如log4j/logback/log4j2等

JCL与SLF4J实现机制对比 image.png

使用

简单入门

1、maven添加依赖

<!--slf4j core 使用slf4j必須添加-->
 <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.28</version>
</dependency>
<!--slf4j 自带的简单日志实现、如果有其他log实现可以不用 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.28</version>
</dependency>

2、测试

public class App {
    public final static Logger LOGGER = LoggerFactory.getLogger(App.class);
    public static void main(String[] args) {
        // 声明日志对象
        LOGGER.info("lm.test");
        LOGGER.warn("lm.test");
    }
}
结果:
[main] INFO com.lm.App - lm.test
[main] WARN com.lm.App - lm.test

slf4j+log4j 使用

1、增加依赖

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.7.28</version>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>1.7.28</version>
</dependency>
<dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  <version>1.2.17</version>
</dependency>

2、测试

public class App {
    public final static Logger LOGGER = LoggerFactory.getLogger(App.class);
    public static void main(String[] args) {
        // 初始化系统配置,不需要配置文件数等
        BasicConfigurator.configure();
        // 声明日志对象
        LOGGER.info("lm.test");
        LOGGER.warn("lm.test");
    }
}
结果:
1 [main] INFO com.lm.App  - lm.test
3 [main] WARN com.lm.App  - lm.test

SLF4J 日志绑定

image.png

门面框架与日志框架进行集成的时,需要通过桥接包进行适配--注意只能存在一个log实现方案

使用slf4j的日志绑定流程

1. 添加slf4j-api的依赖
2. 使用slf4j的API在项目中进行统一的日志记录 
3. 绑定具体的日志实现框架
		1. 绑定已经实现了slf4j的日志框架,直接添加对应依赖
		2. 绑定没有实现slf4j的日志框架,先添加日志的适配器,再添加实现类的依赖
4. slf4j有且仅有一个日志实现框架的绑定(如果出现多个默认使用第一个依赖日志实现)

集成方案

image.png

image.png

桥接旧的日志框架(Bridging)

那如果一个项目中使用了多种具体的日志框架,想要统一是配到slf4j,slf4j再使用一个日志框架进行输出.如之前使用的log4j,现在想使用slf4j 做门面,又不像改api的import代码引入时

image.png

image.png

绑定的适配jar 不能与桥接久框架的适配jar 同时存在,不然可能死循环.如 jcl-over-slf4j.jar和 slf4j-jcl.jar不能同时部署、log4j-over-slf4j.jar和slf4j-log4j12.jar不能同时出现


Logback

背景

Ceki巨佬觉得市场上的日志标准库都是间接实现Slf4j接口,也就是说每次都需要配合桥接包,因此在2006年,Ceki巨佬基于Slf4j接口撸出了Logback日志标准库,做为Slf4j接口的默认实现,Logback也十分给力,在功能完整度和性能上超越了所有已有的日志标准库

目前Java日志体系关系图如下: image.png

介绍

Slf4j是The Simple Logging Facade for Java的简称,是一个简单日志门面抽象框架,它本身只提供了日志Facade API和一个简单的日志类实现,一般常配合Log4j,LogBack,java.util.logging使用。Slf4j作为应用层的Log接入时,程序可以根据实际应用场景动态调整底层的日志实现框架(Log4j/LogBack/JdkLog…)。LogBack和Log4j都是开源日记工具库,LogBack是Log4j的改良版本,比Log4j拥有更多的特性,同时也带来很大性能提升。

LogBack被分为3个组件,logback-core, logback-classic 和 logback-access。

logback-core提供了LogBack的核心功能,是另外两个组件的基础。
logback-classic则实现了Slf4j的API,所以当想配合Slf4j使用时,需要将logback-classic加入classpath。
logback-access是为了集成Servlet环境而准备的,可提供HTTP-access的日志接口。

logback配置

logback 配置文件执行顺序

(1) 尝试在classpath下查找文件logback-test.xml
(2) 如果文件不存在,则查找文件logback.xml
(3) 如果两个文件都不存在,则用BasicConfigurator自动对自己进行配置,把记录输出到控制台

配置文件引入方式

rpc服务:classpath下有配置文件就行

web服务:

    <!--logback-->
    <context-param>
        <param-name>logbackConfigLocation</param-name>
        <param-value>/WEB-INF/conf/logback.xml</param-value>
    </context-param>

    <listener>
        <listener-class>ch.qos.logback.ext.spring.web.LogbackConfigListener</listener-class>
    </listener>

logback配置结构

image.png

简单例子:

<configuration scan="true" scanPeriod="60 seconds" debug="false">  
    <property name="glmapper-name" value="glmapper-demo" /> 
    <contextName>${glmapper-name}</contextName> 
    
    
    <appender>
        //xxxx
    </appender>   
    
    <logger>
        //xxxx
    </logger>
    
    <root>             
       //xxxx
    </root>  
</configuration>  

根节点<configuration>包含的属性

scan

当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。

scanPeriod

设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。

debug

当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false

<configuration scan="true" scanPeriod="60 seconds" debug="false">  
      <!-- 其他配置省略-->  
</configuration>  

设置变量 <property>

property

<property>用来定义变量值的标签,<property> 有两个属性,name和value;其中name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。

<configuration scan="true" scanPeriod="60 seconds" debug="false">  
      <property name="APP_Name" value="myAppName" />   
      <contextName>${APP_Name}</contextName>  
      <!-- 其他配置省略-->  
</configuration>  

获取时间戳字符串<timestamp>

timestamp

两个属性 key:标识此 <timestamp> 的名字;datePattern:设置将当前时间(解析配置文件的时间)转换为字符串的模式,遵循java.txt.SimpleDateFormat的格式

<configuration scan="true" scanPeriod="60 seconds" debug="false">  
      <timestamp key="bySecond" datePattern="yyyyMMdd'T'HHmmss"/>   
      <contextName>${bySecond}</contextName>  
      <!-- 其他配置省略-->  
</configuration>   

配置logger

<logger>用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender>。<logger>仅有一个name属性,一个可选的level和一个可选的addtivity属性

name:

用来指定受此loger约束的某一个包或者具体的某一个类

level:

用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。 如果未设置此属性,那么当前loger将会继承上级的级别。

addtivity:

是否向上级logger传递打印信息。默认是true

<appender-ref>

<logger>可以包含零个或多个<appender-ref>元素,标识这个appender将会添加到这个logger

例子:

    <appender name="watcherScribe" class="com.scribe.logback.AsyncScribeAppender">
        <!-- 这是scribe的发送flume的配置 -->
        <sizeOfInMemoryStoreForward>2000</sizeOfInMemoryStoreForward>
    </appender>
    <logger name="metrics_banma_watcher" additivity="false">
        <level value="INFO"/>
        <appender-ref ref="watcherScribe"/>
    </logger>

配置root

也是元素,但是它是根logger。只有一个level属性,应为已经被命名为"root"

<root>可以包含零个或多个<appender-ref>元素,标识这个appender将会添加到这个loger

    <!-- Root Logger -->
    <root>
        <level value="INFO" />
        <appender-ref ref="banmaLogModule" />
        <appender-ref ref="errorlog" />
        <appender-ref ref="banmaErrorLogModule" />
    </root>

appender 配置

image.png

负责写日志的组件、有两个必要属性name和class。name指定appender名称,class指定appender的全限定名

例子:

<appender name="GLMAPPER-LOGGERONE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <append>true</append>
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
        <level>${logging.level}</level>
    </filter>
    <file>
        ${logging.path}/glmapper-spring-boot/glmapper-loggerone.log
    </file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <FileNamePattern>${logging.path}/glmapper-spring-boot/glmapper-loggerone.log.%d{yyyy-MM-dd}</FileNamePattern>
        <MaxHistory>30</MaxHistory>
    </rollingPolicy>
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        <charset>UTF-8</charset>
    </encoder>
</appender>
filter 子标签

上面说的。可以为appender 添加一个或多个过滤器,可以用任意条件对日志进行过滤。appender 有多个过滤器时,按照配置顺序执行

ThresholdFilter

临界值过滤器,过滤掉低于指定临界值的日志。当日志级别等于或高于临界值时,过滤器返回NEUTRAL;当日志级别低于临界值时,日志会被拒绝

<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
    <level>INFO</level>
</filter>

LevelFilter

级别过滤器,根据日志级别进行过滤。如果日志级别等于配置级别,过滤器会根据onMath(用于配置符合过滤条件的操作) 和 onMismatch(用于配置不符合过滤条件的操作)接收或拒绝日志。

<configuration>   
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">   
  <!-- 将过滤器的日志级别配置为INFO,所有INFO级别的日志交给appender处理,非INFO级别的日志,被过滤掉。 -->
  <filter class="ch.qos.logback.classic.filter.LevelFilter">   
    <level>INFO</level>   
    <onMatch>ACCEPT</onMatch>   
    <onMismatch>DENY</onMismatch>   
  </filter>   
  <encoder>   
    <pattern>   
      %-4relative [%thread] %-5level %logger{30} - %msg%n   
    </pattern>   
  </encoder>   
</appender>   
<root level="DEBUG">   
  <appender-ref ref="CONSOLE" />   
</root>   
Appender

ConsoleAppender:

把日志添加到控制台,有以下子节点:

  • <encoder>:对日志进行格式化。(具体参数稍后讲解 )
  • <target>:字符串 System.out 或者 System.err ,默认 System.out ;

例如:

<configuration>  
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">  
    <encoder>  
      <pattern>%-4relative [%thread] %-5level %logger{35} - %msg %n</pattern>  
    </encoder>  
  </appender>  
  
  <root level="DEBUG">  
    <appender-ref ref="STDOUT" />  
  </root>  
</configuration>  

FileAppender:

把日志添加到文件,有以下子节点:

  • <file>:被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。
  • <append>:如果是 true,日志被追加到文件结尾,如果是 false,清空现存文件,默认是true。
  • <encoder>:对记录事件进行格式化。(具体参数稍后讲解 )
  • <prudent>:如果是 true,日志会被安全的写入文件,即使其他的FileAppender也在向此文件做写入操作,效率低,默认是 false。

例子:

<configuration>  
  <appender name="FILE" class="ch.qos.logback.core.FileAppender">  
    <file>testFile.log</file>  
    <append>true</append>  
    <encoder>  
      <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>  
    </encoder>  
  </appender>  
          
  <root level="DEBUG">  
    <appender-ref ref="FILE" />  
  </root>  
</configuration>  

RollingFileAppender:

滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件。有以下子节点:

  • <file>:被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。
  • <append>:如果是 true,日志被追加到文件结尾,如果是 false,清空现存文件,默认是true。
  • <encoder>:对记录事件进行格式化。(具体参数稍后讲解 )
  • <rollingPolicy>:当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名。
  • <triggeringPolicy >: 告知 RollingFileAppender 合适激活滚动。
  • <prudent>:当为true时,不支持FixedWindowRollingPolicy。支持TimeBasedRollingPolicy,但是有两个限制,1不支持也不允许文件压缩,2不能设置file属性,必须留空

rollingPolicy:

    <appender name="jmonitorappender"  class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <FileNamePattern>/var/sankuai/logs/${app.key}.jmonitor.log.%d{yyyy-MM-dd}</FileNamePattern>
            <!--日志文件保留天数-->
            <MaxHistory>5</MaxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>

TimeBasedRollingPolicy:

最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动,这个下面又包括了两个属性:

  • FileNamePattern:必要节点,包含文件名及“%d”转换符, “%d”可以包含一个java.text.SimpleDateFormat指定的时间格式,如:%d{yyyy-MM}。如果直接使用 %d,默认格式是 yyyy-MM-dd。RollingFileAppender 的file字节点可有可无,通过设置file,可以为活动文件和归档文件指定不同位置,当前日志总是记录到file指定的文件(活动文件),活动文件的名字不会改变;如果没设置file,活动文件的名字会根据fileNamePattern 的值,每隔一段时间改变一次。“/”或者“\”会被当做目录分隔符
  • maxHistory:可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每个月滚动,且是6,则只保存最近6个月的文件,删除之前的旧文件。注意,删除旧文件是,那些为了归档而创建的目录也会被删除。
<rollingPolicy 
   class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <!--日志文件输出的文件名:按天回滚 daily -->
    <FileNamePattern>
        ${logging.path}/glmapper-spring-boot/glmapper-loggerone.log.%d{yyyy-MM-dd}
    </FileNamePattern>
    <!--日志文件保留天数-->
    <MaxHistory>30</MaxHistory>
</rollingPolicy>

FixedWindowRollingPolicy:

根据固定窗口算法重命名文件的滚动策略

  • <minIndex>:窗口索引最小值
  • <maxIndex>:窗口索引最大值,当用户指定的窗口过大时,会自动将窗口设置为12。
  • <fileNamePattern >:必须包含“%i”例如,假设最小值和最大值分别为1和2,命名模式为 mylog%i.log,会产生归档文件mylog1.log和mylog2.log。还可以指定文件压缩选项,例如,mylog%i.log.gz 或者 没有log%i.log.zip

按日期和大小拆分日志例子:

<!-- 按日期和大小拆分日志 -->
<configuration>  
  <appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">  
    <file>mylog.txt</file>  
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">  
      <!-- rollover daily -->  
      <fileNamePattern>mylog-%d{yyyy-MM-dd}.%i.txt</fileNamePattern>  
       <!-- 每个文件最大100MB,保留60天的历史意义,一共最大20gb -->  
       <maxFileSize>100MB</maxFileSize>      
       <maxHistory>60</maxHistory>  
       <totalSizeCap>20GB</totalSizeCap>  
    </rollingPolicy>  
    <encoder>  
      <pattern>%msg%n</pattern>  
    </encoder>  
  </appender>  
  <root level="DEBUG">  
    <appender-ref ref="ROLLING" />  
  </root>  
</configuration>  

Log4j2

自从Logback出来后,可以说Slf4j+Logback组合如日中天,很冲击JCL+Log4j组合,Apache眼看有被Logback反超的势头

image.png

于2012年,Apache直接推出新项目Log4j2(不兼容Log4j),Log4j2全面借鉴Slf4j+Logback(十分明显的抄袭嫌疑)

因为Log4j2不仅仅具有Logback的所有特性,还做了分离设计,分为log4j-api和log4j-core,log4j-api是日志接口,log4j-core是日志标准库,并且Apache也为Log4j2提供了各种桥接包

到目前为止Java日志体系被划分为两大阵营,分别是Apache阵营和Ceki阵营,如下图所示

image.png

介绍

Apache Log4j 2是对Log4j的升级版,参考了logback的一些优秀的设计,并且修复了一些问题,因此带 来了一些重大的提升

  • 1、性能提升, log4j2相较于log4j 和logback都具有很明显的性能提升,后面会有官方测试的数据。
  • 2、自动重载配置,参考了logback的设计,当然会提供自动刷新参数配置,最实用的就是我们在生产 上可以动态的修改日志的级别而不需要重启应用。
  • 3、 无垃圾机制,log4j2在大部分情况下,都可以使用其设计的一套无垃圾机制,避免频繁的日志收集 导致的jvm gc
  • 4、异常处理,在logback中,Appender中的异常不会被应用感知到,但是在log4j2中,提供了一些异 常处理机制。

记录日志的过程,大体上可以分成三个步骤:

1、在程序中对原始日志信息进行采集
2、对采集下来的日志信息进行格式化
3、将格式化好的日志信息写入目的地

Log4j2 的架构也自然是按照这个来的,Log4j2 中有三个重要的组件分别实现了这些功能,分别是 Logger 、Layout 和Appender,日志数据流向图如下: image.png

使用

slf4j+log4j2 入门配置

1、增加配置

<!-- Log4j2 门面API--> <dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.11.1</version>
</dependency>
<!-- Log4j2 日志实现 --> <dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.11.1</version>
</dependency>
<!--使用slf4j作为日志的门面,使用log4j2来记录日志 --> <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<!--为slf4j绑定日志实现 log4j2的适配器 --> <dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.10.0</version>
</dependency>

2、加入log4j2 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="info">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

3、测试代码

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class App {
    public final static Logger LOGGER = LoggerFactory.getLogger(App.class);
    public static void main(String[] args) {
        // 声明日志对象
        LOGGER.debug("lm.test");
        LOGGER.info("lm.test");
        LOGGER.warn("lm.test");
    }
}
结果:
14:23:38.229 [main] INFO  com.lm.App - lm.test
14:23:38.233 [main] WARN  com.lm.App - lm.test
Logger logger = LogManager.getLogger(loggerName);
logger.info(msg);

对于获取logger和调用info操作的通用流程我直接用流程图: image.png

web项目配置

web项目需要加入web.xml

<context-param>  
    <param-name>log4jConfiguration</param-name>  
    <param-value>/WEB-INF/conf/log4j2.xml</param-value>  
</context-param>  
  
<listener>  
    <listener-class>org.apache.logging.log4j.web.Log4jServletContextListener</listener-class>  
</listener>  

Log4j2配置

Log4j2配置方式

Log4j2 有两种配置方式:

  • 通过 ConfigurationFactory 使用编程的方式进行配置
  • 通过配置文件配置,支持 XML 、JSON 、YAML 和 properties 等文件类型

通过编程实现的方式如下:

@Plugin(name = "CustomerConfigurationFactory", category = ConfigurationFactory.CATEGORY)
public class CustomerConfigurationFactory extends ConfigurationFactory {
    protected String[] getSupportedTypes() {
        return new String[0];
    }
    public Configuration getConfiguration(LoggerContext loggerContext, ConfigurationSource configurationSource) {
        ConfigurationBuilder<BuiltConfiguration> builder = newConfigurationBuilder();
        builder.setStatusLevel(Level.INFO);
        return builder.build();
    }
}

在 Log4j2 中,默认实现了四种 ConfigurationFactory,分别用来加载 JSON YAML properties XML 格式的配置文件、log4j2默认会在classpath目录下寻找log4j.json、log4j.jsn、log4j2.xml等名称的文件,如果都没有找到,则会按默认配置输出,也就是输出到控制台,并且默认级别是 Level.ERROR

假如同时存在 log4j2.properties log4j2.yaml log4j2.json log4j2.xml 四个配置文件,加载的优先级为 properties > yaml > json > xml

Log4j2配置文件详解

Configuration配置

Confirguration 是配置文件根元素,每个配置文件有且仅有一个。如果不使用配置文件使用默认配置,以下配置与默认配置等价:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
  <Appenders>
    <Console name="Console" target="SYSTEM_OUT">
      <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
    </Console>
  </Appenders>
  <Loggers>
    <Root level="error">
      <AppenderRef ref="Console"/>
    </Root>
  </Loggers>
</Configuration>

每一个配置都至少包含一个 Appender 和 Logger。status 属性用于配置 Log4j2 内部的的日志级别

日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出

Log4j2 可以自动检测配置文件的变化,monitorInterval 属性可以配置自动检测配置文件的时间,这个属性最小的值为 5,代表 5 秒检测一次

<?xml version="1.0" encoding="UTF-8"?>
<Configuration monitorInterval="30">
....
</Configuration>
Logger配置

Logger 元素用于配置日志记录器。Logger 有两种形式,Logger 和 Root,两个区别在于 Root 没有 name 属性并且不支持 additivity 属性

假如我们要获取一个特定类的的某个级别的日志。调高日志的级别无法完成这样的需求。一种可行的方式是在 Loggers 中添加一个 Logger,配置方式如下:

<Loggers>
    <Logger name="cn.rayjun.Log4j2HelloWorld" level="trace">
       <AppenderRef ref="Console"/>
    </Logger>
    <Root level="error">
        <AppenderRef ref="Console"/>
    </Root>
 </Loggers>

这样配置,除了 cn.rayjun.Log4j2HelloWorld 类会打印出 trace 级别的日志,其他就只会打印出 error 级别的日志,打印结果如下:

19:59:06.957 [main] TRACE cn.rayjun.Log4j2HelloWorld - Enter
19:59:06.957 [main] TRACE cn.rayjun.Log4j2HelloWorld - Enter
19:59:06.959 [main] ERROR cn.rayjun.Log4j2HelloWorld - Hello world
19:59:06.959 [main] ERROR cn.rayjun.Log4j2HelloWorld - Hello world

但是上面打印的日志有个问题,信息都被打印了两遍,为了解决这个问题,需要添加 additivity 参数并且置为 false,如下配置:

<Loggers>
    <Logger name="cn.rayjun.Log4j2HelloWorld" level="trace" additivity="false">
        <AppenderRef ref="Console"/>
    </Logger>
    <Root level="error">
        <AppenderRef ref="Console"/>
    </Root>
</Loggers>

Logger的additivity设置为false,那么这个日志不会再继续向父Logger进行传递

异步logger

log4j2最大的特点就是异步日志,其性能的提升主要也是从异步日志中受益,我们来看看如何使用 log4j2的异步日志

默认情况下 Logger 都是同步的,但是也有异步的实现、Root 的异步版本是 AsyncRoot,Logger 的异步版本是 AsyncLogger,异步 Logger 依赖外部队列 LMAX Disruptor。使用异步 Logger 需要加上外部依赖:

<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.4.2</version>
</dependency>

Log4j2提供了两种实现日志的方式,一个是通过AsyncAppender,一个是通过AsyncLogger,分别对应我们说的Appender组件和Logger组件

AsyncAppender方式

异步依赖arrayblockingqueue队列

image.png AsyncAppender的核心部件是一个阻塞队列,logger将数据通过append方法放入到阻塞队列中,随后后台线程从队列中取出数据然后进行后续的操作

使用case:

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

AsyncLogger方式

Root 的异步版本是 AsyncRoot,Logger 的异步版本是 AsyncLogger,异步 Logger 依赖外部队列 LMAX Disruptor。

image.png AsyncLogger调用Disruptor,然后直接返回。但是Disruptor 比较吃内存、可能会导致cpu 飙升、谨慎使用

例子:

<Configuration status="WARN" monitorInterval="30">
    <Appenders>
        <RollingRandomAccessFile name="applicationAppender" fileName="./log/application.log"
                                 filePattern="./log/$${date:yyyy-MM}/common-%d{yyyy-MM-dd}.log.gz"
                                 append="false">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%p] - %l - %m%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy/>
            </Policies>
        </RollingRandomAccessFile>
    </Appenders>
    <Loggers>
        <!-- AsyncLogger配置 -->
        <AsyncLogger name="log4j2" >
            <AppenderRef ref="applicationAppender"/>
        </AsyncLogger>
        <Root level="info">
            <AppenderRef ref="applicationAppender"/>
        </Root>
    </Loggers>
</Configuration>

注意

  1. 如果使用异步日志,AsyncAppender、AsyncLogger和全局日志,不要同时出现。性能会和 AsyncAppender一致,降至最低
  2. 需要设置includeLocation=false ,如果要打印位置信息会急剧降低异步日志的性能,比同步日志还要 慢
Filter配置

ThresholdFilter

Filters决定日志事件能否被输出。过滤条件有三个值:ACCEPT(接受),DENY(拒绝),NEUTRAL(中立)

image.png

输出warn级别一下的日志

<Filters>
    <!--如果是error级别拒绝,设置 onMismatch="NEUTRAL" 可以让日志经过后续的过滤器-->
    <ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL"/>
    <!--如果是debug\info\warn级别的日志输出-->
    <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>

只输出error级别以上的日志

<Filters>
    <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>

参考:www.docs4dev.com/docs/zh/log…

Appender配置

Appender 负责将日志分发到相应的目的地。也就是说 Appender 决定日志以何种方式展示,上面使用到的就是 ConsoleAppender,这个 Appender 会将日志直接打印到控制台。同时还支持将日志输出到文件、数据库、消息队列

    <Appenders>
        <!--这个输出控制台的配置 -->
        <Console name="Console" target="SYSTEM_OUT">
            <!-- 控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) -->
            <ThresholdFilter level="trace" onMatch="ACCEPT" onMismatch="DENY"/>
            <!-- 这个都知道是输出日志的格式 -->
            <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
        </Console>

        <!--添加过滤器ThresholdFilter,可以有选择的输出某个级别以上的类别  onMatch="ACCEPT" onMismatch="DENY"意思是匹配就接受,否则直接拒绝  -->
        <File name="ERROR_FILE" fileName="./logs/error.log">
            <ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="%d{yyyy.MM.dd 'at' HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n"/>
        </File>

        <File name="WARN_FILE" fileName="./logs/warn.log">
            <ThresholdFilter level="ERROR" onMatch="DENY" onMismatch="NEUTRAL"/>
            <ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="%d{yyyy.MM.dd 'at' HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n"/>
        </File>

        <!--这个会打印出所有的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档 -->
        <RollingFile name="INFO_RollingFile" fileName="./logs/info.log" filePattern="logs/$${date:yyyy-MM}/info-%d{MM-dd-yyyy}-%i.log.gz">
            <ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL"/>
            <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
            <PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n"/>
            <SizeBasedTriggeringPolicy size="2MB"/>
        </RollingFile>

        <RollingFile name="RollingFile" fileName="logs/app.log"
                 filePattern="logs/$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.log.gz">
            <PatternLayout>
                <Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
            </PatternLayout>
            <Policies>
                <!--TimeBasedTriggeringPolicy 会与 filePattern 的配置相匹配,如果 filePattern 是 {yyyy-MM-dd HH-mm} ,最小的时间粒度是分钟,那么就会每隔一分钟生成一个文件,如果改成 {yy-MM-dd HH},最小时间粒度是小时,那么就会每隔一个小时生成一个文件-->
                <TimeBasedTriggeringPolicy />
                <!--SizeBasedTriggeringPolicy 表示设定日志的大小,上面的配置是指日志大小到 250M 后开始生成新的日志文件-->
                <SizeBasedTriggeringPolicy size="250 MB"/>
            </Policies>
        </RollingFile>


    </Appenders>
Layouts配置

Appender 会使用 Layout 来对日志进行格式化。Lo4j1 和 Logback 中的 Layout 会把日志转成 String,而在 Log4j2 中使用的则是 byte 数组,这是从性能的角度进行的优化

Layout 配置支持多种方式。用的最多的方式就是 PatternLayout,就是通过正则表达式来格式化日志,应用的也最多,基本配置如下

<PatternLayout>
    <Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
</PatternLayout>
%d 表示时间,默认情况下表示打印完整时间戳 2012-11-02 14:34:02,123,可以调整 %d 后面的参数来调整输出的时间格式
%p 表示输出日志的等级,可以使用 %highlight{%p} 来高亮显示日志级别
%c 用来输出类名,默认输出的是完整的包名和类名,%c{1.} 输出包名的首字母和完整类名
%t 表示线程名称
%m 表示日志内容,%M 表示方法名称
%n 表示换行符
%L 表示打印日志的代码行数

完整简单demo实现

假设现在有一个日志需求:日志以文件的形式进行保存,需要保存完整的日志信息,包括日志对应的类代码行数等,并且需要根据时间和日志文件的大小生成新的日志文件

<Configuration status="warn">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="[%-5p %d{yyyy-MM-dd HH:mm:ss.SSS}] [%t] [%C.%M] [%m]%n"/>
        </Console>
        <RollingRandomAccessFile name="${service_name}-log"
                                 fileName="~/${service_name}/${service_name}.log"
                                 filePattern="~/${service_name}/${service_name}.log.%d{yyyyMMddHH}"
                                 append="true" immediateFlush="false">
            <PatternLayout
                    pattern="[%-5p %d{yyyy-MM-dd HH:mm:ss.SSS}] [%t] [%C.%M] [%m]%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="250 MB"/>
            </Policies>
        </RollingRandomAccessFile>
    </Appenders>
    <Loggers>
        <AsyncRoot level="${log_level}" includeLocation="true">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="${service_name}-log" />
        </AsyncRoot>
    </Loggers>
</Configuration>

Logger 上如果加上了 includeLocation 后,日志性能会下降的很厉害,如果日志的位置信息不是必要的,就不需要加上