Skip to content

Support for Custom Value Serializer in Log4jEventBuilder#addKeyValue() and Asynchronous Serialization #2996

@AlexxeyKa

Description

@AlexxeyKa

In the current Log4jEventBuilder#addKeyValue(String key, Object value) implementation, complex objects are serialized using String.valueOf(), which lacks flexibility. My suggestion is to allow users to provide a custom serializer that can handle more complex serialization needs

We can provide a StringBuilderFormattable specialization there. That is, we can change addKeyValue() implementation such that:

String encodedValue = value instance StringBuilderFormattable
        ? encodeUsingStringBuilderFormattable((StringBuilderFormattable) value)
        : toString(value);
keyValuePairs.put(key, encodedValue);

But,

  • This doesn't solve the encoding problem. Assume JsonTemplateLayout is employed, it will encode the Map<String, String> passed and this will result in "kvPairs": {"yourKey": "{\"some-value\": \"you-already-JSONified\"}"}. See?
  • This encourages a bad-practice: assumption on the logging layout employed. (What if the effective layout is XML?)
  • There is a better alternative, which I will elaborate on below.

... handling complex objects, especially when structured logging is required.
... complex serialization needs (e.g., converting objects to JSON)

There is so much to unpack here.

For one, logging as follows

LOGGER.info("userContext: {}", toJSON(userContext));

is a bad-practice, because this implies an assumption on the logging layout employed, which contradicts with the separation-of-concerns practiced with the logging API vs. logging implementation separation. What if the used layout is XML? etc. Hence, we should avoid encoding payloads with assumptions on the employed layout. Instead, we should allow registering arbitrarily-typed parameters, if necessary, with sufficient encoding hints (e.g., extending from MultiFormatStringBuilderFormattable) and let them be handled by the employed layout.

Currently, addKeyValue() admissions get registered to a Map<String, String> – we should amend this with a Map<String, Object>. Though the bigger problem is how KV-pairs are passed to the LogBuilder:

try (final Instance c = CloseableThreadContext.putAll(keyValuePairs)) {
  logBuilder.log(message, arguments.toArray());
}

That is, log4j-slf4j2-impl hacks auto-clearing thread context (i.e., CloseableThreadContext) to pass the KV-pairs all the way down to the layout. This is due to the fact that LogBuilder of Log4j doesn't have a counterpart for arbitrary KV-pairs and the feature that comes closest to it we can leverage (and we did) is thread context. There are a couple of angles we can approach this problem from:

  1. Implement accepting non-String-typed values in CloseableThreadContextLOG4J2-1648 proposes this. Registering instances of custom types in a thread context can cause memory leakage in Java EE container environments, though this should not be a problem for CloseableThreadContext.
  2. Extend LogBuilder to accept custom KV-pairs

I propose that the serialization of the value should happen asynchronously, offloading the potentially expensive serialization task to a separate thread. This will prevent blocking the thread that calls logging methods (such as log.info()), thus improving performance in high-throughput logging environments where structured logging is used extensively.

"Improve performance in high-throughput logging environments [by offloading the log event encoding to a background thread]" this is a claim that cannot hold without given sufficient context. Given the context, most of the time it becomes a special case rather than a general one we can practice for every user. Nevertheless, if we can pass arbitrary objects in KV-pairs (and effectively allow the layout to take charge of the complete encoding effort), you can combine this with asynchronous logging to effectively encode events in a thread different from the one invoking the logging API.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    To triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions