Logback 日志模块

Java程序开发中记录日志有许多不同的实现库,包括JDK自带的JUL以及第三方日志模块Log4jLog4j2Logbackcommons-logging等。其中,Logback是一个功能丰富的日志库,而且其性能不错,它也是SpringBoot默认集成的日志模块,这篇笔记我们简单介绍一下Logback的使用。

Logback和SLF4J

实际开发中我们通常并不直接调用Logback的API,LogbackLog4j等许多日志库都实现了SLF4J的API,SLF4J也被称为日志门面库,我们代码中实际使用Logger等类时,你会发现引入的包都是SLF4J的,日志门面库和实现库之间的关系就像接口和实现类一样,使用SLF4J的API而不是直接调用某个日志库,这种做法解耦了应用程序和日志的实现,这也是一种最佳实践。

添加Maven依赖

pom.xml中,我们直接将slf4jlogback的依赖引入项目即可。

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

其中,slf4j-api是SLF4J的接口,其中没有包含具体的日志记录实现,而logback-classic是日志记录实现库(具体实现库实际上是logback-core,但它会被logback-classic间接引入,我们可以不必手动配置)。

SLF4J桥接包解决日志模块冲突

实际开发中我们用到的某些库可能直接依赖一个特定的日志模块,例如某库自行依赖了log4j,而我们整个项目使用的都是Logback,两个日志模块之间可能产生冲突和配置上的麻烦,这是相当糟糕的设计,常见于一些历史遗留项目。我们不能让这些模块自己搞特殊,好在slf4j提供了bridge桥接包能够解决这类问题。

使用桥接包前,我们需要将原库中直接依赖的日志模块排除,例如排除log4j

<dependency>
    ...
    <exclusions>
        <exclusion>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
        </exclusion>
    </exclusions>
</dependency>

然后引入bridge桥接包。

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>log4j-over-slf4j</artifactId>
  <version>1.7.25</version>
</dependency>

之后再正常手动引入SLF4JLogback依赖即可正常使用Logback框架统一实现日志输出了。

注:还有一种情况是库虽然直接依赖了log4j但仍基于SLF4J的API打印日志,这种我们仍需移除log4j,但不必引入桥接包,桥接包仅用于直接使用非SLF4J的API打印日志的情况。

Logback基础使用

Logback有五个日志级别:严重性分级为TRACE < DEBUG < INFO < WARN < ERROR。使用Logback非常简单,首先我们要获得Logger对象,然后在其上调用debug()error()等方法即可,和JDK内置的日志工具使用起来是一样的。

package com.gacfox.demo;

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

public class Main {
    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(Main.class);
        logger.debug("hello");
    }
}

输出结果如下。

16:32:37.233 [main] DEBUG com.gacfox.Main - hello

拼接日志参数

我们输出的日志可能带有参数,如果使用+进行字符串拼接不仅代码可读性差而且可能有性能问题,这种时候我们可以使用拼接日志参数功能。

String name = "Tom";
String age = "18";
logger.debug("My name is {}, I am {} years old", name, age);

输出结果如下。

16:42:38.772 [main] DEBUG com.gacfox.Main - My name is Tom, I am 18 years old

异常信息输出

如果不使用日志框架,我们的异常信息一般都是通过e.printStackTrace()打印到控制台的,这种方式通常仅用于学习阶段,实际企业级的项目中我们需要使用日志框架统一收集这些异常日志,异常使用error()方法记录。

try {
    throw new RuntimeException("Code run failed here...");
} catch (Exception e) {
    logger.error("--e:", e);
}

logger.error()方法有针对Throwable类型的重载,我们不需要手动编写{}占位符等操作。

Logback配置

Logback支持XML或groovy脚本格式的配置文件,我们一般使用XML格式。这个配置文件默认应放置在classpath:logback.xml位置(即Maven项目的src/main/resources目录下),如果不存在任何配置文件Logback则会加载一些默认配置,下面是一个配置文件例子。

logback.xml

<?xml version="1.0" encoding="UTF-8"?>

<configuration>

    <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
        <encoder charset="UTF-8">
            <pattern>%yellow(%date) %highlight(%-5level) %cyan(%logger{5}@[%-4.30thread]) - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="debug">
        <appender-ref ref="stdout"/>
    </root>
</configuration>

上面的配置和默认差不多,只不过修改了输出格式,加上了颜色输出(需要终端支持)。

appender

<appender>指定日志输出到哪里,以什么格式输出。上面例子中,我们把日志输出到了终端(标准输出)上。我们也可以把日志输出到文件中,下面例子我们把日志输出到文件里,同时使用基于时间的滚动策略。

<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">

  <!-- 可选:过滤器,这里用LevelFilter配置只记录ERROR级别的日志 -->
  <filter class="ch.qos.logback.classic.filter.LevelFilter">
    <level>ERROR</level>
    <onMatch>ACCEPT</onMatch>
    <onMismatch>DENY</onMismatch>
  </filter>

  <!-- 按照时间的日志记录策略 -->
  <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <fileNamePattern>E:/%d{yyyy-MM-dd}/error-log.log</fileNamePattern>
  </rollingPolicy>

  <encoder charset="UTF-8">
    <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
  </encoder>
</appender>

除了基于时间的滚动策略,另一种常用的做法是基于文件大小的滚动策略。

<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">

  <!-- 过滤器,只记录ERROR级别的日志 -->
  <filter class="ch.qos.logback.classic.filter.LevelFilter">
    <level>ERROR</level>
    <onMatch>ACCEPT</onMatch>
    <onMismatch>DENY</onMismatch>
  </filter>
  <file>E:/error.log</file>

  <!--基于文件大小的滚动策略-->
  <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
    <fileNamePattern>E:/error-%i.log.zip</fileNamePattern>
    <!--窗口大小为1-3,当归档文件数量大于3时,会向后覆盖旧的日志,默认为7-->
    <!--<minIndex>1</minIndex>-->
    <!--<maxIndex>3</maxIndex>-->
  </rollingPolicy>
  <!--设置日志滚动更新的触发策略-->
  <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
    <maxFileSize>5MB</maxFileSize>
  </triggeringPolicy>

  <encoder charset="UTF-8">
    <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
  </encoder>
</appender>

这样配置后,当error.log中的内容大于5MB时,就会归档压缩为一个类似error-1.log.zip的形式并清空error.log。注意:窗口大小是有默认值7的,也就是说默认情况下,最多出现error-7.log.zip,更新的归档会向后覆盖。

pattern

<appender>中的<pattern>用于指定日志的格式,格式模板配置包括文本颜色和占位符变量。

%yellow%blue等用于指定颜色,这里不多做介绍。注意%hightlight是比较特殊的,它代表高亮色,一般用于指定到%level变量上,使得不同的日志级别有不同的颜色。

%date{yyyy-MM-dd HH:mm:ss}配置日志的输出时间,大括号内为日志的格式化格式,%date也可以写作%d

%logger指定产生日志的类的全路径,我们可以使用类似%logger{5}的形式配置,5是一个路径缩写字符数的指导值,配置后输出以类似c.z.h.p.HikariPool形式缩写。

%thread为线程名。

%level为日志级别。

%msg为日志中具体输出的内容。

%n为换行符,因为不同操作系统可能使用不同的换行符,如果使用\n\r\n可能造成日志格式错乱,因此我们需要以%n换行。

此外还有类似%-5level的写法,-代表左对齐,5代表此处最小宽度为5。

logger

<logger>用于单独设置某个包或某个类的日志输出级别,以及它使用的<appender>。下面是一个例子:

<logger name="com.gacfox.demo.MainController" level="DEBUG" additivity="false">
  <appender-ref ref="stdout" />
</logger>
  • name指定了一个类全名或包名,日志配置会应用到对应类或包上
  • additivity表示子Logger是否继承根Logger配置:true表示继承,根Logger和子Logger配置的所有appender都会生效,false表示只有子Logger配置的appender生效。

root

<root><logger>相似,只不过它配置的是一个根Logger。

日志异步输出

默认情况下异步是以同步方式写入的,日志的IO操作本身也会消耗时间,因此同步方式记录日志在高并发系统中是无法满足要求的。Logback内置了异步日志输出组件,我们可以直接使用。

<appender name="async-file" class="ch.qos.logback.classic.AsyncAppender">
  <appender-ref ref="file" />
</appender>

Logback的AsyncAppender内部使用了默认大小为256的BlockingQueue队列实现异步功能。代码中,我们创建了一个名为async-file的appender组件,内部使用<appender-ref>标签关联了另一个写入文件的RollingFileAppender

映射诊断上下文(MDC)

Mapped Diagnostic Contexts(MDC)中文译为映射诊断上下文,不要被这个专业名词吓到,说白了就是把一个链路跟踪ID(traceId)写到日志文本里。我们知道Tomcat等应用容器都是通过多线程机制处理用户请求的,链路跟踪可以实现在微服务环境下记录请求的整个链路,而MDC可以将请求链路所有节点的日志串联起来。SLF4J提供了一个静态类MDC,我们可以调用MDC.put()方法向线程局部变量写入数据,然后配置日志输出显示该值。

这里我们直接看一个Spring工程的例子。

logback.xml

<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
        <encoder charset="UTF-8">
            <pattern>%yellow(%date) %highlight(%-5level) %cyan(%logger{5}@[%-4.30thread]) %cyan(%X{traceId}) - %msg%n</pattern>
        </encoder>
    </appender>

    <logger name="org.springframework" level="info" additivity="false">
        <appender-ref ref="stdout"/>
    </logger>

    <root level="debug">
        <appender-ref ref="stdout"/>
    </root>
</configuration>

logback.xml中,有一个比较特殊的配置%X{traceId},其中%X表示取MDC中的变量,traceId是我们放入MDC的键。

MdcInterceptor.java

package com.gacfox.interceptor;

import org.slf4j.MDC;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class MdcInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String traceId = request.getHeader("traceId");
        MDC.put("traceId", traceId);
        return true;
    }
}

上面代码是一个SpringMVC拦截器,这个拦截器配配置为了拦截所有请求,其代码逻辑非常简单,就是从HTTP请求头中取出traceId并放入MDC。此时我们如果在请求头中加入traceId,就可以在日志中看到其输出了。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。