我有方法可以在其中添加特定的日志记录:
@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 值/条件,支持分隔空格、逗号和单词 with
和 and
。
也许有现成的图书馆吗?或者至少有一种更简单的编码方法?
如果没有,我只需要编写自己的库来记录消息。此外,这种库将提供所有日志都是一致的。
如果您没有发现三个可选参数的问题,想象一下还有更多(并且您不能总是将它们打包到一个类中 - 另一个仅用于参数记录的类层会导致更多的复杂性)。
最后,我知道我可以分别记录每个操作。但是有了这个我得到了更多的日志,我不会在一个地方有最重要的信息。其他日志在 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/