*Abstract:* Nested logging does not always work, it conflicts with
`log4j2.formatMsgAsync`, and I need help on fixing it.
*Problem description*
Nested logging – that is, while trying to `log()`,
exception/argument/message access invokes `log()` too – only works with
message factories that eagerly format the message. `LoggerConfig` contains
the following logic:
var logEvent = logEventFactory.createEvent();
log(logEvent); // goes to `AppenderControl#shouldSkip()`
Imagine the following:
private static final class ArgThatLogs {
@Override
public String toString() {
logger.trace("foo");
return "bar";
}
}
logger.info("{} buzz", new ArgThatLogs());
In `LoggerConfig`, if the `logEventFactory` of subject is
`ReusableLogEventFactory`, `createEvent()` will first try to format the
message (which will trigger `ArgThatLogs#toString()`, which will trigger
`logger.trace("foo")`), and then call `log(logEvent)`. Hence the logging
output will be as follows:
foo
bar buzz
If the `logEventFactory` of subject is `DefaultLogEventFactory`,
`createEvent()` will perform no formatting, `log(logEvent)` will be
invoked, and then `ArghThatLogs#toString()`, and then
`logger.trace("foo")`, and then `AppenderControl#shouldSkip()` will return
`true` due to `isRecursiveCall()` returning `true`. Hence the logging
output will be as follows:
bar buzz
Note the missing `foo` log line. As a matter of fact, all `*Nested*Test`
classes fail when `DefaultLogEventFactory` is used.
I thought of matching the behavior between the two log event factories by
making the `DefaultLogEventFactory` format the message at creation. This
would imply both message factories will be formatting the message eagerly.
(Formatting the message at the logger thread was also suggested by Nitsan
Wakart
<https://github.com/apache/logging-log4j2/issues/2220#issuecomment-1902035073>,
the JVM performance celebrity, too.)
While examining the code base on this subject, I came across the
`log4j2.formatMsgAsync` property (flag that *"will make sure the message is
formatted in the caller thread"*) and the `@AsynchronouslyFormattable`
annotation. This gets the things even more complicated:
1. These configuration knobs contradict with the *"eagerly format
messages everywhere"* feature I suggested to implement above.
2. If `log4j2.formatMsgAsync=true`, nested logging does not work
anymore. That is, postponing the message formatting to `log(logEvent)`
causes recursion and hence the nested `log(logEvent)` gets skipped.
(`DefaultLogEventFactory` fails to log the nested call exactly due to the
same reason.)
3. One would expect these configuration knobs to be effective somewhere
in my nested logging example above. But they surprisingly play no role in
this entire flow in almost all cases. That is, there is
`InternalAsyncUtil.makeMessageImmutable()` taking these configuration knobs
into account, but it is only called in async. contexts (e.g.,
`AsyncAppender` or `AsyncLogger) when a message is not reusable. It makes
sense that the configuration is taken into account only when the context is
async. But it should *not* depend on if-the-message-is-reusable
condition. That is, the fact that reusable messages are *always*
formatted eagerly violates the `log4j2.formatMsgAsync=true` condition, if
provided.
There is a performance issue in the design
of `InternalAsyncUtil.makeMessageImmutable()` too. It invokes
`Message#getFormattedMessage()` to indicate to the message that it can
format the message and cache the result, if possible. Yet,
`getFormattedMessage()` has a return type of `String`, forcing the message
to allocate a string unnecessarily.
Formatting messages asynchronously (LOG4J2-898
<https://issues.apache.org/jira/browse/LOG4J2-898>) was implemented by
Remko. AFAIC, formatting messages async. and nested logging are features
that don't always work and enabling one breaks the behaviour of the other.
A user caring about logger latency (i.e., `log4j2.formatMsgAsync=true`),
cannot have nested logging. Likewise, a user caring about nested logging
cannot have async. message formatting, since postponing message formatting
is causing recursion which eventually causes the nested logging to get
dropped. Plus, none of the features work as advertised for all cases. They
both unexpectedly don't work under certain circumstances.
AFAIK, nested logging is not a documented feature. Yet one can find several
tickets people has filed issues that were fixed by us. Hence, it is safe to
conclude that it is used.
I also don't know why recursion is not allowed in `AppenderControl`.
(Related history is too old and could not get in while migrating from SVN
to Git.)
*Summary*
Let me try to recap the story:
1. Nested logging is not a documented feature, yet expected to work by
users
2. Nested logging doesn't always work (when message formatting is
postponed either by using `DefaultLogEventFactory` or setting
`log4j2.formatMsgAsync=true`)
3. `log4j2.formatMsgAsync` and `@AsynchronouslyFormattable` don't always
work (`log4j2.formatMsgAsync=false` is ignored for reusable messages, which
are formatted always eagerly)
4. `log4j2.formatMsgAsync`/`@AsynchronouslyFormattable` and the way
currently nested logging works (e.g., eagerly formatting the message) are
conflicting features
At this stage... I am lost. I don't know what to fix. I don't know which
behaviour we shall keep/drop. I will appreciate it if you can think along
with me.