【问题标题】:Logging with optional parameters使用可选参数进行日志记录
【发布时间】:2021-02-27 04:44:41
【问题描述】:

我有要添加特定日志记录的方法:

@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]);

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

也许有这方面的现有图书馆?或者也许至少有一种更简单的编码方式?

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

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

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

【问题讨论】:

    标签: java log4j log4j2 logback slf4j


    【解决方案1】:

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

    • 向 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。所有 Logger 都会在他们的对象上调用 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。您不必根据要使用的内容来实施具体细节
    • 知识被封装在一个易于测试的对象中。这应该可以缓解您所说的容易出错的格式问题。
    • 无需复杂的格式化程序库或实现
    • 最终将使日志记录看起来更加美观和紧凑。 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;
       }
    }
    
    

    我从 Programmatically configure LogBack appender 借用了 Logger 实例

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

    在我的例子中,我只说明了如何根据参数截取和修改消息。

    为什么我会推荐这个,除非真的需要它:

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

    最后一个(未提出的)答案:

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

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

    这是我过去所做的:

    定义 1 个您喜欢“以不同方式”记录的 bean。我称之为metadata。这个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

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

    【讨论】:

    • 另一种替代方法也可能是为此使用 MDC,但这会带来保持上下文清洁的额外复杂性。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-03-06
    • 2012-05-11
    • 2011-03-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多