当前位置: 首页 > 知识库问答 >
问题:

参数可选的日志记录

长孙景天
2023-03-14
@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/condition,支持用和分隔空格、逗号和单词。

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

如果不是这样,我就没有其他办法为日志记录中的消息编写自己的库了。此外,这种库将提供所有日志的一致性。

共有1个答案

龚招
2023-03-14

这两个都有可能。您可以:

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

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

例如,与其记录每个参数,不如收集它们并分别定义它们的日志记录。请参见此代码

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。所有记录器都将对其对象调用tostring(),这就是它们如何确定应该打印什么(除非应用了特殊的格式化程序等)。

这样,log语句就会打印出来:

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 
  • 这适用于任何记录器。您不必根据您想要使用的东西来实现细节
  • 知识被封装在一个易于测试的对象中。这将缓解您所述的容易出错的格式问题。
  • 不需要复杂的格式化程序库或实现
  • 它最终会使日志看起来更好、更紧凑。log.info(“{}”,对象);

缺点:

  • 需要编写bean。

我们可以定义一个布局,它拥有如何使用自定义格式指令的知识。

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;
   }
}

我从以下地方借用了记录器instatiation:以编程方式配置LogBack appender

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

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

最后一个(未经询问的)回答:

为什么不使用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
{
    "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的可选中。

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

 类似资料:
  • 问题内容: 使用,如何记录参数并抛出异常? 我找不到任何方法。 我应该使用? 问题答案: 有没有办法把两者以中。 我会转换为并使用: 您还可以创建log指向的自己的方法,例如:

  • 我在Scala上有一个项目。我使用这个库记录https://github.com/typesafehub/scala-logging 我创建记录器 和两个标记 为什么marker和marker2可见,为什么DENY不起作用? 如何排除两个标记? 如何只排除一个标记?

  • 我想在我的应用程序中使用SLF4J+logback用于两个目的--日志和审计。 14:41:57.978[main]信息AUDIT_LOGGER-110欢迎使用main 如何确保审核消息在审核记录器下只出现一次?

  • 问题内容: 我正在考虑将Redis用于Web应用程序日志记录目的。我用谷歌搜索,有人将日志转储到Redis队列/列表中,然后将计划的工作人员转储到磁盘中。 http://nosql.mypopescu.com/post/8652869828/another-redis-use-case- centralized-logging 我希望寻求理解,为什么不直接使用Redis持久化到磁盘?如果我分配了一

  • logging 模块自 2.3 版以来一直是 Python 标准库的一部分。在 PEP 282 中有对它的简洁描述。除了 基础日志教程 之外,这些文档是非常难以阅读的。 日志记录一般有两个目的: 诊断日志 记录与应用程序操作相关的日志。例如,当用户遇到程序报错时, 可通过搜索诊断日志以获得上下文信息。 审计日志 为商业分析而记录的日志。从审计日志中,可提取用户的交易信息, 并结合其他用户资料构成用

  • 用户在这个插件的很多方面都可以进行定制,用户通过插件选项来达到这个目的(这些选项会在插件初始化的时候起效)。这些选项包括: 选项 描述 aspectRatio 长宽比,以后在选择时候就会维持不变。例如:"4:3" autoHide 如果设为true,那么在选择完后区域会消失。Default:false classPrefix 预先给插件元素的前缀 Default:imgareaselect dis