From f54dddbb1b3b703c026f3961fe9e864979e49c58 Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Thu, 16 Jan 2025 20:28:18 +0200 Subject: [PATCH] Structured logging properties have no effect in a native image Add RuntimeHints for GraylogExtendedLogFormatProperties, StructuredLoggingJsonProperties and ElasticCommonSchemaProperties properties. Add BeanFactoryInitializationAotProcessor to register RuntimeHints for a custom StructuredLoggingJsonMembersCustomizer. Closes gh-43861 Signed-off-by: Dmytro Nosan --- .../ElasticCommonSchemaProperties.java | 11 +- .../GraylogExtendedLogFormatProperties.java | 9 ++ ...BeanFactoryInitializationAotProcessor.java | 69 ++++++++++++ .../StructuredLoggingJsonProperties.java | 11 +- .../resources/META-INF/spring/aot.factories | 6 +- .../ElasticCommonSchemaPropertiesTests.java | 27 ++++- ...aylogExtendedLogFormatPropertiesTests.java | 25 +++++ ...actoryInitializationAotProcessorTests.java | 103 ++++++++++++++++++ .../StructuredLoggingJsonPropertiesTests.java | 24 +++- 9 files changed, 280 insertions(+), 5 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessor.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessorTests.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/ElasticCommonSchemaProperties.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/ElasticCommonSchemaProperties.java index 89056fc6951a..f401e549076a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/ElasticCommonSchemaProperties.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/ElasticCommonSchemaProperties.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. @@ -16,6 +16,7 @@ package org.springframework.boot.logging.structured; +import org.springframework.boot.context.properties.bind.BindableRuntimeHintsRegistrar; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.json.JsonWriter; import org.springframework.boot.json.JsonWriter.Members; @@ -91,4 +92,12 @@ Service withDefaults(Environment environment) { } + static class ElasticCommonSchemaPropertiesRuntimeHints extends BindableRuntimeHintsRegistrar { + + ElasticCommonSchemaPropertiesRuntimeHints() { + super(ElasticCommonSchemaProperties.class); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/GraylogExtendedLogFormatProperties.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/GraylogExtendedLogFormatProperties.java index 4cb0491afee8..1db65c80883a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/GraylogExtendedLogFormatProperties.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/GraylogExtendedLogFormatProperties.java @@ -16,6 +16,7 @@ package org.springframework.boot.logging.structured; +import org.springframework.boot.context.properties.bind.BindableRuntimeHintsRegistrar; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.json.JsonWriter; import org.springframework.core.env.Environment; @@ -91,4 +92,12 @@ void jsonMembers(JsonWriter.Members members) { } + static class GraylogExtendedLogFormatPropertiesRuntimeHints extends BindableRuntimeHintsRegistrar { + + GraylogExtendedLogFormatPropertiesRuntimeHints() { + super(GraylogExtendedLogFormatProperties.class); + } + + } + } 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 new file mode 100644 index 000000000000..a60819e292e4 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessor.java @@ -0,0 +1,69 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.logging.structured; + +import java.util.Optional; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; +import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.env.Environment; + +/** + * {@link BeanFactoryInitializationAotProcessor} that registers {@link RuntimeHints} for + * {@link StructuredLoggingJsonPropertiesJsonMembersCustomizer}. + * + * @author Dmytro Nosan + */ +class StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessor + implements BeanFactoryInitializationAotProcessor { + + private static final String ENVIRONMENT_BEAN_NAME = "environment"; + + @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); + } + + private static final class AotContribution implements BeanFactoryInitializationAotContribution { + + private final Class> customizer; + + private AotContribution(Class> customizer) { + this.customizer = customizer; + } + + @Override + public void applyTo(GenerationContext generationContext, + BeanFactoryInitializationCode beanFactoryInitializationCode) { + generationContext.getRuntimeHints() + .reflection() + .registerType(this.customizer, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, + MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); + } + + } + +} 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 cf34cb1a7d10..82a4f55e4091 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 @@ -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. @@ -19,6 +19,7 @@ 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.core.env.Environment; @@ -42,4 +43,12 @@ static StructuredLoggingJsonProperties get(Environment environment) { .orElse(null); } + static class StructuredLoggingJsonPropertiesRuntimeHints extends BindableRuntimeHintsRegistrar { + + StructuredLoggingJsonPropertiesRuntimeHints() { + super(StructuredLoggingJsonProperties.class); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories index e439a91ea690..f78f261e659a 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories @@ -10,13 +10,17 @@ org.springframework.boot.jdbc.DataSourceBuilderRuntimeHints,\ org.springframework.boot.json.JacksonRuntimeHints,\ org.springframework.boot.logging.java.JavaLoggingSystemRuntimeHints,\ org.springframework.boot.logging.logback.LogbackRuntimeHints,\ +org.springframework.boot.logging.structured.ElasticCommonSchemaProperties.ElasticCommonSchemaPropertiesRuntimeHints,\ +org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties.GraylogExtendedLogFormatPropertiesRuntimeHints,\ +org.springframework.boot.logging.structured.StructuredLoggingJsonProperties.StructuredLoggingJsonPropertiesRuntimeHints,\ org.springframework.boot.web.embedded.undertow.UndertowWebServer.UndertowWebServerRuntimeHints,\ org.springframework.boot.web.server.MimeMappings.MimeMappingsRuntimeHints org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\ org.springframework.boot.context.properties.ConfigurationPropertiesBeanFactoryInitializationAotProcessor,\ org.springframework.boot.env.EnvironmentPostProcessorApplicationListener.EnvironmentBeanFactoryInitializationAotProcessor,\ -org.springframework.boot.jackson.JsonComponentModule.JsonComponentBeanFactoryInitializationAotProcessor +org.springframework.boot.jackson.JsonComponentModule.JsonComponentBeanFactoryInitializationAotProcessor,\ +org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessor org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrationAotProcessor,\ diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/ElasticCommonSchemaPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/ElasticCommonSchemaPropertiesTests.java index 84eec313c8a3..7ecba4443c16 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/ElasticCommonSchemaPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/ElasticCommonSchemaPropertiesTests.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. @@ -18,7 +18,12 @@ import org.junit.jupiter.api.Test; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.beans.factory.aot.AotServices; import org.springframework.boot.json.JsonWriter; +import org.springframework.boot.logging.structured.ElasticCommonSchemaProperties.ElasticCommonSchemaPropertiesRuntimeHints; import org.springframework.boot.logging.structured.ElasticCommonSchemaProperties.Service; import org.springframework.mock.env.MockEnvironment; @@ -77,4 +82,24 @@ void addToJsonMembersCreatesValidJson() { + "\"service.environment\":\"prod\",\"service.node.name\":\"boot\"}"); } + @Test + void shouldRegisterRuntimeHints() throws Exception { + RuntimeHints hints = new RuntimeHints(); + new ElasticCommonSchemaPropertiesRuntimeHints().registerHints(hints, getClass().getClassLoader()); + assertThat(RuntimeHintsPredicates.reflection().onType(ElasticCommonSchemaProperties.class)).accepts(hints); + assertThat(RuntimeHintsPredicates.reflection() + .onConstructor(ElasticCommonSchemaProperties.class.getConstructor(Service.class)) + .invoke()).accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onType(Service.class)).accepts(hints); + assertThat(RuntimeHintsPredicates.reflection() + .onConstructor(Service.class.getConstructor(String.class, String.class, String.class, String.class)) + .invoke()).accepts(hints); + } + + @Test + void elasticCommonSchemaPropertiesRuntimeHintsIsRegistered() { + assertThat(AotServices.factories().load(RuntimeHintsRegistrar.class)) + .anyMatch(ElasticCommonSchemaPropertiesRuntimeHints.class::isInstance); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/GraylogExtendedLogFormatPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/GraylogExtendedLogFormatPropertiesTests.java index f030323647c7..74dae1e1634a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/GraylogExtendedLogFormatPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/GraylogExtendedLogFormatPropertiesTests.java @@ -18,7 +18,12 @@ import org.junit.jupiter.api.Test; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.beans.factory.aot.AotServices; import org.springframework.boot.json.JsonWriter; +import org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties.GraylogExtendedLogFormatPropertiesRuntimeHints; import org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties.Service; import org.springframework.mock.env.MockEnvironment; @@ -98,4 +103,24 @@ void addToJsonMembersCreatesValidJson() { assertThat(writer.writeToString(properties)).isEqualTo("{\"host\":\"spring\",\"_service_version\":\"1.2.3\"}"); } + @Test + void shouldRegisterRuntimeHints() throws Exception { + RuntimeHints hints = new RuntimeHints(); + new GraylogExtendedLogFormatPropertiesRuntimeHints().registerHints(hints, getClass().getClassLoader()); + assertThat(RuntimeHintsPredicates.reflection().onType(GraylogExtendedLogFormatProperties.class)).accepts(hints); + assertThat(RuntimeHintsPredicates.reflection() + .onConstructor(GraylogExtendedLogFormatProperties.class.getConstructor(String.class, Service.class)) + .invoke()).accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onType(Service.class)).accepts(hints); + assertThat(RuntimeHintsPredicates.reflection() + .onConstructor(GraylogExtendedLogFormatProperties.Service.class.getConstructor(String.class)) + .invoke()).accepts(hints); + } + + @Test + void graylogExtendedLogFormatPropertiesRuntimeHintsIsRegistered() { + assertThat(AotServices.factories().load(RuntimeHintsRegistrar.class)) + .anyMatch(GraylogExtendedLogFormatPropertiesRuntimeHints.class::isInstance); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessorTests.java new file mode 100644 index 000000000000..a09c66ddad53 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessorTests.java @@ -0,0 +1,103 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.logging.structured; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.aot.AotServices; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; +import org.springframework.boot.json.JsonWriter.Members; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for + * {@link StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessor}. + * + * @author Dmytro Nosan + */ +class StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessorTests { + + @Test + void structuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessorIsRegistered() { + assertThat(AotServices.factories().load(BeanFactoryInitializationAotProcessor.class)) + .anyMatch(StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessor.class::isInstance); + } + + @Test + void shouldRegisterStructuredLoggingJsonMembersCustomizerRuntimeHints() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("logging.structured.json.customizer", TestCustomizer.class.getName()); + + BeanFactoryInitializationAotContribution contribution = getContribution(environment); + assertThat(contribution).isNotNull(); + + RuntimeHints hints = getRuntimeHints(contribution); + assertThat(RuntimeHintsPredicates.reflection() + .onType(TestCustomizer.class) + .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, + MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)) + .accepts(hints); + } + + @Test + void shouldNotRegisterStructuredLoggingJsonMembersCustomizerRuntimeHints() { + MockEnvironment environment = new MockEnvironment(); + BeanFactoryInitializationAotContribution contribution = getContribution(environment); + assertThat(contribution).isNull(); + } + + @Test + void shouldNotRegisterStructuredLoggingJsonMembersCustomizerRuntimeHintsWhenCustomizerIsNotSet() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("logging.structured.json.exclude", "something"); + BeanFactoryInitializationAotContribution contribution = getContribution(environment); + assertThat(contribution).isNull(); + } + + private BeanFactoryInitializationAotContribution getContribution(ConfigurableEnvironment environment) { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.setEnvironment(environment); + context.refresh(); + return new StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessor() + .processAheadOfTime(context.getBeanFactory()); + } + } + + private RuntimeHints getRuntimeHints(BeanFactoryInitializationAotContribution contribution) { + TestGenerationContext generationContext = new TestGenerationContext(); + contribution.applyTo(generationContext, null); + return generationContext.getRuntimeHints(); + } + + static class TestCustomizer implements StructuredLoggingJsonMembersCustomizer { + + @Override + public void customize(Members 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 43b1510317ea..a893d4a936f7 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 @@ -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. @@ -21,7 +21,12 @@ import org.junit.jupiter.api.Test; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.beans.factory.aot.AotServices; import org.springframework.boot.json.JsonWriter.Members; +import org.springframework.boot.logging.structured.StructuredLoggingJsonProperties.StructuredLoggingJsonPropertiesRuntimeHints; import org.springframework.mock.env.MockEnvironment; import static org.assertj.core.api.Assertions.assertThat; @@ -52,6 +57,23 @@ void getWhenNoBoundPropertiesReturnsNull() { StructuredLoggingJsonProperties.get(environment); } + @Test + void shouldRegisterRuntimeHints() throws Exception { + RuntimeHints hints = new RuntimeHints(); + new StructuredLoggingJsonPropertiesRuntimeHints().registerHints(hints, getClass().getClassLoader()); + assertThat(RuntimeHintsPredicates.reflection().onType(StructuredLoggingJsonProperties.class)).accepts(hints); + assertThat(RuntimeHintsPredicates.reflection() + .onConstructor(StructuredLoggingJsonProperties.class.getDeclaredConstructor(Set.class, Set.class, Map.class, + Map.class, Class.class)) + .invoke()).accepts(hints); + } + + @Test + void structuredLoggingJsonPropertiesRuntimeHintsRuntimeHintsIsRegistered() { + assertThat(AotServices.factories().load(RuntimeHintsRegistrar.class)) + .anyMatch(StructuredLoggingJsonPropertiesRuntimeHints.class::isInstance); + } + static class TestCustomizer implements StructuredLoggingJsonMembersCustomizer { @Override