Java日志通关(三) - Slf4j、Logback 介绍 介绍

一、Slf4j 详解

工厂函数

要使用Slf4j,需要先创建一个org.slf4j.Logger实例,可以使用它的工厂函数org.slf4j.LoggerFactory.getLogger(),参数可以是字符串或Class:

  • 如果是字符串,这个字符串会作为返回Logger实例的名字;
  • 如果是Class,会调用它的getName()获取Class的全路径,作为Logger实例的名字;
public class ExampleService {
    // 传 Class,一般都是传当前的 Class
    private static final Logger log = LoggerFactory.getLogger(ExampleService.class);
    // 上边那一行相当于:
    private static final Logger log = LoggerFactory.getLogger("com.example.service.ExampleService");

    // 你也可以指定任意字符串
    private static final Logger log = LoggerFactory.getLogger("service");
}

这个字符串格式的「实例名字」可以称之为LoggerName,用于在日志实现层区分如何打印日志(见下一篇【3.1 Conversion Word】节)

Lombok

Lombok也提供了针对各种日志系统的支持,比如你只需要@lombok.extern.slf4j.Slf4j注解就可以得到一个静态的log字段,不用再手动调用工厂函数。默认的LoggerName 即是被注解的Class;同时也支持字符串格式的topic字段指定LoggerName。


@Slf4j
public class ExampleService {
    // 注解 @Slf4j 会帮你生成下边这行代码
    // private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ExampleService.class);
}

@Slf4j(topic = "service")
public class ExampleService {
    // 注解 @Slf4j(topic = "service") 会帮你自动生成下边这行代码
    // private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger("service");
}

除了Slf4j,Lombok几乎支持目前市面上所有的日志方案,从接口到实现都没放过。具体明细可以参考Lombok的官方文档@Log (and friends)[1]。

日志级别

通过org.slf4j.event.Level我们可以看到一共有五个等级,按优先级从低到高依次为:

  • TRACE:一般用于记录调用链路,比如方法进入时打印xxx start;

  • DEBUG:个人觉得它和 trace 等级可以合并,如果一定要区分,可以用来打印方法的出入参;

  • INFO:默认级别,一般用于记录代码执行时的关键信息;

  • WARN:当代码执行遇到预期外场景,但它不影响后续执行时,可以使用;

  • ERROR:出现异常,以及代码无法兜底时使用;

多说一句,Logback额外还有两个级别ALL/OFF表示完全开启/关闭日志输出,我们记日志时并不涉及。

日志的实现层会决定哪个等级的日志可以输出,这也是我们打日志时需要区分等级的原因,在保证重要的日志不丢失的同时,仅在有需要时才打印用于Debug的日志。

@Slf4j
public class ExampleService {
    @Resource
    private RpcService rpcService;

    public String querySomething(String request) {
        // 使用 trace 标识这个方法调用情况
        log.trace("querySomething start");
        // 使用 debug 记录出入参
        log.debug("querySomething request={}", request);

        String response = null;
        try {
            RpcResult rpcResult = rpcService.call(a);
            if (rpcResult.isSuccess()) {
                response = rpcResult.getData();

                // 使用 info 标识重要节点
                log.info("querySomething rpcService.call succeed, request={}, rpcResult={}", request, rpcResult);
            } else {
                // 使用 warn 标识程序调用有预期外错误,但这个错误在可控范围内
                log.warn("querySomething rpcService.call failed, request={}, rpcResult={}", request, rpcResult);
            }
        } catch (Exception e) {
            // 使用 error 记录程序的异常信息
            log.error("querySomething rpcService.call abnormal, request={}, exception={}", request, e.getMessage(), e);
        }

        // 使用 debug 记录出入参
        log.debug("querySomething response={}", response);
        // 使用 trace 标识这个方法调用情况
        log.trace("querySomething end");

        return response;
    }
}

打印接口

通过org.slf4j.Logger我们可以看到有非常多的日志打印接口,不过定义的格式都类似,以info为例,一共有两大类:
public boolean info(…);

public boolean isInfoEnabled(…);

info 方法

这个方法有大量的重载,不过使用逻辑是一致的,为了便于说明,我们直接上图:

在这里插入图片描述

isInfoEnabled 方法

通过isInfoEnabled方法可以获取当前Logger实例是否开启了对应的日志级别,比如我们可能见过类似这样的代码:


if (log.isInfoEnabled()) {
    log.info(...)
}

但其实日志实现层本身就会判断当前Logger实例的输出等级,低于此等级的日志并不会输出,所以一般并不太需要这样的判断。但如果你的输出需要额外消耗资源,那么先判断一下会比较好,比如:


if (log.isInfoEnabled()) {
    // 有远程调用
    String resource = rpcService.call();
    log.info("resource={}", resource)

    // 要解析大对象
    Object result = ....; // 一个大对象
    log.info("result={}", JSON.toJSONString(result));
}

Marker

在前边介绍接口时,我们只提到了log.info()中填字符串模板及参数的情况,细心的朋友应该发现,还有一些接口多了一个org.slf4j.Marker类型的入参,比如:
log.info(Marker, …)

我们可以通过工厂函数创建 Marker 并使用,比如:

Marker marker = MarkerFactory.getMarker("foobar");
log.info(marker, "test a={}", 1);

这个 Marker 是一个标记,它会传递给日志实现层,由实现层决定 Marker 的处理方式,比如:

  • 将Marker通过%marker打印出来
  • 使用MarkerFilter[3]过滤出(或过滤掉)带有某个Marker的日志,比如把需要Sunfire监控的日志都过滤出来写到一个单独的日志文件

MDC

MDC的全称是Mapped Diagnostic Context,直译为映射调试上下文,说人话就是用来存储扩展字段的地方,而且它是线程安全的。比如OpenTelemetry[4]的traceId就会被存到MDC中(见下一篇【五、MDC 中的 traceId】节)。

而且MDC的使用也很简单,就像是一个Map<String, String>实例,常用的方法put/get/remove/clear都有,又到了举粟子🌰时间


// 和 Map<String, String> 相似的接口定义
MDC.put("key", "value");
String value = MDC.get("key");
MDC.remove("key");
MDC.clear();

// 获取 MDC 中的所有内容
Map<String, String> context = MDC.getCopyOfContextMap();

Fluent API (链式调用)

Fluent API也可以直译为「流式 API」, Slf4j从2.0.x开始支持[5],它很像Lombok中@Builder提供的能力,即通过链式调用分别设置各个属性,最后再调用.log()(就像调用.build()那样)完成整个调用。


Marker marker = MarkerFactory.getMarker("foobar");
Exception e = new RuntimeException();

// == 以下几个示例的最终效果是完全一致的 ==

// 这是传统的调用方式
log.info(market, "request a={}, b={}", 1, 2, e);

// Fluent API 例1
log.atInfo() // 表示这是 INFO 级别。你猜对了,还有 atTrace/atDebug/atWarn/atError
    .addMarker(marker)
    .log("request a={}, b={}", 1, 2, e); // 与传统 API 很像

// Fluent API 例2
log.atInfo()
    .addMarker(marker)
    .setCause(e)
    .setMessage("request a={}, b={}") // 传字符串模板
    .setMessage(() -> "request a={}, b={}") // setMessage 支持传入 Supplier
    .addArgument(1) // 添加与字符串模板中占们符所对应的值
    .addArgument(() -> 2) // addArgument 支持传入 Supplier
    .log(); // 大火收汁


// == addKeyValue 的输出格式依赖日志实现层的配置,默认格式与上边示例不同 ==

// Fluent API 例3
log.atInfo()
    .setMessage("request") // 注意这里没有占位符
    .setKeyValue("a", 1) // 通过 setKeyValue 添加关心的变量
    .setKeyValue("b", () -> 2) // value 支持传入 Supplier
    .log();
// 通过 setKeyValue 设置的值默认会放在 message 前边,比如上边这个例子,默认会输出:
// a=1 b=2 request

总结一下:

  • 所有add前缀的方法,都支持设置多个,比如- -addMarker/addArgument/addKeyValue。所以在Fluent API中是支持给一条日志添加多个Marker的,而传统API不可以。
  • 所有set前缀的方法,对应的值都只有一个,比如setMessage/setCause,虽然你可以多次调用,但只有最后一次会生效。

在上边的示例中传统API看起来更简洁。但如果日志中占位符很多,那用Fluent API,特别是使用其中的addKeyValue就很有优势。

在这里插入图片描述

顺便说一下,相比Slf4j,更晚推出的Log4j 2在传统API中也支持通过传入Supplier惰性求值,就像这样:

log.info("request a={}", () -> a);

二、Logback

2.1 一、配置入口

Logback支持XML、Groovy的配置方式,以XML来说,它会默认查找resources目录下的logback-test.xml(用于测试)/logback.xml文件。

而如果你使用的Spring Boot,那么你还可以使用logback-spring.xml文件进行配置。这两者的区别是:

  • logback-spring.xml是由 Spring Boot 找到,插入自己的上下文信息[1]并做进一步处理后再传递给Logback的,你可以在其中使用区分环境配置,也可以使用拿到Spring上下文信息(比如spring.application.name)。
  • logback.xml是由Logback自己找到的,自然不会有Spring Boot相关的能力。

2.2 配置文件介绍

下来我们以logback-spring.xml为例进行介绍。一个Logback配置文件主要有以下几个标签:

  • confinuration:最外层的父标签,其中有几个属性配置,但项目中较少使用,就不啰嗦了;

  • property:定义变量;

  • appender:负责日志输出(一般是写到文件),我们可以通过它设置输出方案;

  • logger:用来设置某个LoggerName的打印级别;

  • root:logger的兜底配置,从而我们不必配置每个LoggerName;

  • conversionRule:定义转换规则,参考【四、Java API】;

2.2.1 springProperty 和 property

前文提到<springProperty>用来插入Spring上下文,那就是 Logback 自己定义变量的标签。直接看示例:

<springProperty scope="context" name="APP_NAME" source="spring.application.name"/>

<property name="LOG_PATH" value="${user.home}/${APP_NAME}/logs"/>
<property name="APP_LOG_FILE" value="${LOG_PATH}/application.log"/>
<property name="APP_LOG_PATTERN"
          value="%date{yyyy-MM-dd HH:mm:ss.SSS}|%-5level|%X{trace_id}|%thread|%logger{20}|%message%n%exception"/>

我们首先用<springProperty>插入APP_NAME这个变量来表示应用名,随后用它拼出LOG_PATH变量。示例中还用到了${user.home}这个Logback内建支持的上下文变量[2]。APP_LOG_FILE是log文件路径;APP_LOG_PATTERN是日志格式(请参考【三、占位符】节)。

2.2.2 appender

这一节涉及到的知识点很多,但一码胜千言,先直接给出示例:


<appender name="APPLICATION" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${APP_LOG_FILE}</file>
    <encoder>
        <pattern>${APP_LOG_PATTERN}</pattern>
    </encoder>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <fileNamePattern>${APP_LOG_FILE}.%d{yyyy-MM-dd}.%i</fileNamePattern>
        <maxHistory>30</maxHistory>
        <maxFileSize>200MB</maxFileSize>
        <totalSizeCap>10GB</totalSizeCap>
    </rollingPolicy>
</appender>
<appender name="APPLICATION-async" class="ch.qos.logback.classic.AsyncAppender">
    <queueSize>256</queueSize>
    <discardingThreshold>0</discardingThreshold>
    <neverBlock>true</neverBlock>
    <appender-ref ref="APPLICATION"/>
</appender>

示例中涉及的变量在上一节已经提到,这里不啰嗦了。需要关注的是以下几个点:

  • ch.qos.logback.core.rolling.RollingFileAppender负责将日志滚动打印,避免单文件体积过大。具体的滚动策略都在中指定,各配置项都还算好理解。

  • ch.qos.logback.classic.AsyncAppender负责将日志异步打印,避免大量打印日志时阻塞线程。

除了上边这两个,还有如ch.qos.logback.core.ConsoleAppender是用来将日志输出到控制台,如果你用到了,建议参考【下一篇九、不要将日志输出至Console】节。
更多Appender和RollingPolicy相关的介绍,可以参考官方文档Chapter 4: Appenders[3]。

2.2.4 logger和root

<logger> :用来设置某个LoggerName的打印级别。比如:

<logger level="INFO" additivity="false" name="com.foo.bar">
    <appender-ref ref="APPLICATION-async"/>
</logger>
<root level="INFO">
    <appender-ref ref="APPLICATION-async"/>
</root>

<logger>:某模块路径的打印级别配置

<logger name="com.javastarboy.worknotice" level="info" />

上面的配置指定所有LoggerName为com.foo.bar的日志以INFO级别进行打印(TRACE和DEBUG级别将不会输出),此配置绑定的输出器(appender)为APPLICATION-async。

其中LoggerName会以 . 为分隔符逐级向上匹配,比如实际LoggerName为com.foo.bar.service.ExampleService,那么它的查找过程依次为:
com.foo.bar.service.ExampleService
com.foo.bar.service
com.foo.bar(此时命中了我们示例中的<logger>,另外因为配置了additivity=“false” 所以停止继续向下查找)
com.foo
com
<root>

而 <root>就是兜底配置了,当LoggerName没匹配到任何一项 时,就会使用 <root>,所以它是没有additivity和name属性的。
一般实际业务场景中,所有<logger>都建议加上additivity=“false” ,否则日志就会因查找到多个<logger>(或<root>)而打印多份。

2.2.5 日志异步打印

ch.qos.logback.classic.AsyncAppender是logback的一个异步打印实现类,指明使用该类打印日志即开启了异步打印功能。

异步打印原理
https://blog.csdn.net/qq496013218/article/details/76603779

当Logging Event进入AsyncAppender后,AsyncAppender会调用appender方法,append方法中在将event填入Buffer(这里选用的数据结构为BlockingQueue)中前,会先判断当前buffer的容量以及丢弃日志特性是否开启,当消费能力不如生产能力时,AsyncAppender会超出Buffer容量的Logging Event的级别,进行丢弃,作为消费速度一旦跟不上生产速度,中转buffer的溢出处理的一种方案。AsyncAppender有个线程类Worker,它是一个简单的线程类,是AsyncAppender的后台线程,所要做的工作是:从buffer中取出event交给对应的appender进行后面的日志推送。

从上面的描述中可以看出,AsyncAppender并不处理日志,只是将日志缓冲到一个BlockingQueue里面去,并在内部创建一个工作线程从队列头部获取日志,之后将获取的日志循环记录到附加的其他appender上去,从而达到不阻塞主线程的效果。因此AsynAppender仅仅充当事件转发器,必须引用另一个appender来做事。

在使用AsyncAppender的时候,有些选项还是要注意的。由于使用了BlockingQueue来缓存日志,因此就会出现队列满的情况。正如上面原理中所说的,在这种情况下,AsyncAppender会做出一些处理:默认情况下,如果队列80%已满,AsyncAppender将丢弃TRACE、DEBUG和INFO级别的event,从这点就可以看出,该策略有一个惊人的对event丢失的代价性能的影响。另外其他的一些选项信息,也会对性能产生影响

在这里插入图片描述
默认情况下,event queue配置最大容量为256个events。如果队列被填满,应用程序线程被阻止记录新的events,直到工作线程有机会来转发一个或多个events。因此队列深度需要根据业务场景进行相应的测试,做出相应的更改,以达到较好的性能。

<appender name="FILE" class= "ch.qos.logback.core.rolling.RollingFileAppender">  
            <!-- 按天来回滚,如果需要按小时来回滚,则设置为{yyyy-MM-dd_HH} -->  
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">  
                 <fileNamePattern>/opt/log/test.%d{yyyy-MM-dd}.log</fileNamePattern>  
                 <!-- 如果按天来回滚,则最大保存时间为1天,1天之前的都将被清理掉 -->  
                 <maxHistory>30</maxHistory>  
            <!-- 日志输出格式 -->  
            <layout class="ch.qos.logback.classic.PatternLayout">  
                 <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%msg%n</Pattern>  
            </layout>  
</appender>  
     <!-- 异步输出 -->  
     <appender name ="ASYNC" class= "ch.qos.logback.classic.AsyncAppender">  
            <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->  
            <discardingThreshold >0</discardingThreshold>  
            <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->  
            <queueSize>512</queueSize>  
            <!-- 添加附加的appender,最多只能添加一个 -->  
         <appender-ref ref ="FILE"/>  
     </appender>  
       
     <root level ="trace">  
            <appender-ref ref ="ASYNC"/>  
     </root>  

示例二

 <appender name="rollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/${application.name}.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] [traceId=%X{traceId} spanId=%X{spanId}] %-5level %logger{36} - %msg%n
            </pattern>
        </encoder>
    </appender>


    <appender name="AsyncAppender" class="ch.qos.logback.classic.AsyncAppender">
        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
        <discardingThreshold >0</discardingThreshold>
        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
        <queueSize>512</queueSize>
        <!-- 添加附加的appender,最多只能添加一个 -->
        <appender-ref ref ="rollingFile"/>
    </appender>

在这里插入图片描述

2.2.5 springProfile

Spring还提供了<springProfile>标签,用来根据Spring Profiles[4](即 spring.profiles.active的值)动态调整日志信息,比如我们希望线上环境使用INFO级别,而预发、日常使用TRACE级别:


<springProfile name="production">
    <root level="INFO">
        <appender-ref ref="APPLICATION-async"/>
    </root>
</springProfile>
<springProfile name="staging,testing">
    <root level="TRACE">
        <appender-ref ref="APPLICATION-async"/>
    </root>
</springProfile>

三、占位符

3.1 Conversion Word

Logback提供了大量有用的占位符给大家使用,官方文档在Conversion Word[5]。
比如一些常用的占位符(大部分占位符都有缩写形式,比如%logger可以简写为%c,我不在这里一一列举了,具体可以查看上边给出的官方文档):

占位符说明
%logger输出LoggerName(参考【第三篇:1.1 工厂函数】节)
%message输出你实际要打印的日志信息(参考【第三篇:3.1 info方法】节)
%exception输出异常堆栈,对应通过Slf4j传入的异常(参考【第三篇:3.1 info方法】节)
%level输出日志级别,即TRACE/DEBUG/INFO/WARN/ERROR/FATAL。注意这里有别于Slf4j(参考【第三篇:二、日志级别】),多了个FATAL级别,这是为了适配Log4j而存在的。
%xException输出异常堆栈,同时包含每行堆栈所归属的JAR包名
%marker输出通过Marker(参考【第三篇:四、Marker】节)传入的字符串
%mdc输出MDC(参考【第三篇:五、MDC】节)中对应的值
%kvp输出通过addKeyValue(参考【第三篇:六、Fluent API (链式调用)】节)传入的KV对
%date输出时间,可以添加符合ISO 8601[6]的参数指定输出格式,比如我们常用的yyyy-MM-dd HH:mm:ss
%thread输出打印日志方法所在的线程名
%n输出一个换行符
%nopex忽略传入的堆栈,不打印。请参考【第五篇:七、将堆栈合并为一行】节Logback会判断你的日志pattern,如果没有输出堆栈,会默认追加%exception,以保证传入的异常信息不会丢。这个占位符就是明确要求不做追加。

另外,针对%logger额外做一下补充。%logger的参数中可以传一个正整数,用于指定输出长度,当LoggerName长度超过限制时,Logback会以 . 为分隔智能缩短。假设LoggerName是com.example.foo.bar.ExampleService(这个字符串长度为34),那么:

配置输出结果说明
[%logger][com.example.foo.bar.ExampleService]原样输出
[%logger{32}][c.example.foo.bar.ExampleService]实际长度32,与限制值一致
[%logger{30}][c.e.foo.bar.ExampleService]实际长度26,比限制值小。因为每一级 package要么保留原样,要么只取第一个字符
[%logger{10}][c.e.f.b.ExampleService]实际长度20,比限制值大。因为每一级 package至少会保留一个字符,且最后一级不会被缩短
[%logger{0}][ExampleService]0比较特殊,表示只保留最后一级,且不会被缩短

3.2 Format modifiers

从前边我们可以看到,占位符的基本使用方式是:%占位符{参数},但其实还有一个用于控制格式的可选配置,可以放在%与占位符之间,叫作Format modifiers[7]。一个完整的格式配置包含五个部分,比如:-10.-20,我们分别解释:

  • -:第一个-表示不足最小长度时在右侧填充空格,即输出内容左对齐(默认是在左侧填充空格,即右对齐);

  • 10:第一个数字表示输出最小长度,不足的补空格;

  • .:与后边两项配置的分隔符,本身没有含义;

  • -:第二个-表示超过最大长度时先裁剪右侧,即保留左侧字符(默认先裁剪左侧,即保留右侧字符);

  • 20:第二个数字表示输出最大长度,超出部分会做裁剪;

举个几个例子:

配置文本输出结果说明
[%5level]INFO[ INFO]最小5个字符,右对齐
[%-5level]INFO[INFO ]最小5个字符,左对齐
[%.-1level]INFO[I]最大1个字符,优先保留左侧字符
[%-5,-10logger]com.foo.bar.Service[com.foo.ba]最大10个字符,优先保留左侧字符
[%-5,10logger]com.foo.bar.Service[ar.Service]最大10个字符,优先保留右侧字符

四、日志示例

4.1 日志配置实例

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="2 seconds">
    <timestamp key="TIMESTAMP" datePattern="yyyy-MM-dd"/>
    <springProperty scope="context" name="application.name" source="spring.application.name" defaultValue="xxxx-xxxx"/>
    <property name="LOGPATH" value="log"/>
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] [traceId=%X{traceId} spanId=%X{spanId}] %-5level %logger{36} - %msg%n
            </pattern>
        </encoder>
    </appender>

    <appender name="rollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/${application.name}.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] [traceId=%X{traceId} spanId=%X{spanId}] %-5level %logger{36} - %msg%n
            </pattern>
        </encoder>
    </appender>

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

        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

            <fileNamePattern>logs/${application.name}_error.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] [traceId=%X{traceId} spanId=%X{spanId}] %-5level %logger{36} - %msg%n
            </pattern>
        </encoder>

    </appender>

    <!-- project default level -->
    <logger name="avicit" level="info"/>

    <root level="info">
        <appender-ref ref="console"/>
        <appender-ref ref="rollingFile"/>
        <appender-ref ref="errorFile"/>
    </root>
</configuration>


4.2 logback 常用配置详解 <appender>

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

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

4.2.2 .RollingFileAppender

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

<file>:被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。

<append>:如果是 true,日志被追加到文件结尾,如果是 false,清空现存文件,默认是true。
<encoder>:对记录事件进行格式化。(具体参数稍后讲解 )
<rollingPolicy>:当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名。
<triggeringPolicy >: 告知 RollingFileAppender 合适激活滚动。

<prudent>:当为true时,不支持FixedWindowRollingPolicy。支持TimeBasedRollingPolicy,但是有两个限制,1不支持也不允许文件压缩,2不能设置file属性,必须留空。

<appender name="rollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/${application.name}.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] [traceId=%X{traceId} spanId=%X{spanId}] %-5level %logger{36} - %msg%n
            </pattern>
        </encoder>
    </appender>
4.2.2.1 rollingPolicy 日志记录器的滚动策略
TimeBasedRollingPolicy 最常用的滚动策略

最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。有以下子节点:

  • <fileNamePattern>:
    必要节点,包含文件名及“%d”转换符, “%d”可以包含一个java.text.SimpleDateFormat指定的时间格式,如:%d{yyyy-MM}。如果直接使用 %d,默认格式是 yyyy-MM-dd。RollingFileAppender 的file字节点可有可无,通过设置file,可以为活动文件和归档文件指定不同位置,当前日志总是记录到file指定的文件(活动文件),活动文件的名字不会改变;如果没设置file,活动文件的名字会根据fileNamePattern 的值,每隔一段时间改变一次。“/”或者“\”会被当做目录分隔符。

  • <maxHistory>:

可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每个月滚动,且是6,则只保存最近6个月的文件,删除之前的旧文件。注意,删除旧文件是,那些为了归档而创建的目录也会被删除。

例如:每天生成一个日志文件,保存30天的日志文件。

<configuration>   
  <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">   
      
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">   
      <fileNamePattern>logFile.%d{yyyy-MM-dd}.log</fileNamePattern>   
      <maxHistory>30</maxHistory>    
    </rollingPolicy>   
   
    <encoder>   
      <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>   
    </encoder>   
  </appender>    
   
  <root level="DEBUG">   
    <appender-ref ref="FILE" />   
  </root>   
</configuration>  

MaxHistory 属性可以按“文件数量、小时、天、月、年”等策略实现文件保留。但是很多人都遇到过此配置不生效问题,网上都只介绍了 cleanHistoryOnStart ,而我是另一种情况,这里汇总一下。

<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<contextName>logback</contextName>
<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
<property name="log.path" value="../logs/worknotice"/>

<!-- 时间滚动输出 level为 INFO 日志 -->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!-- 正在记录的日志文件的路径及文件名 -->
    <file>${log.path}/log_info.log</file>
    <!--日志文件输出格式-->
    <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>
    <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <!-- 每天日志归档路径以及格式,编码器 -->
        <fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
        <!--每个文件最多100MB-->
        <maxFileSize>100MB</maxFileSize>
        <!--日志文件保留天数-->
        <maxHistory>15</maxHistory>
        <!--每个文件最多100MB,保留15天的历史记录,但最多20GB-->
        <totalSizeCap>20GB</totalSizeCap>
        <!--重启清理日志文件-->
        <cleanHistoryOnStart>true</cleanHistoryOnStart>
    </rollingPolicy>
    <!-- 此日志文件只记录info级别的 -->
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
        <level>info</level>
        <onMatch>ACCEPT</onMatch>
        <onMismatch>DENY</onMismatch>
    </filter>
</appender>

参考:
https://blog.csdn.net/u012723183/article/details/107685109

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

有以下子节点:

  • <minIndex>:窗口索引最小值

  • <maxIndex>:窗口索引最大值,当用户指定的窗口过大时,会自动将窗口设置为12。

  • <fileNamePattern >:
    必须包含“%i”例如,假设最小值和最大值分别为1和2,命名模式为 mylog%i.log,会产生归档文件mylog1.log和mylog2.log。还可以指定文件压缩选项,例如,mylog%i.log.gz 或者 没有log%i.log.zip

4.2.2.2 triggeringPolicy
SizeBasedTriggeringPolicy

**: 查看当前活动文件的大小,如果超过指定大小会告知RollingFileAppender 触发当前活动文件滚动。只有一个节点:

  • <maxFileSize>:这是活动文件的大小,默认值是10MB。

    例如:按照固定窗口模式生成日志文件,当文件大于20MB时,生成新的日志文件。窗口大小是1到3,当保存了3个归档文件后,将覆盖最早的日志。

<configuration>   
  <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">   
    <file>test.log</file>   
   
    <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">   
      <fileNamePattern>tests.%i.log.zip</fileNamePattern>   
      <minIndex>1</minIndex>   
      <maxIndex>3</maxIndex>   
    </rollingPolicy>   
   
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">   
      <maxFileSize>5MB</maxFileSize>   
    </triggeringPolicy>   
    <encoder>   
      <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>   
    </encoder>   
  </appender>   
           
  <root level="DEBUG">   
    <appender-ref ref="FILE" />   
  </root>   
</configuration>  

另外还有SocketAppender、SMTPAppender、DBAppender、SyslogAppender、SiftingAppender,并不常用,这些就不在这里讲解了,大家可以参考官方文档。当然大家可以编写自己的Appender。

4.2.3 filter:

过滤此日志文件只记录xx级别的日志

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

        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

            <fileNamePattern>logs/${application.name}_error.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] [traceId=%X{traceId} spanId=%X{spanId}] %-5level %logger{36} - %msg%n
            </pattern>
        </encoder>

    </appender>

4.2.3 <encoder>:

负责两件事,一是把日志信息转换成字节数组,二是把字节数组写入到输出流。

目前PatternLayoutEncoder 是唯一有用的且默认的encoder ,有一个<pattern>节点,用来设置日志的输入格式。使用“%”加“转换符”方式,如果要输出“%”,则必须用“\”对“%”进行转义。

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

<pattern>里面的转换符说明:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
格式修饰符,与转换符共同使用:

可选的格式修饰符位于“%”和转换符之间。
第一个可选修饰符是左对齐 标志,符号是减号“-”;接着是可选的最小宽度 修饰符,用十进制数表示。如果字符小于最小宽度,则左填充或右填充,默认是左填充(即右对齐),填充符为空格。如果字符大于最小宽度,字符永远不会被截断。最大宽度 修饰符,符号是点号"."后面加十进制数。如果字符大于最大宽度,则从前面截断。点符号“.”后面加减号“-”在加数字,表示从尾部截断。

例如:%-4relative 表示,将输出从程序启动到创建日志记录的时间 进行左对齐 且最小宽度为4。

五、Java API

除了使用XML配置文件外,Logback还提供了大量的Java API[8]以支持更复杂的业务诉求。我们通过三个非常实用的场景来简单介绍一下。

  1. 场景一:使用log.info(“obj={}”, obj) 时,如何将obj统一转JSON String后输出;
  2. 场景二:日志中涉及到的手机号、身份证号,如何脱敏后再记录日志;
  3. 场景三:Logback配置基于XML,如何不改代码不发布,也可以动态修改日志级别;

其中前两个问题都可以通过MessageConverter[9]实现,因为篇幅原因,具体介绍可关注后续文章。
第三个问题可以借助LoggerContext[10]及Logger[11],同样因为篇幅原因,具体介绍可关注后续文章。

六、MDC 中的 traceId

单独拿出一节讲这个,是因为我发现有很多同学会手动记录traceId,比如:

log.info("traceId={}, blah blah blah", Span.current().getSpanContext().getTraceId());

其实OpenTelemetry[12]已经自动将traceId加到了MDC,对应的Key是trace_id,使用%mdc{trace_id}(参考【第三篇五、MDC】节)即可打印出traceId。比如我们在【2.1 springProperty和property】一节的示例中,就使用了这个Key。

七、后记

以上只是简单介绍了Logback的常用功能,如需进一步了解可以参考官方文档Logback documentation[13]。特别是其中很多例子是结合Slf4j一起介绍的,非常易懂。

  • 14
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
log4j-to-slf4j 和 jul-to-slf4j 都是用于将不同日志框架(log4j和JUL)的日志转发到slf4j的桥接器。它们的作用是在项目中统一使用slf4j接口进行日志记录,而不需要直接使用特定的日志框架。简单来说,它们是用来解决日志框架的兼容性问题的。 引用提到了一个错误信息,即log4j-slf4j-impl 不能与log4j-to-slf4j 同时存在。这是因为log4j-slf4j-impl是log4j框架的一个实现,而log4j-to-slf4j是将log4j框架转发到slf4j的桥接器。因此,当同时存在这两个包时会造成冲突。 综上所述,log4j-to-slf4j 和 jul-to-slf4j都是用于桥接不同日志框架到slf4j的工具,用于统一日志记录接口。在使用过程中需要注意避免与其他框架的冲突,比如log4j-slf4j-impl。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [log4j-slf4j-impl cannot be present with log4j-to-slf4j](https://blog.csdn.net/Master_Shifu_/article/details/125925944)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [slf4j log4j log4j-over-slf4j self-log4j12](https://blog.csdn.net/song854601134/article/details/130624626)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值