From 55633a1377c8e75114a6c17aaf6dfc1d40d2cf20 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 24 Jan 2025 13:50:24 -0800 Subject: [PATCH] Polish 'Add support for multiple StructuredLoggingJsonMembersCustomizers' See gh-43368 --- .../reference/pages/features/logging.adoc | 3 +- ...BeanFactoryInitializationAotProcessor.java | 26 +++++++------ .../StructuredLoggingJsonProperties.java | 39 ++++++++++++++++++- ...ngJsonPropertiesJsonMembersCustomizer.java | 15 +------ ...itional-spring-configuration-metadata.json | 5 +++ ...nPropertiesJsonMembersCustomizerTests.java | 31 ++++++++++----- .../StructuredLoggingJsonPropertiesTests.java | 4 +- 7 files changed, 84 insertions(+), 39 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc index 4af1cb2030..1488b4626d 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc @@ -641,7 +641,8 @@ logging: corpname: mycorp ---- -TIP: For more advanced customizations, you can write your own class that implements the javadoc:org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizer[] interface and declare it using the configprop:logging.structured.json.customizer[] property. +TIP: For more advanced customizations, you can use the javadoc:org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizer[] interface. +You can reference a single implementation using the configprop:logging.structured.json.customizer[] property, or use configprop:logging.structured.json.customizers[] if you have more than one. You can also declare implementations by listing them in a `META-INF/spring.factories` file. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessor.java index bf707d78f3..79cdc396da 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessor.java @@ -16,11 +16,11 @@ package org.springframework.boot.logging.structured; -import java.util.Optional; import java.util.Set; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.RuntimeHints; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; @@ -34,6 +34,7 @@ import org.springframework.core.env.Environment; * * @author Dmytro Nosan * @author Yanming Zhou + * @author Phillip Webb */ class StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor { @@ -43,27 +44,28 @@ class StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcesso @Override public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { Environment environment = beanFactory.getBean(ENVIRONMENT_BEAN_NAME, Environment.class); - return Optional.ofNullable(StructuredLoggingJsonProperties.get(environment)) - .map(StructuredLoggingJsonProperties::customizer) - .map(AotContribution::new) - .orElse(null); + StructuredLoggingJsonProperties properties = StructuredLoggingJsonProperties.get(environment); + return (properties != null) ? AotContribution.get(properties.allCustomizers()) : null; } private static final class AotContribution implements BeanFactoryInitializationAotContribution { - private final Set>> customizer; + private final Set>> customizers; - private AotContribution(Set>> customizer) { - this.customizer = customizer; + private AotContribution(Set>> customizers) { + this.customizers = customizers; } @Override public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { - this.customizer.forEach((it) -> generationContext.getRuntimeHints() - .reflection() - .registerType(it, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, - MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)); + ReflectionHints reflection = generationContext.getRuntimeHints().reflection(); + this.customizers.forEach((customizer) -> reflection.registerType(customizer, + MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)); + } + + static AotContribution get(Set>> customizers) { + return (!customizers.isEmpty()) ? new AotContribution(customizers) : null; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonProperties.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonProperties.java index a5b076a00a..cc1f6fc42e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonProperties.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonProperties.java @@ -16,12 +16,17 @@ package org.springframework.boot.logging.structured; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import org.springframework.boot.context.properties.bind.BindableRuntimeHintsRegistrar; import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.util.Instantiator; import org.springframework.core.env.Environment; +import org.springframework.util.CollectionUtils; /** * Properties that can be used to customize structured logging JSON. @@ -31,12 +36,42 @@ import org.springframework.core.env.Environment; * @param rename a map of path to replacement names * @param add a map of additional elements {@link StructuredLoggingJsonMembersCustomizer} * @param customizer the fully qualified name of a - * {@link StructuredLoggingJsonMembersCustomizer} + * {@link StructuredLoggingJsonMembersCustomizer} implementation + * @param customizers the fully qualified names of + * {@link StructuredLoggingJsonMembersCustomizer} implementations * @author Phillip Webb * @author Yanming Zhou */ record StructuredLoggingJsonProperties(Set include, Set exclude, Map rename, - Map add, Set>> customizer) { + Map add, Class> customizer, + Set>> customizers) { + + Collection> allCustomizers(Instantiator instantiator) { + return allCustomizers().stream().map((customizer) -> instantiateCustomizer(instantiator, customizer)).toList(); + } + + Set>> allCustomizers() { + return merge(customizer(), customizers()); + } + + private Set merge(T element, Set elements) { + if (CollectionUtils.isEmpty(elements)) { + return (element != null) ? Set.of(element) : Collections.emptySet(); + } + if (element == null) { + return elements; + } + Set result = new LinkedHashSet<>(elements.size() + 1); + result.add(element); + result.addAll(elements); + return result; + } + + @SuppressWarnings("unchecked") + private StructuredLoggingJsonMembersCustomizer instantiateCustomizer(Instantiator instantiator, + Class> customizer) { + return (StructuredLoggingJsonMembersCustomizer) instantiator.instantiateType(customizer); + } static StructuredLoggingJsonProperties get(Environment environment) { return Binder.get(environment) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizer.java index b24aecf838..389a3cc555 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.boot.logging.structured; import java.util.Map; -import java.util.Set; import org.springframework.boot.json.JsonWriter.MemberPath; import org.springframework.boot.json.JsonWriter.Members; @@ -51,10 +50,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizer implements Structured if (!CollectionUtils.isEmpty(add)) { add.forEach(members::add); } - Set>> customizer = this.properties.customizer(); - if (customizer != null) { - customizer.forEach((c) -> createAndApplyCustomizer(members, c)); - } + this.properties.allCustomizers(this.instantiator).forEach((customizer) -> customizer.customize(members)); } String renameJsonMembers(MemberPath path, String existingName) { @@ -71,11 +67,4 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizer implements Structured return (!included || excluded); } - @SuppressWarnings({ "unchecked", "rawtypes" }) - private void createAndApplyCustomizer(Members members, - Class> customizerClass) { - ((StructuredLoggingJsonMembersCustomizer) this.instantiator.instantiateType(customizerClass)) - .customize(members); - } - } diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index b552721104..ea31f4d157 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -268,6 +268,11 @@ }, { "name": "logging.structured.json.customizer", + "type": "java.lang.Class>", + "description": "The fully qualified class name of a StructuredLoggingJsonMembersCustomizer" + }, + { + "name": "logging.structured.json.customizers", "type": "java.util.Set>>", "description": "The fully qualified class names of a StructuredLoggingJsonMembersCustomizer" }, diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizerTests.java index 454d15a8f9..2a34bc7e85 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests { @Test void customizeWhenHasExcludeFiltersMember() { StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(), - Set.of("a"), Collections.emptyMap(), Collections.emptyMap(), null); + Set.of("a"), Collections.emptyMap(), Collections.emptyMap(), null, null); StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer( this.instantiator, properties); assertThat(writeSampleJson(customizer)).doesNotContain("a").contains("b"); @@ -57,7 +57,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests { @Test void customizeWhenHasIncludeFiltersOtherMembers() { StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Set.of("a"), - Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), null); + Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), null, null); StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer( this.instantiator, properties); assertThat(writeSampleJson(customizer)).contains("a") @@ -69,7 +69,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests { @Test void customizeWhenHasIncludeAndExcludeFiltersMembers() { StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Set.of("a", "b"), Set.of("b"), - Collections.emptyMap(), Collections.emptyMap(), null); + Collections.emptyMap(), Collections.emptyMap(), null, null); StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer( this.instantiator, properties); assertThat(writeSampleJson(customizer)).contains("a") @@ -81,7 +81,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests { @Test void customizeWhenHasRenameRenamesMember() { StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(), - Collections.emptySet(), Map.of("a", "z"), Collections.emptyMap(), null); + Collections.emptySet(), Map.of("a", "z"), Collections.emptyMap(), null, null); StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer( this.instantiator, properties); assertThat(writeSampleJson(customizer)).contains("\"z\":\"a\""); @@ -90,7 +90,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests { @Test void customizeWhenHasAddAddsMemeber() { StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(), - Collections.emptySet(), Collections.emptyMap(), Map.of("z", "z"), null); + Collections.emptySet(), Collections.emptyMap(), Map.of("z", "z"), null, null); StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer( this.instantiator, properties); assertThat(writeSampleJson(customizer)).contains("\"z\":\"z\""); @@ -103,7 +103,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests { .applyingNameProcessor(NameProcessor.of(String::toUpperCase)); given(((Instantiator) this.instantiator).instantiateType(TestCustomizer.class)).willReturn(uppercaseCustomizer); StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(), - Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), Set.of(TestCustomizer.class)); + Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), TestCustomizer.class, null); StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer( this.instantiator, properties); assertThat(writeSampleJson(customizer)).contains("\"A\":\"a\""); @@ -111,17 +111,30 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests { @Test @SuppressWarnings({ "rawtypes", "unchecked" }) - void multipleCustomizers() { + void customizeWhenHasCustomizersCustomizesMember() { given(((Instantiator) this.instantiator).instantiateType(FooCustomizer.class)).willReturn(new FooCustomizer()); given(((Instantiator) this.instantiator).instantiateType(BarCustomizer.class)).willReturn(new BarCustomizer()); StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(), - Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), + Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), null, Set.of(FooCustomizer.class, BarCustomizer.class)); StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer( this.instantiator, properties); assertThat(writeSampleJson(customizer)).contains("\"foo\":\"foo\"").contains("\"bar\":\"bar\""); } + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + void customizeWhenHasCustomizerAndCustomizersCustomizesMember() { + given(((Instantiator) this.instantiator).instantiateType(FooCustomizer.class)).willReturn(new FooCustomizer()); + given(((Instantiator) this.instantiator).instantiateType(BarCustomizer.class)).willReturn(new BarCustomizer()); + StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(), + Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), FooCustomizer.class, + Set.of(BarCustomizer.class)); + StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer( + this.instantiator, properties); + assertThat(writeSampleJson(customizer)).contains("\"foo\":\"foo\"").contains("\"bar\":\"bar\""); + } + @SuppressWarnings({ "rawtypes", "unchecked" }) private String writeSampleJson(StructuredLoggingJsonMembersCustomizer customizer) { return JsonWriter.of((members) -> { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesTests.java index ceeaf63edf..26b557085d 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesTests.java @@ -48,7 +48,7 @@ class StructuredLoggingJsonPropertiesTests { environment.setProperty("logging.structured.json.customizer", TestCustomizer.class.getName()); StructuredLoggingJsonProperties properties = StructuredLoggingJsonProperties.get(environment); assertThat(properties).isEqualTo(new StructuredLoggingJsonProperties(Set.of("a", "b"), Set.of("c", "d"), - Map.of("e", "f"), Map.of("g", "h"), Set.of(TestCustomizer.class))); + Map.of("e", "f"), Map.of("g", "h"), TestCustomizer.class, null)); } @Test @@ -64,7 +64,7 @@ class StructuredLoggingJsonPropertiesTests { assertThat(RuntimeHintsPredicates.reflection().onType(StructuredLoggingJsonProperties.class)).accepts(hints); assertThat(RuntimeHintsPredicates.reflection() .onConstructor(StructuredLoggingJsonProperties.class.getDeclaredConstructor(Set.class, Set.class, Map.class, - Map.class, Set.class)) + Map.class, Class.class, Set.class)) .invoke()).accepts(hints); }