Contents

Flogger 简介

 1. 概述

在本教程中,我们将讨论Flogger 框架,这是 Google 为 Java 设计的流畅的日志记录 API。

2. 为什么要使用 Flogger?

对于目前市场上所有的日志框架,比如 Log4j 和 Logback,为什么我们还需要另一个日志框架?

事实证明,与其他框架相比,Flogger 有几个优势——让我们来看看。

2.1. 可读性

Flogger API 的流畅特性大大提高了它的可读性。

让我们看一个示例,我们希望每十次迭代记录一条消息。

使用传统的日志框架,我们会看到如下内容:

int i = 0;
// ...
if (i % 10 == 0) {
    logger.info("This log shows every 10 iterations");
    i++;
}

但是现在,有了 Flogger,上面的内容可以简化为:

logger.atInfo().every(10).log("This log shows every 10 iterations");

虽然有人会争辩说,Flogger 版本的 logger 语句看起来比传统版本更冗长,但它确实允许更强大的功能,并最终导致更具可读性和表达性的日志语句

2.2. 表现

只要我们避免在记录的对象上调用toString,记录对象就会得到优化:

User user = new User();
logger.atInfo().log("The user is: %s", user);

如果我们记录,如上所示,后端有机会优化记录。另一方面,如果我们直接调用toString,或者连接字符串,那么这个机会就失去了:

logger.atInfo().log("Ths user is: %s", user.toString());
logger.atInfo().log("Ths user is: %s" + user);

2.3. 可扩展性

Flogger 框架已经涵盖了我们期望从日志框架中获得的大部分基本功能。

但是,在某些情况下,我们需要添加功能。在这些情况下,可以扩展 API。

**目前,这需要一个单独的支持类。**例如,我们可以通过编写UserLogger类来扩展 Flogger API:

logger.at(INFO).forUserId(id).withUsername(username).log("Message: %s", param);

在我们想要一致地格式化消息的情况下,这可能很有用。然后,  UserLogger将为自定义方法 *forUserId(String id)*和 withUsername(String username) 提供实现。

为此,UserLogger必须扩展AbstractLogger类并提供 API 的实现。如果我们看一下FluentLogger,它只是一个没有其他方法的记录器,因此我们可以从按原样复制这个类开始,然后通过向它添加方法在此基础上构建。

2.4. 效率

传统框架广泛使用可变参数。这些方法需要在调用方法之前分配和填充一个新的Object[] 。此外,传入的任何基本类型都必须自动装箱。

**这一切都会在调用站点产生额外的字节码和延迟。如果日志语句实际上没有启用,**那就特别不幸了。在经常出现在循环中的调试级别日志中,成本变得更加明显。Flogger 通过完全避免可变参数来消除这些成本。

**Flogger 通过使用流畅的调用链来解决这个问题,从该调用链可以构建日志语句。**这允许框架仅对log方法进行少量覆盖 ,从而能够避免诸如可变参数和自动装箱之类的事情。这意味着 API 可以适应各种新功能而不会出现组合爆炸式增长。

一个典型的日志框架会有这些方法:

level(String, Object)
level(String, Object...)

其中level可以是大约七个日志级别名称之一(例如,*severe *的),以及具有接受附加日志级别的规范日志方法:

log(Level, Object...)

除此之外,通常还有一些方法的变体采用与日志语句关联的原因(Throwable实例):

level(Throwable, String, Object)
level(Throwable, String, Object...)

很明显,API 将三个关注点耦合到一个方法调用中:

  1. 它正在尝试指定日志级别(方法选择)
  2. 尝试将元数据附加到日志语句(Throwable原因)
  3. 此外,指定日志消息和参数。

这种方法迅速增加了满足这些独立关注点所需的不同日志记录方法的数量。

我们现在可以看到为什么在链中拥有两种方法很重要:

logger.atInfo().withCause(e).log("Message: %s", arg);

现在让我们看看如何在我们的代码库中使用它。

3. 依赖关系

设置 Flogger 非常简单。我们只需要将flogger 和 flogger-system-backend 添加到我们的pom 中:

<dependencies>
    <dependency>
        <groupId>com.google.flogger</groupId>
        <artifactId>flogger</artifactId>
        <version>0.4</version>
    </dependency>
    <dependency>
        <groupId>com.google.flogger</groupId>
        <artifactId>flogger-system-backend</artifactId>
        <version>0.4</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>

设置了这些依赖项后,我们现在可以继续探索我们可以使用的 API。

4. 探索 Fluent API

首先,让我们为记录器声明一个static实例:

private static final FluentLogger logger = FluentLogger.forEnclosingClass();

现在我们可以开始记录了。我们将从简单的事情开始:

int result = 45 / 3;
logger.atInfo().log("The result is %d", result);

日志消息可以使用任何 Java 的printf格式说明符,例如*%s、%d%016x*。

4.1. 避免在日志站点工作

Flogger 创建者建议我们避免在日志站点上工作。

假设我们有以下长时间运行的方法来总结组件的当前状态:

public static String collectSummaries() {
    longRunningProcess();
    int items = 110;
    int s = 30;
    return String.format("%d seconds elapsed so far. %d items pending processing", s, items);
}

在我们的日志语句中直接调用collectSummaries是很诱人的:

logger.atFine().log("stats=%s", collectSummaries());

不管配置的日志级别或速率限制如何,现在每次都会调用collectSummaries方法。

使禁用日志语句的成本几乎免费是日志框架的核心。反过来,这意味着它们中的更多可以完整地保留在代码中而不会受到伤害。像我们刚才那样编写日志语句会带走这个优势。

相反,我们应该使用 LazyArgs.lazy方法

logger.atFine().log("stats=%s", LazyArgs.lazy(() -> collectSummaries()));

现在,几乎没有在日志站点上完成任何工作——只是为 lambda 表达式创建实例。如果 Flogger 打算实际记录消息,它只会评估这个 lambda。

虽然允许使用 isEnabled保护日志语句:

if (logger.atFine().isEnabled()) {
    logger.atFine().log("summaries=%s", collectSummaries());
}

这不是必需的,我们应该避免它,因为 Flogger 会为我们进行这些检查。这种方法也只按级别保护日志语句,对限速日志语句没有帮助。

4.2. 处理异常

异常情况如何,我们如何处理它们?

好吧,Flogger 带有一个withStackTrace方法,我们可以使用它来记录Throwable实例:

try {
    int result = 45 / 0;
} catch (RuntimeException re) {
    logger.atInfo().withStackTrace(StackSize.FULL).withCause(re).log("Message");
}

其中withStackTrace将具有常量值SMALL、MEDIUM、LARGEFULLStackSize枚举作为参数。withStackTrace()生成的堆栈跟踪将在默认的java.util.logging后端显示为LogSiteStackTrace异常。不过,其他后端可能会选择以不同方式处理此问题。

4.3. 日志记录配置和级别

到目前为止,我们在大多数示例中都使用了 logger.atInfo ,但 Flogger 确实支持许多其他级别。我们将看看这些,但首先,让我们介绍如何配置日志记录选项。

要配置日志记录,我们使用 LoggerConfig类。

例如,当我们要将日志记录级别设置为 FINE时:

LoggerConfig.of(logger).setLevel(Level.FINE);

Flogger 支持各种日志级别:

logger.atInfo().log("Info Message");
logger.atWarning().log("Warning Message");
logger.atSevere().log("Severe Message");
logger.atFine().log("Fine Message");
logger.atFiner().log("Finer Message");
logger.atFinest().log("Finest Message");
logger.atConfig().log("Config Message");

4.4. 速率限制

限速问题如何?我们如何处理不想记录每次迭代的情况?

*Flogger 使用*every(int n)方法来拯救我们

IntStream.range(0, 100).forEach(value -> {
    logger.atInfo().every(40).log("This log shows every 40 iterations => %d", value);
});

当我们运行上面的代码时,我们得到以下输出:

Sep 18, 2019 5:04:02 PM com.blogdemo.flogger.FloggerUnitTest lambda$givenAnInterval_shouldLogAfterEveryTInterval$0
INFO: This log shows every 40 iterations => 0 [CONTEXT ratelimit_count=40 ]
Sep 18, 2019 5:04:02 PM com.blogdemo.flogger.FloggerUnitTest lambda$givenAnInterval_shouldLogAfterEveryTInterval$0
INFO: This log shows every 40 iterations => 40 [CONTEXT ratelimit_count=40 ]
Sep 18, 2019 5:04:02 PM com.blogdemo.flogger.FloggerUnitTest lambda$givenAnInterval_shouldLogAfterEveryTInterval$0
INFO: This log shows every 40 iterations => 80 [CONTEXT ratelimit_count=40 ]

如果我们想每 10 秒记录一次呢?然后,我们可以使用atMostEvery(int n, TimeUnit unit)

IntStream.range(0, 1_000_0000).forEach(value -> {
    logger.atInfo().atMostEvery(10, TimeUnit.SECONDS).log("This log shows [every 10 seconds] => %d", value);
});

有了这个,结果现在变成了:

Sep 18, 2019 5:08:06 PM com.blogdemo.flogger.FloggerUnitTest lambda$givenATimeInterval_shouldLogAfterEveryTimeInterval$1
INFO: This log shows [every 10 seconds] => 0 [CONTEXT ratelimit_period="10 SECONDS" ]
Sep 18, 2019 5:08:16 PM com.blogdemo.flogger.FloggerUnitTest lambda$givenATimeInterval_shouldLogAfterEveryTimeInterval$1
INFO: This log shows [every 10 seconds] => 3545373 [CONTEXT ratelimit_period="10 SECONDS [skipped: 3545372]" ]
Sep 18, 2019 5:08:26 PM com.blogdemo.flogger.FloggerUnitTest lambda$givenATimeInterval_shouldLogAfterEveryTimeInterval$1
INFO: This log shows [every 10 seconds] => 7236301 [CONTEXT ratelimit_period="10 SECONDS [skipped: 3690927]" ]

5. 将 Flogger 与其他后端一起使用

那么,如果我们想**将 Flogger 添加到已经使用 Slf4jLog4j **的现有应用程序中 怎么办?在我们想要利用现有配置的情况下,这可能很有用。正如我们将看到的,Flogger 支持多个后端。

5.1. Flogger 与 Slf4j

配置 Slf4j 后端很简单。首先,我们需要将flogger-slf4j-backend 依赖添加到我们的pom中:

<dependency>
    <groupId>com.google.flogger</groupId>
    <artifactId>flogger-slf4j-backend</artifactId>
    <version>0.4</version>
</dependency>

接下来,我们需要告诉 Flogger 我们想使用与默认后端不同的后端。我们通过系统属性注册一个 Flogger 工厂来做到这一点:

System.setProperty(
  "flogger.backend_factory", "com.google.common.flogger.backend.slf4j.Slf4jBackendFactory#getInstance");

现在我们的应用程序将使用现有配置。

5.2. Flogger 与 Log4j

我们遵循类似的步骤来配置 Log4j 后端。让我们将flogger-log4j-backend 依赖添加到我们的pom中:

<dependency>
    <groupId>com.google.flogger</groupId>
    <artifactId>flogger-log4j-backend</artifactId>
    <version>0.4</version>
    <exclusions>
        <exclusion>
            <groupId>com.sun.jmx</groupId>
            <artifactId>jmxri</artifactId>
        </exclusion>
        <exclusion>
            <groupId>com.sun.jdmk</groupId>
            <artifactId>jmxtools</artifactId>
        </exclusion>
        <exclusion>
            <groupId>javax.jms</groupId>
            <artifactId>jms</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>apache-log4j-extras</artifactId>
    <version>1.2.17</version>
</dependency>

我们还需要为 Log4j 注册一个 Flogger 后端工厂:

System.setProperty(
  "flogger.backend_factory", "com.google.common.flogger.backend.log4j.Log4jBackendFactory#getInstance");

就是这样,我们的应用程序现在设置为使用现有的 Log4j 配置!