package com.example;

import org.springframework.boot.json.JsonWriter.Members;
import org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizer;

public class MyCustomizer implements StructuredLoggingJsonMembersCustomizer<String> {

    @Override
    public void customize(Members<String> members) {
        members.add("test", "value");

    }

}

package com.example;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;

@SpringBootTest
@ExtendWith(OutputCaptureExtension.class)
public class MyCustomizerTests {

    @Test
    void test(CapturedOutput output) {
        assertThat(output).contains("{\"@timestamp\""); // structured logging is working
        assertThat(output).contains("\"test\":\"value\""); // MyCustomizer is working
    }
}

test failed with:

java.lang.AssertionError: 
Expecting actual:
  {"@timestamp":"2024-11-28T08:42:45.558754Z","log.level":"INFO","process.pid":71176,"process.thread.name":"main","log.logger":"com.example.MyCustomizerTests","message":"Starting MyCustomizerTests using Java 17.0.13 with PID 71176","ecs.version":"8.11"}
{"@timestamp":"2024-11-28T08:42:45.562533Z","log.level":"INFO","process.pid":71176,"process.thread.name":"main","log.logger":"com.example.MyCustomizerTests","message":"No active profile set, falling back to 1 default profile: \"default\"","ecs.version":"8.11"}
{"@timestamp":"2024-11-28T08:42:46.289631Z","log.level":"INFO","process.pid":71176,"process.thread.name":"main","log.logger":"com.example.MyCustomizerTests","message":"Started MyCustomizerTests in 0.939 seconds (process running for 1.622)","ecs.version":"8.11"}

to contain:
  ""test":"value"" 
    at com.example.MyCustomizerTests.test(MyCustomizerTests.java:18)
    at java.base/java.lang.reflect.Method.invoke(Method.java:569)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

Here is a reproducer project structured-logging.zip

Comment From: wilkinsona

Thanks for the sample, @quaff. The problem's due to generics and the use of LambdaSafe to invoke the customizer. It will work if you change your customizer to implement StructuredLoggingJsonMembersCustomizer<Object>. The problem doesn't occur when using logging.structured.json.customizer to declare the customizer as StructuredLoggingJsonPropertiesJsonMembersCustomizer doesn't use LambdaSafe to invoke the customizer.

Comment From: quaff

Is it by design that logging.structured.json.customizer doesn't accept multiple Customizers like spring.factories? @wilkinsona

Comment From: wilkinsona

Sorry, I don't remember. Do you, @philwebb or @mhalbritter?

Comment From: mhalbritter

No, sorry, I don't know.

Comment From: philwebb

It's not really by design as such. The StructuredLoggingJsonMembersCustomizer was quite a last minute addition and to save development time I just added support for a single customizer. I think the injection into StructuredLogFormatter constructors gets a bit more complex if we directly support a list.

I do think we could create a ComposteStructuredLoggingJsonMembersCustomizer pretty easily if we want to support lists.