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.