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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 作者日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式,后续作者打算在团队内做一次Java日志的分享,本文是整理出的系列文章第三篇。

一、创建 Logger 实例


1.1 工厂函数

要使用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】节)


1.2 Lombok

无论大家对Lombok或褒或贬,但它已经是Java开发的必备依赖了,我个人是推荐使用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(...);


3.1 info 方法

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

image.png

可以看到,IDEA编辑器对Slf4j API的支持非常好,那些黄底的警告可以让我们马上知道这句日志记录有问题。


虽然使用字符串模板会略有性能损耗(比较[2]),但相比于它提供的可读性和便捷性,这个缺点是可以接受的。最终开发者传入的参数,会由日志实现层拼装,并根据配置输出最终结果(请参考下一篇【三、占位符】节)。


3.2 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就很有优势。


不过目前 IDEA 编辑器对流式API的支持还不太好,无法支持占位符与参数不匹配的情况:

image.png

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

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


七、后记

以上只是简单介绍了Slf4j的常用功能,如需进一步了解可以参考官方文档SLF4J user manual[6]


参考链接:

[1]https://projectlombok.org/features/log

[2]https://juejin.cn/post/6915015034565951501

[3]https://logback.qos.ch/apidocs/ch/qos/logback/classic/turbo/MarkerFilter.html

[4]https://www.aliyun.com/product/xtrace

[5]https://www.slf4j.org/manual.html#fluent

[6]https://www.slf4j.org/manual.htm



来源  |  阿里云开发者公众号

作者  |  尚左

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
22天前
|
JSON Java fastjson
Java日志通关(五) - 最佳实践
作者日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式,后续作者打算在团队内做一次Java日志的分享,本文是整理出的系列文章第五篇。
|
25天前
|
XML Java API
Java日志通关(四) - Logback 介绍
作者日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式,后续作者打算在团队内做一次Java日志的分享,本文是整理出的系列文章第四篇。
|
26天前
|
存储 监控 Java
Java日志通关(三) - Slf4j 介绍
作者日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式,后续作者打算在团队内做一次Java日志的分享,本文是整理出的系列文章第三篇。
|
27天前
|
JavaScript Java API
Java日志通关(二) - Slf4j+Logback 整合及排包
作者日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式,后续作者打算在团队内做一次Java日志的分享,本文是整理出的系列文章第二篇。
|
5天前
|
Java
日志框架log4j打印异常堆栈信息携带traceId,方便接口异常排查
日常项目运行日志,异常栈打印是不带traceId,导致排查问题查找异常栈很麻烦。
|
27天前
|
XML Java Maven
log4j 日志的简单使用
这篇文章介绍了Log4j日志框架的基本使用方法,包括在Maven项目中添加依赖、配置`log4j.properties`文件以及在代码中创建和使用Logger对象进行日志记录,但实际打印结果中日志级别没有颜色显示。
log4j 日志的简单使用
|
1月前
|
XML Java Maven
Spring5入门到实战------16、Spring5新功能 --整合日志框架(Log4j2)
这篇文章是Spring5框架的入门到实战教程,介绍了Spring5的新功能——整合日志框架Log4j2,包括Spring5对日志框架的通用封装、如何在项目中引入Log4j2、编写Log4j2的XML配置文件,并通过测试类展示了如何使用Log4j2进行日志记录。
Spring5入门到实战------16、Spring5新功能 --整合日志框架(Log4j2)
|
19天前
|
存储 消息中间件 监控
Java日志详解:日志级别,优先级、配置文件、常见日志管理系统ELK、日志收集分析
Java日志详解:日志级别,优先级、配置文件、常见日志管理系统、日志收集分析。日志级别从小到大的关系(优先级从低到高): ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF 低级别的会输出高级别的信息,高级别的不会输出低级别的信息
|
1月前
|
存储 运维 Java
SpringBoot使用log4j2将日志记录到文件及自定义数据库
通过上述步骤,你可以在Spring Boot应用中利用Log4j2将日志输出到文件和数据库中。这不仅促进了良好的日志管理实践,也为应用的监控和故障排查提供了强大的工具。强调一点,配置文件和代码的具体实现可能需要根据应用的实际需求和运行环境进行调优和修改,始终记住测试配置以确保一切运行正常。
250 0
|
2月前
|
Java 测试技术 Apache
《手把手教你》系列基础篇(八十六)-java+ selenium自动化测试-框架设计基础-Log4j实现日志输出(详解教程)
【7月更文挑战第4天】Apache Log4j 是一个广泛使用的 Java 日志框架,它允许开发者控制日志信息的输出目的地、格式和级别。Log4j 包含三个主要组件:Loggers(记录器)负责生成日志信息,Appenders(输出源)确定日志输出的位置(如控制台、文件、数据库等),而 Layouts(布局)则控制日志信息的格式。通过配置 Log4j,可以灵活地定制日志记录行为。
44 4