*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.

Reply via email to