java - 使用可选参数记录

标签 java log4j log4j2 logback slf4j

我有方法可以在其中添加特定的日志记录:

@Slf4j
@Service
public class SomethingService {

    public void doSomething(Something data, String comment, Integer limit) {
        Long id = saveSomethingToDatabase(data, comment);
        boolean sentNotification = doSomething(id);
        // ...

        // Log what you done.
        // Variables that always have important data: data.getName(), id
        // Variables that are optional: sentNotification, comment, limit 
        // (optional means they aren't mandatory, rarely contains essential data, often null, false or empty string).
    }
}

我可以简单地记录所有:

log.info("Done something '{}' and saved (id {}, sentNotification={}) with comment '{}' and limit {}",
                something.getName(), id, sentNotification, comment, limit);
// Done something 'Name of data' and saved (id 23, sentNotification=true) with comment 'Comment about something' and limit 2

但大多数时候大部分参数都是无关紧要的。通过以上,我得到如下日志:

// Done something 'Name of data' and saved (id 23, sentNotification=false) with comment 'null' and limit null

这使得日志难以阅读、冗长且不必要地复杂(在大多数情况下其他参数不存在)。

我想处理所有情况,只保留必要的数据。示例:

// Done something 'Name of data' and saved (id 23)
// Done something 'Name of data' and saved (id 23) with comment 'Comment about something'
// Done something 'Name of data' and saved (id 23) with limit 2
// Done something 'Name of data' and saved (id 23) with comment 'Comment about something' and limit 2
// Done something 'Name of data' and saved (id 23, sent notification)
// Done something 'Name of data' and saved (id 23, sent notification) with limit 2
// Done something 'Name of data' and saved (id 23, sent notification) with comment 'Comment about something'
// Done something 'Name of data' and saved (id 23, sent notification) with comment 'Comment about something' and limit 2

我可以手动编码:

String notificationMessage = sentNotification ? ", sent notification" : "";
String commentMessage = comment != null ? String.format(" with comment '%s'", comment) : "";
String limitMessage = "";
if (limit != null) {
    limitMessage = String.format("limit %s", limit);
    limitMessage = comment != null ? String.format(" and %s", limitMessage) : String.format(" with %s", limitMessage);
}
log.info("Done something '{}' and saved (id {}{}){}{}",
        something.getName(), id, notificationMessage, commentMessage, limitMessage);

但它难写、难读、复杂且容易出错。

我想要指定部分日志之类的东西。

示例伪代码:

log.info("Done something '{}' and saved (id {} $notification) $parameters",
        something.getName(), id,
        $notification: sentNotification ? "sent notification" : "",
        $parameters: [comment, limit]);

它应该支持可选参数,用给定的字符串替换 boolean 值/条件,支持分隔空格、逗号和单词 withand

也许有现成的图书馆吗?或者至少有一种更简单的编码方法?

如果没有,我只需要编写自己的库来记录消息。此外,这种库将提供所有日志都是一致的。

如果您没有发现三个可选参数的问题,想象一下还有更多(并且您不能总是将它们打包到一个类中 - 另一个仅用于参数记录的类层会导致更多的复杂性)。

最后,我知道我可以分别记录每个操作。但是有了这个我得到了更多的日志,我不会在一个地方有最重要的信息。其他日志在 debug 级别,而不是 info

最佳答案

这两个都是可以的。您可以:

  • 向 Logger 注册一个组件来为您完成工作
  • 为您的记录器编写一个包装器类以供使用

我将对两者进行演示并解释为什么我认为第二个是更好的选择。让我们从这里开始:

与其让 Logger 拥有如何格式化您的特定属性的知识,不如让您的代码承担此责任。

例如,与其记录每个参数,不如收集它们并分别定义它们的记录。看这段代码:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoggingExample {

  private static final Logger LOGGER = LoggerFactory.getLogger(LoggingExample.class);

  public static void main(String[] args) {
    LogObject o = new LogObject();

    LOGGER.info("{}", o);

    o.first = "hello";

    LOGGER.info("{}", o);

    o.second = "World";

    LOGGER.info("{}", o);

    o.last = "And finally";

    LOGGER.info("{}", o);
  }

  public static class LogObject {

    String first;
    String second;
    String last;

    @Override
    public String toString() {
      StringBuffer buffer = new StringBuffer();
      buffer.append("Log Object: ");
      if (first != null) {
        buffer.append("First: " + first + " ");
      }
      if (second != null) {
        buffer.append("Second: " + second + " ");
      }
      if (last != null) {
        buffer.append("Second: " + last + " ");
      }
      return buffer.toString();
    }
  }
}

我们将LogObject定义为一个容器,这个容器实现了toString。所有 Loggers 都会在它们的对象上调用 toString(),这就是它们确定应该打印什么的方式(除非应用了特殊的格式化程序等)。

有了这个,日志语句打印:

11:04:12.465 [main] INFO LoggingExample - Log Object: 
11:04:12.467 [main] INFO LoggingExample - Log Object: First: hello 
11:04:12.467 [main] INFO LoggingExample - Log Object: First: hello Second: World 
11:04:12.467 [main] INFO LoggingExample - Log Object: First: hello Second: World Second: And finally 

优点:

  • 这适用于任何 Logger。您不必根据要使用的内容实现具体细节
  • 知识封装在 1 个对象中,可以轻松测试。这应该可以缓解您所说的容易出错的格式问题。
  • 不需要复杂的格式化程序库或实现
  • 它最终会使日志看起来更漂亮、更紧凑。 log.info("{}", object);

缺点:

  • 您需要编写 Bean。

现在可以使用例如自定义布局来实现相同的目的。我正在使用 logback,所以这是一个 logback 的例子。

我们可以定义一个 Layout,它知道如何处理您的自定义格式化指令。

import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.LayoutBase;

public class LoggingExample2 {


  private static final Logger CUSTOM_LOGGER = createLoggerFor("test");
  
  public static void main(String[] args) {
    LogObject o = new LogObject();

    CUSTOM_LOGGER.info("{}", o);

    o.first = "hello";

    CUSTOM_LOGGER.info("{}", o);
    
    o.second = "World";

    CUSTOM_LOGGER.info("{}", o);
    
    o.last = "And finally";

    CUSTOM_LOGGER.info("{}", o);
  }

  public static class LogObject {

    String first;
    String second;
    String last;

    @Override
    public String toString() {
      StringBuffer buffer = new StringBuffer();
      buffer.append("Log Object: ");
      if (first != null) {
        buffer.append("First: " + first + " ");
      }
      if (second != null) {
        buffer.append("Second: " + second + " ");
      }
      if (last != null) {
        buffer.append("Second: " + last + " ");
      }
      return buffer.toString();
    }
  }

  public static class ModifyLogLayout extends LayoutBase<ILoggingEvent> {

    @Override
    public String doLayout(ILoggingEvent event) {
      String formattedMessage = event.getFormattedMessage() + "\n";
      Object[] args = event.getArgumentArray();

      return String.format(formattedMessage, args);
    }

  }
  
  private static Logger createLoggerFor(String string) {
      LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
      PatternLayoutEncoder ple = new PatternLayoutEncoder();
      
      ple.setPattern("%date %level [%thread] %logger{10} [%file:%line] %msg%n");
      ple.setContext(lc);
      ple.start();
      
      ConsoleAppender<ILoggingEvent> consoleAppender = new ConsoleAppender<ILoggingEvent>();
      consoleAppender.setEncoder(ple);
      consoleAppender.setLayout(new ModifyLogLayout());
      consoleAppender.setContext(lc);
      consoleAppender.start();

      Logger logger = (Logger) LoggerFactory.getLogger(string);
      logger.addAppender(consoleAppender);
      logger.setLevel(Level.DEBUG);
      logger.setAdditive(false); /* set to true if root should log too */

      return logger;
   }
}

我从以下位置借用了 Logger 实例:Programmatically configure LogBack appender

请注意,我还没有找到可以解析您列出的复杂表达式的库。我认为您可能必须编写自己的实现。

在我的示例中,我仅说明如何根据参数拦截和修改消息。

除非确实需要,否则为什么我推荐它:

  • 实现是特定于 logback 的
  • 编写正确的格式很难......它会产生比创建自定义对象格式更多的错误
  • 它更难测试,因为您确实有无限的对象可以通过此(和格式化)。您的代码现在和将来都必须对此具有弹性,因为任何开发人员都可能随时添加最奇怪的东西。

最后(未询问的)答案:

为什么不用json编码器?然后使用 logstash 之类的东西进行聚合(或 cloudlwatch,或其他任何东西)。

这应该可以解决您所有的问题。

这是我过去所做的:

定义 1 个您想“以不同方式”记录的 bean。我称之为元数据。这个 bean 可以是

public class MetaHolder {
 // map holding key/values 
} 

这基本上只是用一个键存储所有变量。它允许您有效地搜索这些键,将它们放入数据库等。

在您的日志中,您只需执行以下操作:

var meta = // create class 
meta.put("comment", comment); 
// put other properties here
log.info("formatted string", formattedArguments, meta); // meta is always the last arg

Layout 中,这可以很好地转换。因为您不再记录“人类语言”,所以没有“withs”和“in”可以替换。您的日志将只是:

{
    "time" : "...",
    "message" : "...",
    "meta" : {
        "comment" : "this is a comment"
        // no other variables set, so this was it 
    }
}

最后一个(最后一个)纯 Java,如果你想要的话。你可以这样写:

public static void main(String[] args) {

    String comment = null;
    String limit = "test";
    String id = "id";

    LOGGER.info(
        "{} {} {}",
        Optional.ofNullable(comment).map(s -> "The comment " + s).orElse(""),
        Optional.ofNullable(limit).map(s -> "The Limit " + s).orElse(""),
        Optional.ofNullable(id).map(s -> "The id " + s).orElse(""));
  }

这有效地将您想要的格式中的条件逻辑移动到 Java 的 Optional 中。

我发现这也很难阅读和测试,仍然会推荐第一个解决方案

关于java - 使用可选参数记录,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64850328/

相关文章:

java - Power mockito 验证静态调用真实方法

java - 未使用 log4j 配置

java - Log4j2:能够在多用户环境下记录不同日志级别的数据

java - 在weblogic控制台上查看eclipse link JPA执行的SQL查询或使用log4j配置的日志文件

java - 如何在 spring web 应用程序运行时获取 spring-web-mvc 中的 WebApplicationContext 和 DispatcherServlet 的实例

java - 使用java将csv文件导入oracle数据库

file - 解析一个 log4j 日志文件

java - 需要在 java 的 log4j 之前将参数附加到每个日志

java - 通过方法 DI 将事件处理程序从 Controller 传递到 View - JavaFX

java - Log4J2 - 在运行时分配文件附加程序文件名