Java日志梳理

log4j

在Java1.4之前,JDK并没有日志。Apache基金会最早实现一套日志框架log4j,在Java1.4之前只有这一种选择。

Log4j中有三个主要组成部分:

  • loggers: 负责捕获记录信息。
  • appenders : 负责发布日志信息,以不同的首选目的地。
  • layouts: 负责格式化不同风格的日志信息。

log4j框架

Log4j API设计为分层结构,其中每一层提供了不同的象,对象执行不同的任务。这使得设计灵活,根据将来需要来扩展。

有两种类型可用在Log4j的框架对象。

  • 核心对象: 框架的强制对象和框架的使用。
  • 支持对象: 框架和支持体核心对象,可选的对象执行另外重要的任务。
log4j

核心对象

  • Logger对象:顶级层的Logger,它提供Logger对象。Logger对象负责捕获日志信息及它们存储在一个空间的层次结构。
  • 布局(Layout)对象:该层提供其用于格式化不同风格的日志信息的对象。布局层提供支持Appender对象到发布日志信息之前。布局对象的发布方式是人类可读的及可重复使用的记录信息的一个重要的角色。
  • Appender对象:下位层提供Appender对象。Appender对象负责发布日志信息,以不同的首选目的地,如数据库,文件,控制台,UNIX系统日志等。

支持对象

log4j框架的其他重要的对象起到日志框架的一个重要作用:

  • Level对象:级别对象定义的任何记录信息的粒度和优先级。有记录的七个级别在API中定义:OFF, DEBUG, INFO, ERROR, WARN, FATAL 和 ALL。
  • Filter对象:过滤对象用于分析日志信息及是否应记录或不用这些信息做出进一步的决定。一个appender对象可以有与之关联的几个Filter对象。如果日志记录信息传递给特定Appender对象,都和特定Appender相关的Filter对象批准的日志信息,然后才能发布到所连接的目的地。
  • 对象渲染器ObjectRenderer对象是一个指定提供传递到日志框架的不同对象的字符串表示。这个对象所使用的布局对象来准备最后的日志信息。
  • 日志管理(LogManager):日志管理对象管理的日志框架。它负责从一个系统级的配置文件或配置类读取初始配置参数。

log4j配置

配置log4j涉及分配级别,定义追加程序,并在配置文件中指定布局的对象

log4j.properties

  • 根日志记录器(logger)的级别定义为DEBUG并连接**附加器(appender)**命名为X

  • 附加器X定义为org.apache.log4j.FileAppender, 并写入到一个名为“log.out”位于日志log目录下

  • 定义的布局模式是%m%n,这意味着每打印日志消息之后,将加上一个换行符

      # Define the root logger with appender X 
    log4j.rootLogger = DEBUG, X
    # Set the appender named X to be a File appender
    log4j.appender.X=org.apache.log4j.FileAppender
    log4j.appender.X.File=${log}/log.out
    #Define the layout for X appender
    log4j.appender.X.layout=org.apache.log4j.PatternLayout
    log4j.appender.X.layout.conversionPattern=%m%n
    

    需要注意的是log4j支持UNIX风格的变量替换,如 ${variableName}

日志等级

日志等级有如下:

  • ALL
  • TRACE
  • DEBUG
  • INFO
  • WARN
  • ERROR
  • FATAL
  • OFF

级别p的级别使用q,在记录日志请求时,如果p>=q启用。对于标准级别它们关系如下:ALL < DEBUG < INFO < WARN < ERROR < FATAL < OFF。

Appenders

Apache的log4j提供Appender对象主要负责打印日志消息到不同的目的地,如控制台,文件,sockets,NT事件日志等等。

每个Appender对象具有与之相关联的不同的属性,并且这些属性表明对象的行为:

属性 描述
layout Appender使用布局Layout 对象和与之相关的格式化的日志记录信息转换模式
target 目标可以是一个控制台,一个文件,或根据附加器的另一个项目
level 级别是必需的,以控制日志消息的过滤
threshold Appender可以有与之独立的记录器级别相关联的级别阈值水平。Appender忽略具有级别低于阈级别的任何日志消息
filter Filter 对象可以分析超出级别的匹配记录信息,并决定是否记录的请求应该由一个特定 Appender 或忽

可以通过包括以下方法的配置文件中的下面设置一个 Appender 对象添加到记录器:

log4j.logger.[logger-name]=level, appender1,appender..n 

可以编写以XML格式相同的结构如下:

<logger name="com.apress.logging.log4j" additivity="false">   
  <appender-ref ref="appender1"/>    
  <appender-ref ref="appender2"/>
</logger>

我们仅使用一个附加目的地FileAppender在我们上面的例子。所有可能的附加目的地选项有:

  • AsyncAppender
  • ConsoleAppender
  • FileAppender
  • DailyRollingFileAppender
  • JDBCAppender
  • SMTPAppender
  • SocketAppender
  • ......

Layout

所有可能的选项有:

  • DateLayout
  • HTMLLayout
  • PatternLayout(最常用)
  • SimpleLayout
  • XMLLayout

使用HTMLLayout和XMLLayout,可以在HTML和XML格式和生成日志。

# Define the root logger with appender file
log = /usr/home/log4j
log4j.rootLogger = DEBUG, FILE

# Define the file appender
log4j.appender.FILE=org.apache.log4j.FileAppender
log4j.appender.FILE.File=${log}/htmlLayout.html

# Define the layout for file appender
log4j.appender.FILE.layout=org.apache.log4j.HTMLLayout
log4j.appender.FILE.layout.Title=HTML Layout Example
log4j.appender.FILE.layout.LocationInfo=true
public class log4jExample{
  static Logger log = Logger.getLogger(log4jExample.class.getName());

  public static void main(String[] args)
                throws IOException,SQLException{

     log.debug("Hello this is an debug message");
     log.info("Hello this is an info message");
  }
}

JUL(Java Util Log)

在一段时间内,Log4j近乎成了Java社区的日志标准。据说Apache基金会还曾经建议Sun引入Log4j到java的标准库中,但Sun拒绝了。
终于在2002年Java1.4发布,Sun推出了自己的日志库J.U.L(jdk-logging)。但基本上是模仿Log4j的实现。有点儿鸡肋,但最起码解决了有无的问题。从此开发者有了两种选择。(虽然并不推荐JUL).

JCL(Commons Logging)

因为有了两种选择,所以导致了日志使用的混乱。所以Apache推出了J.C.L(commons-logging)。它只是定义了一套日志接口,支持运行时动态加载日志组件。应用层编写代码时,只需要使用J.C.L提供的统一接口来记录日志,**在程序运行时会优先找系统是否集成Log4j,如果集成则使用Log4j做为日志实现,如果没找到则使用J.U.L做为日志实现。**J.C.L的出现解决了多种日志框架共存的尴尬,也是面向接口编程思想的一种具体体现。

通过LogFactory获取Log类的实例; 第二步,使用Log实例的方法打日志。

<!--引入common-logging-->
<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;

public class Main {
    public static void main(String[] args) {
        Log log = LogFactory.getLog(Main.class);
        log.info("start...");
        log.warn("end.");
    }
}

Commons Logging定义了6个日志级别:

  • TRACE
  • DEBUG
  • INFO
  • WARNING
  • ERROR
  • FATAL

SLF4J基本概念

作为元老级日志 Log4j 的作者 (Ceki Gülcü),他觉得 JCL 不够优秀,所以他再度出山,搞出了一套更优雅的日志框架 SLF4J(这个也是抽象层),即简单日志门面(Simple Logging Facade for Java),并为 SLF4J 实现了一个亲儿子——logback,确实更加优雅了。

SLF4J代表_Simple Logging Facade for Java_。它提供了Java中所有日志框架的简单抽象。
SLF4J

如同使用JDBC基本不用考虑具体数据库一样,SLF4J提供了统一的记录日志的接口,只要按照其提供的方法记录即可,最终日志的格式、记录级别、输出方式等通过具体日志系统的配置来实现,因此可以在应用中灵活切换日志系统。比如:slf4j-simple、logback都是slf4j的具体实现;log4j并不直接实现slf4j,但是有专门的一层桥接slf4j-log4j12来实现slf4j。

使用SLF4J时,结构如下:

  • slf4j-api(接口层):这个包只有日志的接口,并没有实现
  • 各日志实现包的连接层( slf4j-jdk14, slf4j-log4j12): 各日志实现包的适配器
  • 各日志实现包

SLF4J集成logback

logback分成三个模块:

  • logback-core 提供了logBack的核心功能,是另外两个组件的基础;
  • logback-classic 模块实现了SLF4J API;
  • logback-access 模块与Servlet容器集成提供Http来访问日志的功能。

依赖

<!--slf4j -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.20</version>
</dependency>

<!-- logback -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.1.7</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.1.7</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-access</artifactId>
    <version>1.1.7</version>
</dependency>

配置文件

<!--每天生成一个文件,归档文件保存30天:-->
<configuration >

    <!--设置自定义pattern属性-->
    <property name="pattern" value="%d{HH:mm:ss.SSS} [%-5level] [%thread] [%logger] %msg%n"/>

    <!--控制台输出日志-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--设置控制台输出日志的格式-->
        <encoder>
            <pattern>${pattern}</pattern>
        </encoder>
    </appender>

    <!--滚动记录日志文件:-->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--当天生成的日志文件名称:-->
        <file>e:/log.out</file>
        <!--根据时间来记录日志文件:-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--归档日志文件的名称:-->
            <fileNamePattern>testLog-%d{yyyy-MM-dd}.log</fileNamePattern>
            <!--归档文件保存30天-->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <!--生成的日志信息格式-->
        <encoder>
            <pattern>${pattern}</pattern>
        </encoder>
    </appender>

    <!--根root logger-->
    <root level="DEBUG">
        <!--设置根logger的日志输出目的地-->
        <appender-ref ref="FILE" />
        <appender-ref ref="CONSOLE" />
    </root>

</configuration>

使用

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class slf4j_logbackDemo {
    Logger logger=  LoggerFactory.getLogger(slf4j_logbackDemo.class);

    @Test
    public void test() {
        logger.debug("debug message");
        logger.info("info message");
        logger.warn("warning message");
        logger.error("error message");
        logger.warn("login message");
    }
}

配置文件详解

配置文件

configuration:配置根节点

<configuration scan="true" scanPeriod="60" debug="false"></configuration>  
  • scan:程序运行时配置文件被修改,是否重新加载。true,重新加载;false,不重新加载;默认为true;
  • scanPeriod:监测配置文件被修改的时间间隔,scan属性必须设置为true才可生效;默认为1分钟,默认单位是毫秒;
  • debug:是否打印logback程序运行的日志信息。true,打印;false,不打印;默认为false;

property:属性变量

<configuration scan="true" scanPeriod="60" debug="false">
    <property name="pattern" value="%d{HH:mm:ss.SSS} [%-5level] [%logger] %msg%n" >
    </property>
</configuration>  
  • name:变量的名称,可以随意起名,但建议名字要简明直译;
  • value:变量的值;

在配置文件中,我们可以用 ${} 的方式来使用,将变量引入到其他节点中去。如果有多处使用相同的内容,便可使用属性变量的方式进行统一,减少很多不必要的代码;

logger:日志对象

logger分为2种,一种是普通日志对象,另一种是根日志对象。对于大部分应用来说,只设置根日志对象即可。在java日志系统中,无论是log4j还是logback,他们的日志对象体系都是呈现“树”的形式,根日志对象为最顶层节点,其余包或者类中的日志对象都继承于根日志节点;

对于普通日志对象来说,我们可以设置某一个包或者某一个类的日志级别,还可以单独设置日志的输出目的地;

<configuration scan="true" scanPeriod="60" debug="false">   
    <logger name="java.sql" level="debug" addtivity="true">
        <appender-ref ref="CONSOLE" />
    </logger>   
</configuration>  
  • name:用来指定此logger属于哪个包或者哪个类;
  • level:用来指定此logger的日志打印级别;
  • addtivity:是否向上传递日志打印信息。之前说了,logger对象呈现一个树的结构,根logger是树的顶端,下面的子logger的addtivity属性如果设置为true则会向上传递打印信息,出现日志重复打印的现象;
  • appender-ref:日志输出目的地,将此logger所打印的日志交给此appender;

值得注意的是,上面的例子中,如果此logger没有指定appender,而且addtivity也设置为true,那么此logger对应的日志信息只会打印一遍,是由root来完成的;但是如果addtivity设置成false,那么此logger将不会输出任何日志信息;

logger:根日志对象'

root也是日志对象中的一种,但它位于logger体系中的最顶层。当一个类中的logger对象进行打印请求时,如果配置文件中没有为该类单独指定日志对象,那么都会交给root根日志对象来完成;

root节点中只有一个level属性,还可以单独指定日志输除目的地;

<configuration scan="true" scanPeriod="60" debug="false">   
    <root level="DEBUG">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

appender:日志输出目的地

与log4j中的appender一样,logback中的节点也同样负责日志输出的目的地。

appender中有2个必填属性: nameclass。name为节点的名称,class为的全限定类名,也就是日志输出目的地的处理类。此外,我们还可以在中单独指定日志的格式,设置日志过滤器等操作;

ConsoleAppender

将日志输出到控制台,可以在其节点中设置子节点,设置日志输出的格式;

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

FileAppender

将日志输出到具体的磁盘文件中,可以单独指定具体的位置,也可以设置日志的输出格式;

<appender name="FILE" class="ch.qos.logback.core.FileAppender">  
    <file>e:/log.out</file>  
    <append>true</append>  
    <prudent>false</prudent>
    <encoder>  
        <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>  
    </encoder>  
</appender> 
  • :新增的日志是否以追加到文件结尾的方式写入到log.out文件中,true为追加,fasle为清空现存文件写入;
  • :日志是否被安全的写入磁盘文件,默认为false。如果为true,则效率低下

RollingFileAppender

滚动记录日志,当符合节点中设置的条件时,会将现有日志移到新的文件中去。节点中可设置的条件为:文件的大小、时间等;

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>e:/log.out</file>
    <append>true</append>  
    <prudent>false</prudent>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>testLog-%d{yyyy-MM-dd}.log</fileNamePattern>
        <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder>
        <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
    </encoder>
</appender>

其中,rollingPolicy 有如下几种:

  • TimeBasedRollingPolicy表示根据时间制定日志文件的滚动策略
  • FixedWindowRollingPolicy表示如果日志文件大小超过指定范围时,会根据文件名拆分成多个文件;
  • SizeBasedTriggeringPolicy表示根据日志文件大小,超过制定大小会触发日志滚动;

AsyncAppender

异步记录日志,内部通过使用缓存的方式来实现异步打印,将日志打印事件event放入缓存中。具体数据结构为BlockingQueue;

<appender name="FILE" class="ch.qos.logback.core.FileAppender">  
    <file>e:/log.out</file>  
    <append>true</append>  
    <prudent>false</prudent>
    <encoder>  
        <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>  
    </encoder>  
</appender> 
<appender name ="ASYNC" class= "ch.qos.logback.classic.AsyncAppender">  
    <discardingThreshold>0</discardingThreshold>  
    <queueSize>512</queueSize>  
    <appender-ref ref ="FILE"/>  
</appender>  
  • :指的是BlockingQueue的队列容量大小,默认为256个;
  • :如果BlockingQueue中还剩余20%的容量,那么程序会丢弃TRACE、DEBUG和INFO级别的日志打印事件event,只保留WARN和ERROR级别的。为了保留所有的日志打印事件,可以将该值设置为0。

encoder

日志格式化节点,负责格式化日志信息。只负责了两件事情,第一负责将日志信息转换成字节数组,第二将字节数组写到输出流当中去;
中使用来设置对应的格式;

<encoder> 
  <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder  

SLFJ适配器

slf4j是通过自己的api去调用实现组件的api,这样来完成适配的。我们重点看看是怎么做到适配的。

SLF4J集成log4j

依赖

<dependencies>
  
   <!-- 引入log4j需要log4j-api和log4j-core -->
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.17</version>
    </dependency>


  <!-- 引入log4j12需要log4j-api和log4j-core
   <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-api</artifactId>
       <version>1.7.25</version>
  </dependency>
  <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-core</artifactId>
     <version>1.7.25</version>
  </dependency>
  -->

    <!-- 引入slfj接口-->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.25</version>
    </dependency>
  

   <!-- 引入适配器-->
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-slf4j-impl</artifactId>
      <version>1.7.25</version>
    </dependency>
  </dependencies>

配置文件

log4j.rootLogger = DEBUG,stdout,D

# 配置日志信息输出目的地
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
# Target是输出目的地的目标
log4j.appender.stdout.Target = System.out
# 指定日志消息的输出最低层次
log4j.appender.stdout.Threshold = INFO
# 定义名为stdout的输出端的layout类型
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
# 如果使用pattern布局就要指定的打印信息的具体格式ConversionPattern
log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss} %l%m%n



# 名字为D的对应日志处理
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
# File是输出目的地的文件名
log4j.appender.D.File = LOG//app_debug.log
#false:默认值是true,即将消息增加到指定文件中,false指将消息覆盖指定的文件内容
log4j.appender.D.Append = true
log4j.appender.D.Threshold = DEBUG
log4j.appender.D.layout = org.apache.log4j.TTCCLayout

使用

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;



public class TestLog{
    private final static Logger logger = LoggerFactory.getLogger(TestLog.class);
    public static void main(String[] args) {
        logger.info("programer processing......");
        logger.error("Programer error......");
        logger.debug("start Debug detail......");
    }
}

slf4j其他适配器

A. jul
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-jdk14</artifactId>
            <version>2.0.0-alpha1</version>
        </dependency>
 B. log4j
          <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.14.1</version>
        </dependency>

C.log4j2
          <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>2.0.0-alpha1</version>
        </dependency>
D. logback
         <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.3.0-alpha5</version>
        </dependency>

slfj桥接器

现在还有一个问题,假如你正在开发应用程序所调用的组件当中已经使用了 JCL 的,还有一些组建可能直接调用了 java.util.logging,这时你需要一个桥接器(名字为 XXX-over-slf4j.jar)把他们的日志输出重定向到 SLF4J

<!-- 配置 log4j 的桥接器 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.25</version>
</dependency>

<!-- 配置 jcl 的桥接器 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>1.7.8</version>
</dependency>

<!-- 配置 jul 的桥接器 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jul-to-slf4j</artifactId>
    <version>1.7.25</version>
</dependency>
  • 所谓的桥接器就是一个假的日志实现工具,比如当你把 jcl-over-slf4j.jar 放到 CLASS_PATH 时,即使某个组件原本是通过 JCL 输出日志的,现在却会被 jcl-over-slf4j “骗到”SLF4J 里,然后 SLF4J 又会根据绑定器把日志交给具体的日志实现工具。

  • 需要把其他包中的目标包排掉。比如使用jcl-over-slf4时,先把其他地方的commons-log包排掉,这样就会加载jcl-over-slf4内部的org.apache.commons.logging.LogFactory而不是 commons-log内部的 org.apache.commons.logging.LogFactory.

    package org.apache.commons.logging;
    public abstract class LogFactory {
      // ****
    static LogFactory logFactory = new SLF4JLogFactory();
    }
    

MDC

MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。

package org.slf4j;
public class MDC {
  // 将一个K-V的键值对放到容器,其实是放到当前线程的ThreadLocalMap中
  public static void put(String key, String val);

  // 根据key在当前线程的MDC容器中获取对应的值
  public static String get(String key);

  // 根据key移除容器中的值
  public static void remove(String key);

  // 清空当前线程的MDC容器
  public static void clear();
}

经常用来在日志中记录traceId, 在进程的trace上下文更改时进行设置:

public class Slf4jMdcUpdater extends ContextListener {

    private static final Slf4jMdcUpdater singleton = new Slf4jMdcUpdater();

    private EagleEyeSlf4jMdcUpdater() {
    }

    public static Slf4jMdcUpdater getInstance() {
        return singleton;
    }

    @Override
    public void beforeSet(RpcContext_inner context) {
        if (context != null) {
            MDC.put("TRACE_ID", context.getTraceId());
            MDC.put("RPC_ID", context.getRpcId());
        } else {
            MDC.remove("TRACE_ID");
            MDC.remove("RPC_ID");
        }
    }
}

参考: