Hello.

In my company, they expect us that the JSON messages we log must always contain the event.kind entry while forwarding out messages to Elastic. Given that such property can take specific useful values, my idea was to provide a default value event in case it was not provided and to overwrite such default with another value when needed. As such, I configured my Spring Boot 3.5.6 (Java 25) application as follows:

logging:
  structured:
    format.console: ecs
    ecs.service.environment: dev
    json.add:
      event.kind: event
      deployment.environment.name: dev
  level:
    root: info
    org.hibernate.validator.internal.util.Version: warn

and, to test the overwrite, I just wrote a line like

var logger = LoggerFactory.getLogger(getClass());
logger.atInfo().addKeyValue("event.kind", "metric").log("This is a log entry on INFO level");

but then this fails with

13:47:47,369 |-ERROR in ch.qos.logback.core.ConsoleAppender[CONSOLE] - Appender [CONSOLE] failed to append. java.lang.IllegalStateException: The name 'event' has already been written
    at java.lang.IllegalStateException: The name 'event' has already been written
    at  at org.springframework.util.Assert.state(Assert.java:101)
    at  at org.springframework.boot.json.JsonValueWriter.writePair(JsonValueWriter.java:252)
    at  at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:987)
    at  at org.springframework.boot.logging.structured.ContextPairs$Pairs.nested(ContextPairs.java:196)
    at  at org.springframework.boot.json.JsonWriter$Member.lambda$getValueToWrite$4(JsonWriter.java:655)
    at  at org.springframework.boot.json.JsonValueWriter.writePairs(JsonValueWriter.java:242)
    at  at org.springframework.boot.json.JsonWriter$Member.lambda$getValueToWrite$5(JsonWriter.java:655)

which is the same as https://github.com/spring-projects/spring-boot/issues/45217, just the usage context is different. /cc @philwebb given he's already looking and the mentioned similar issue.

Comment From: cdprete

@philwebb it seems it happens for the entire event object. I've the class

public class StructuredLoggingPingTask implements Runnable {
    private static final Logger logger = LoggerFactory.getLogger(StructuredLoggingPingTask.class);

    static final String EVENT_CATEGORY_KEY = "event.category";
    static final String EVENT_CATEGORY_VALUE = "process";
    static final String EVENT_TYPE_KEY = "event.type";
    static final String EVENT_TYPE_VALUE = "info";
    static final String EVENT_OUTCOME_KEY = "event.outcome";
    static final String EVENT_OUTCOME_VALUE = "success";

    private final StructuredLoggingPingProperties properties;

    public StructuredLoggingPingTask(StructuredLoggingPingProperties properties) {
        this.properties = properties;
    }

    @Override
    public void run() {
        logger.atInfo()
                .addKeyValue(EVENT_CATEGORY_KEY, EVENT_CATEGORY_VALUE)
                .addKeyValue(EVENT_TYPE_KEY, EVENT_TYPE_VALUE)
                .addKeyValue(EVENT_OUTCOME_KEY, EVENT_OUTCOME_VALUE)
                .log(properties.message());
    }
}

and even for different keys (category, type and outcome) under event I get back

11:40:43,447 |-ERROR in ch.qos.logback.core.ConsoleAppender[CONSOLE] - Appender [CONSOLE] failed to append. java.lang.IllegalStateException: The name 'event' has already been written
    at java.lang.IllegalStateException: The name 'event' has already been written
    at  at org.springframework.util.Assert.state(Assert.java:101)
    at  at org.springframework.boot.json.JsonValueWriter.writePair(JsonValueWriter.java:252)
    at  at java.base/java.util.LinkedHashMap.forEach(Unknown Source)
    at  at org.springframework.boot.logging.structured.ContextPairs$Pairs.nested(ContextPairs.java:196)
    at  at org.springframework.boot.json.JsonWriter$Member.lambda$getValueToWrite$4(JsonWriter.java:655)
    at  at org.springframework.boot.json.JsonValueWriter.writePairs(JsonValueWriter.java:242)
    at  at org.springframework.boot.json.JsonWriter$Member.lambda$getValueToWrite$5(JsonWriter.java:655)

Moreover, what's worse is that the ECS format is not followed anymore, as you can see, making the forward of those logs not even possible actually. The situation is therefore way more problematic than expected. Are there any potential temporary workarounds possible?

Comment From: cdprete

Current workaround from my side:

  1. Configure logback by hand and use logback-ecs-encoder as dependency
  2. Extend the workaround in https://github.com/elastic/ecs-logging-java/issues/49 to provide the event.kind if it's not already in there