英文:
What's the best way to capture Log4J (2) log entries during a test?
问题
我正在编写一个日志测试框架,在其中我打算支持多个日志后端和测试工具。在每个测试工具中,都有一个设置/拆卸周期,让我有机会将某种形式的“日志捕获”实例注入到日志系统中。
在JDK日志中,很容易向特定的记录器安装新的处理程序来捕获它看到的所有内容,但在Log4J2中,我很难获得相同行为的东西。
我已经阅读了https://logging.apache.org/log4j/log4j-2.1/manual/architecture.html中的所有文档,我完全理解为什么对日志进行编程配置是一个不好的主意,但对于测试来说,这真的是唯一合理的方法。
在Log4J中,我猜想添加一个Appender是最好的方法,但在配置层次结构的正确位置添加(然后删除)Appender是困难的。
特别是,我需要能够为一个包添加一个Appender(即在代码测试下不存在记录器的包),所以我想要一种说法:
"为此名称获取记录器配置,如果不存在则创建它"
相反,我找到的各种配置获取器只会搜索层次结构并找到父配置(通常是我的情况下的根)。
我的要求是:
- 在配置命名空间的特定位置添加一个捕获Appender。
- 使该Appender捕获配置命名空间中该点以下所有记录器的日志。
- 在完成时有一种稳健的方法删除Appender。
- 不影响实际的日志级别或现有记录器配置的任何其他部分。
我已经设法为现有记录器安装了一个Appender,但任何“子”记录器似乎都不使用这个。
// "loggerName"可以是包名,而在此之前无需存在记录器。
Logger logger = (Logger) LogManager.getLogger(loggerName);
Configuration configuration = ((LoggerContext) LogManager.getContext()).getConfiguration();
configuration.addLoggerAppender(logger, appender);
LoggerConfig config = configuration.getLoggerConfig(loggerName);
// 测试结束后删除Appender的回调。
return () -> {
try {
// 我不明白为什么没有办法通过引用删除append实例,
// 所以我通过使用随机名称字符串进行了破解,因为我不想涉及到多个测试并行运行的问题。
// 我希望有一种避免这种情况的解决方案。
config.removeAppender(probablyUniqueAppenderName);
} catch (RuntimeException e) {
// 在close()上忽略。
}
};
我不幸注意到,在其他尝试中的各个地方,我看到了记录器的现有日志级别被添加Appender的看似简单的操作修改。这显然是我尝试做的事情无法接受的行为(当创建新配置时,它继承父配置的级别而不是使用已在记录器上设置的任何日志级别)。
我也不相信我所问的问题明显与我在这里关于Log4J的以前的(通常是几年前的)问题完全相同,所以请不要假设我应该使用其中一个(我尝试过的那些都不起作用)。
编辑:我首先尝试的另一段代码片段(也不起作用)是:
Logger logger = (Logger) LogManager.getLogger(loggerName);
// 这会*更改*记录器的级别,因为没有现有配置。
// 例如,将现有记录器设置为TRACE级别后,此调用后将设置为ERROR,
// 因为为此记录器创建新配置的行为是从父配置(根)继承,而不是使用实例上设置的级别。
logger.addAppender(appender);
return () -> {
try {
logger.removeAppender(appender);
} catch (RuntimeException e) {
// 在close()上忽略。
}
};
编辑2:我还尝试通过将现有配置与我以编程方式创建的自定义配置合并来重置配置,但在尝试时有几件事情没能正常工作(特别是,CompositeConfiguration
类不允许我合并Configuration
接口的实例,只允许合并AbstractConfiguration
类的子类的实例(这不是LogManager
返回的)。
编辑3:我现在找到了https://logging.apache.org/log4j/2.x/manual/customconfig.html#programmatically-modifying-the-current-configuration-after-initi,看起来很有希望(尽管有点复杂),但文档没有解释:
AppenderRef
是做什么的,或者为什么需要它- 如何撤消对配置的任何修改以在测试完成后重新安装原始配置(尽管我可能可以猜测)。
英文:
I'm writing a logger testing Framework in which I intend to support multiple logging backends and test harnesses. In each test harness there's a setup/teardown cycle that gives me a chance to inject some form of "log capturing" instance into the logging system.
In JDK logging it's easy to install a new handler to a specific logger to capture everything it sees, but in Log4J2 I've struggled to get something with the same behaviour.
I've read all the docs in https://logging.apache.org/log4j/log4j-2.1/manual/architecture.html and I fully understand why programmatic configuration of logging is a bad idea, but for tests it's really the only reasonable approach.
In Log4J I'm guess that adding an Appender is the best way to go, but getting an Appender added (and then removed) to the right place in the configuration hierarchy is difficult.
In particular, I need to be able to add an Appender for a package (i.e. for which no logger exists in the code-under-test), so I'd like a way to say:
"Get me the logger config for this name, creating it if it doesn't exist"
Instead, the various config getters I've found will just search up the hierarchy and find the parent config (typically root in my case).
My requirements are:
- Add a capturing appender at a specific point in the config namespace.
- Have the appender capture logs for all loggers at or below that point in the config namespace.
- Have a robust way to remove the appender when I'm done.
- Don't affect actual log levels or any other part of the existing logger configuration.
I've managed to get an Appender installed for an existing logger, but any "child" loggers seem to not use this.
// The "loggerName" can be a package name and no logger need exist for it before this point.
Logger logger = (Logger) LogManager.getLogger(loggerName);
Configuration configuration = ((LoggerContext) LogManager.getContext()).getConfiguration();
configuration.addLoggerAppender(logger, appender);
LoggerConfig config = configuration.getLoggerConfig(loggerName);
// A callback to remove the appendr after the test.
return () -> {
try {
// I don't understand why there's no way to remove the append instance via its reference,
// so I hacked it to use a random name string since I don't want to get into issues
// with multiple tests running in parallel. I'd like a solution that avoids this.
config.removeAppender(probablyUniqueAppenderName);
} catch (RuntimeException e) {
// Ignored on close().
}
};
One unfortunate thing I've noticed is that in various places during other attempts I've seen the existing log level of a logger be modified by the seemingly simple act of adding an Appender. This is clearly unacceptable to what I'm trying to do and I've no idea why that's desireable behaviour (when a new config is created it inherits the parent config's level rather than using any log level already set on the logger).
I also don't believe what I'm asking is obviously identical to any of the previous (often years old) questions I've seen here about Log4J, so please don't just assume I should use one of those (the ones I've tried didn't work).
Edit: The other code snippet I tried first (which also doesn't work) is:
Logger logger = (Logger) LogManager.getLogger(loggerName);
// This *changes* the logger's level because there's no existing config.
// For example, existing loggers set to TRACE level become set to ERROR after
// this call because the act of creating a new config for this logger inherits
// from the parent (root) instead of using the level set on the instance.
logger.addAppender(appender);
return () -> {
try {
logger.removeAppender(appender);
} catch (RuntimeException e) {
// Ignored on close().
}
};
Edit 2: I've also looked at resetting the configuration by merging the existing configuration with a custom one I created programmatically, but several things didn't work when trying that (in particular, the CompositeConfiguration
class won't let me merge instances of the Configuration
interface, only sublcasses of the AbstractConfiguration
class (which isn't what LogManager
returns).
Edit 3: I've now found https://logging.apache.org/log4j/2.x/manual/customconfig.html#programmatically-modifying-the-current-configuration-after-initi which looks promising (if a little complex), but the docs don't explain:
- what an
AppenderRef
does, or why it's needed - how to undo any modifications to re-installl the original configuration after testing is complete (though I can probably guess that).
答案1
得分: 2
我发现使用Configurator
(内部API)足以在添加appender时更改记录器的级别,而不会被还原。因此,在我的测试中,我不再使用:
logger.setLevel(Level.FOO)
而是使用:
Configurator.setLevel(logger, Level.FOO)
然后,在测试期间添加/移除appender,我只需使用最初的想法,即logger.addAppender()
/ removeAppender()
(使用“core”API)。
基本上,我从中发现Log4J对于编程配置有非常明确的观点(这没问题,它们大多数与我的观点一致),但它在解释某些记录器状态是短暂的,并且会在配置的任何操作的副作用下被覆盖方面做得不好。
因此,如果你需要进行这种编程操作,你最终会陷入困境。这并不是我想要实现的100%,但它确实帮助我继续前进。
我还不得不假设他们实际上永远不会删除配置器(Hyrum的定律可能意味着它太常用,以至于在这一点上无法被删除)。
英文:
I discovered that using the Configurator
(internal API) was good enough to change a logger's level without it being reverted when I add the appender.
So in my tests, instead of using:
logger.setLevel(Level.FOO)
I now use:
Configurator.setLevel(logger, Level.FOO)
Then to add/remove appenders for the duration of the test, I can just use my original idea via logger.addAppender()
/ removeAppender()
(using the "core" API).
Basically what I've discovered from this is the Log4J has very strong opinions about programmatic configuration (which is fine, they mostly match mine), but doesn't do a good job of explaining how some logger state is transitory and will be overridden as a side-effect of any operations on the configuration.
So if you are needing to do this sort of programmatic manipulation, you'll end up chasing your tail. This isn't a 100% what I wanted to achieve, but it does help me move forward.
I'm also having to assume they'll never actually remove the configurator (Hyrum's Law probably means it's too well used to be removable at this point).
答案2
得分: 0
以下是您代码中需要翻译的部分:
-
根据javadoc中的说明,
LogManager#getContext()
通常不会给您与LogManager#getLogger(...)
相同的记录器上下文:警告 - 此方法返回的LoggerContext可能不是用于为调用类创建记录器的LoggerContext。
使用
LogManager.getContext(false)
或Logger.getContext()
。 -
一旦您完成配置更改,需要调用
LoggerContext#updateLoggers()
来提交配置更改。
正如您所指出的,不建议使用编程配置。从语义上讲,次要版本更改可能会破坏它。但是,在当前版本(截止到撰写本文时为2.20.0)中,您可以使用以下代码:
final org.apache.logging.log4j.spi.LoggerContext c = LogManager.getContext(false);
if (c instanceof LoggerContext) {
final LoggerContext context = (LoggerContext) c;
final LoggerConfig oldConfig = context.getConfiguration().getLoggerConfig(loggerName);
final LoggerConfig newConfig;
// 添加新的记录器配置或使用现有的配置
if (loggerName.equals(oldConfig.getName())) {
newConfig = oldConfig;
} else {
newConfig = new LoggerConfig(loggerName, level, true);
}
// 实际上添加一个AppenderRef
newConfig.addAppender(appender, null, null);
// 设置LoggerConfig的级别
newConfig.setLevel(level);
context.getConfiguration().addLogger(loggerName, newConfig);
context.updateLoggers();
}
**备注:**您的设置可能不适用于并行测试。请参阅此邮件列表线程以获取一些替代方案。
英文:
There are a couple of problems in your code:
-
As stated in the javadoc,
LogManager#getContext()
usually does not give you the same logger context asLogManager#getLogger(...)
:> WARNING - The LoggerContext returned by this method may not be the LoggerContext used to create a Logger for the calling class.
Use
LogManager.getContext(false)
orLogger.getContext()
instead. -
Once you are done with configuration changes you need to call
LoggerContext#updateLoggers()
to commit your configuration changes.
As you remarked, programmatic configuration is not recommended. Semantically a minor version change can break it. However, on the current version (2.20.0 as of writing) you can use:
final org.apache.logging.log4j.spi.LoggerContext c = LogManager.getContext(false);
if (c instanceof LoggerContext) {
final LoggerContext context = (LoggerContext) c;
final LoggerConfig oldConfig = context.getConfiguration().getLoggerConfig(loggerName);
final LoggerConfig newConfig;
// Add a new logger config or use the existent one
if (loggerName.equals(oldConfig.getName())) {
newConfig = oldConfig;
} else {
newConfig = new LoggerConfig(loggerName, level, true);
}
// Actually adds an AppenderRef
newConfig.addAppender(appender, null, null);
// Sets the level of the LoggerConfig
newConfig.setLevel(level);
context.getConfiguration().addLogger(loggerName, newConfig);
context.updateLoggers();
}
Remark: Your setup will probably not work for parallel tests. See this mailing list thread for some alternatives.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论