Skip to content

Commit

Permalink
Fix binding of structured logging properties in a native image
Browse files Browse the repository at this point in the history
Add RuntimeHints for GraylogExtendedLogFormatProperties,
StructuredLoggingJsonProperties and ElasticCommonSchemaProperties
properties.

Add BeanFactoryInitializationAotProcessor to register RuntimeHints
for a custom StructuredLoggingJsonMembersCustomizer.

See gh-43862

Signed-off-by: Dmytro Nosan <dimanosan@gmail.com>
  • Loading branch information
nosan authored and wilkinsona committed Jan 21, 2025
1 parent 9036249 commit 0a83bcd
Show file tree
Hide file tree
Showing 9 changed files with 280 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -91,4 +92,12 @@ Service withDefaults(Environment environment) {

}

static class ElasticCommonSchemaPropertiesRuntimeHints extends BindableRuntimeHintsRegistrar {

ElasticCommonSchemaPropertiesRuntimeHints() {
super(ElasticCommonSchemaProperties.class);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -91,4 +92,12 @@ void jsonMembers(JsonWriter.Members<?> members) {

}

static class GraylogExtendedLogFormatPropertiesRuntimeHints extends BindableRuntimeHintsRegistrar {

GraylogExtendedLogFormatPropertiesRuntimeHints() {
super(GraylogExtendedLogFormatProperties.class);
}

}

}
Original file line number Diff line number Diff line change
@@ -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<? extends StructuredLoggingJsonMembersCustomizer<?>> customizer;

private AotContribution(Class<? extends StructuredLoggingJsonMembersCustomizer<?>> 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);
}

}

}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;

Expand All @@ -42,4 +43,12 @@ static StructuredLoggingJsonProperties get(Environment environment) {
.orElse(null);
}

static class StructuredLoggingJsonPropertiesRuntimeHints extends BindableRuntimeHintsRegistrar {

StructuredLoggingJsonPropertiesRuntimeHints() {
super(StructuredLoggingJsonProperties.class);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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,\
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;

Expand Down Expand Up @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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<String> {

@Override
public void customize(Members<String> members) {
}

}

}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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<String> {

@Override
Expand Down

0 comments on commit 0a83bcd

Please sign in to comment.