SLF4J/Java 日志记录:是否可以自动添加日志参数?

SLF4J/Java logging: Is it possible to add log arguments automatically?

简介

我们在几个 Spring(引导)应用程序中结合使用 SLF4J 和 Logback,并且最近开始使用 logstash-logback-encoder 来实现结构化日志记录。由于我们还必须支持纯文本日志,我们想知道是否可以自动将参数附加到日志消息中,而无需使用 {} 标记将它们手动添加到消息中。

所需行为的示例

为了说明所需的行为,这就是我们所希望的:

log.info("My message", kv("arg1", "firstArgument"), kv("arg2", "secondArgument"))

导致以下所需输出,其中参数自动附加在消息末尾的括号中:

My message (arg1="firstArgument", arg2="secondArgument")

或者另一个在消息中既有显式参数又在末尾有参数的例子:

log.info("Status changed: {} => {}", v("from", "READY"), v("to", "UNAVAILABLE"), kv("service", "database"))

导致以下所需输出:

Status changed: READY => UNAVAILABLE (from="READY", to="UNAVAILABLE", service="database")

问题

SLF4J/Logback 可以吗?如果不知道,您是否知道其他日志记录框架或实现此目的的方法(Java)?

我不知道有任何日志框架可以让您执行此操作,但您可以轻松编写自己的日志框架。因为这真的只是一个简单的 API 扩展,因此,您需要复制的只是各种 log 消息。例如,这个 one-liner 会处理它:

public static class LoggingExtensions {
    @lombok.Value public static final class LogKeyValue {
        String key, value;
    }

    public static LogKeyValue kv(String key, Object value) {
        return new LogKeyValue(key, String.valueOf(value));
    }

    public static void info(Logger log, String message, Object... args) {
        int extra = 0;
        int len = args.length;

        // Last arg could be a throwable, leave that alone.
        if (len > 0 && args[len - 1] instanceof Throwable) len--;

        for (int i = len - 1; i >= 0; i--) {
            if (!(args[i] instanceof LogKeyValue)) break;
            extra++;
        }
        if (extra > 0) {
          StringBuilder sb = new StringBuilder(message.length() + 2 + (extra.size() - 1) * 2);
          sb.append(message).append("({}");
          for (int i = 1; i < extra; i++) sb.append(", {}");
          message = sb.append(")").toString();
        }
        log.info(message, args);
    }
}

此代码在消息末尾添加 ({}, {} {}),每个 'kv' 类型添加 1 个。请注意,大多数日志记录框架(包括 slf4j)都允许您在末尾添加 1 个异常,即使消息中没有匹配的 {},因此此方法需要您首先列出所有 {} 参数,然后是任何 kv 个参数,然后是 0 或 1 个可抛出的参数。

不过有一些注意事项:

  • 您必须更改所有代码才能调用这些实用程序方法。您可以使用静态导入使其看起来不错,但它确实会使您的代码不那么惯用,这是一个缺点。
  • 大多数日志框架都有大量的方法,因为可变参数会导致创建数组。在热点代码中,JDK 可能会使其足够高效,这无关紧要,但由于日志语句往往无处不在,否则你会遇到千刀万剐的情况。 不太可能日志框架调用大量方法来避免可变参数惩罚是一个明智的举动;通常,日志最终会存储在磁盘上,甚至会被 fsynced,这对性能的影响要大 许多 个数量级。但是,日志框架必须迎合所有场景,并且由于日志级别配置而最终完全被忽略的日志,在一个紧密的循环中,可以由于避免了 varargs 惩罚,可以看到一些性能改进。如果发现您的日志框架正在影响性能,您也可以尝试优化:您可以询问日志处理程序所要求的日志级别是否相关,如果不相关,则立即 return; 。然后您也可以跟随并创建这个 'explosion'。请参阅 slf4j,其中每个日志级别都有 10 个方法,许多其他框架甚至更多(在求助于 varargs 之前,它们有 1、2、3,有时甚至是 4 个参数的变体)。