Logback 日志模块

Java生态中,记录日志有JDK自带的Logger,除此之外十分常用的第三方日志模块有Log4jLog4j2Logbackcommons-logging等。

Logback是一个功能丰富的日志库,而且性能不错,是SpringBoot默认集成的日志模块,实际上LogbackLog4j等许多日志库实现了SLF4J的API,我们代码中使用Logger等类时,你会发现引入的包都是SLF4J的,它们之间的关系就像接口和实现类一样,使用SLF4J的API而不是直接调用某个日志库的接口也是最为推荐的方式。

这里我们简单介绍一下Logback的使用。

添加Maven依赖

我们直接将slf4jlogback的依赖引入项目即可。

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.7.25</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-access</artifactId>
  <version>1.2.3</version>
</dependency>
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.2.3</version>
</dependency>

日志模块冲突的解决

实际开发中我们用到的某些库可能依赖一个特定的日志模块,这是相当糟糕的设计,比如ZooKeeper的客户端包就自行依赖了log4j(至少我下面用到的版本是这样的),而我们整个项目都是Logback的,我们不能让这些模块自己搞特殊,好在slf4j提供了bridge包能够解决这个问题。

以ZooKeeper为例,我们通过依赖分析,发现它自带了log4jslf4j的绑定包,我们把这几个依赖直接排除。

<dependency>
  <groupId>org.apache.zookeeper</groupId>
  <artifactId>zookeeper</artifactId>
  <version>3.4.10</version>
  <exclusions>
    <exclusion>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
    </exclusion>
    <exclusion>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
    </exclusion>
    <exclusion>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
    </exclusion>
  </exclusions>
</dependency>

然后引入bridge兼容包。

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

之后再手动引入slf4j-apilogback等依赖即可。

Logback简单使用

Logback有五个日志级别:严重性TRACE<DEBUG<INFO<WARN<ERROR

使用Logback非常简单,首先我们要获得Logger对象,然后在其上调用debug()error()等方法即可,和JDK内置的日志工具使用起来是一样的。

package com.gacfox;

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()打印到控制台的。日志框架中则写法例子如下

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,如果不存在任何配置文件,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进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap