From 5ce43bdfa21077fa4be764f7401f67c9e61d3644 Mon Sep 17 00:00:00 2001 From: Luccas Asaphe <167210535+LuccasAps@users.noreply.github.com> Date: Sun, 26 Apr 2026 00:57:01 -0300 Subject: [PATCH 1/6] Add ObservationRegistry support to ConfigClientRequestTemplateFactory Closes #2972 Signed-off-by: Luccas Asaphe <167210535+LuccasAps@users.noreply.github.com> --- spring-cloud-config-client/pom.xml | 5 ++ .../ConfigClientRequestTemplateFactory.java | 19 +++++ .../client/ConfigServerBootstrapper.java | 13 ++++ ...onfigServerConfigDataLocationResolver.java | 4 +- ...nfigClientRequestTemplateFactoryTests.java | 77 +++++++++++++++++++ 5 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryTests.java diff --git a/spring-cloud-config-client/pom.xml b/spring-cloud-config-client/pom.xml index c38d75265..2f92b8291 100644 --- a/spring-cloud-config-client/pom.xml +++ b/spring-cloud-config-client/pom.xml @@ -58,6 +58,11 @@ spring-boot-starter-actuator true + + org.springframework.boot + spring-boot-restclient + true + org.springframework.boot spring-boot-starter-aspectj diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java index 6ceca390f..e6926054a 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java @@ -26,6 +26,7 @@ import javax.net.ssl.SSLContext; +import io.micrometer.observation.ObservationRegistry; import org.apache.commons.logging.Log; import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; @@ -35,6 +36,7 @@ import org.apache.hc.core5.http.io.SocketConfig; import org.apache.hc.core5.util.Timeout; +import org.springframework.boot.restclient.observation.ObservationRestTemplateCustomizer; import org.springframework.cloud.configuration.SSLContextFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpRequest; @@ -44,6 +46,7 @@ import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.http.client.observation.DefaultClientRequestObservationConvention; import org.springframework.web.client.RestTemplate; import static org.springframework.cloud.config.client.ConfigClientProperties.AUTHORIZATION; @@ -54,9 +57,20 @@ public class ConfigClientRequestTemplateFactory { private final ConfigClientProperties properties; + private final ObservationRegistry observationRegistry; + public ConfigClientRequestTemplateFactory(Log log, ConfigClientProperties properties) { this.log = log; this.properties = properties; + this.observationRegistry = ObservationRegistry.NOOP; + + } + + public ConfigClientRequestTemplateFactory(Log log, ConfigClientProperties properties, + ObservationRegistry observationRegistry) { + this.log = log; + this.properties = properties; + this.observationRegistry = observationRegistry; } public Log getLog() { @@ -83,6 +97,11 @@ public RestTemplate create() { template.setInterceptors(Arrays.asList(new GenericRequestHeaderInterceptor(headers))); } + if (observationRegistry != null && observationRegistry != ObservationRegistry.NOOP) { + new ObservationRestTemplateCustomizer(observationRegistry, new DefaultClientRequestObservationConvention()) + .customize(template); + } + return template; } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerBootstrapper.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerBootstrapper.java index a95a26275..721d114a7 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerBootstrapper.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerBootstrapper.java @@ -19,6 +19,8 @@ import java.util.function.BiFunction; import java.util.function.Function; +import io.micrometer.observation.ObservationRegistry; + import org.springframework.boot.bootstrap.BootstrapContext; import org.springframework.boot.bootstrap.BootstrapRegistry; import org.springframework.boot.bootstrap.BootstrapRegistry.InstanceSupplier; @@ -35,6 +37,8 @@ public class ConfigServerBootstrapper implements BootstrapRegistryInitializer { private LoaderInterceptor loaderInterceptor; + private ObservationRegistry observationRegistry; + static ConfigServerBootstrapper create() { return new ConfigServerBootstrapper(); } @@ -46,6 +50,11 @@ public ConfigServerBootstrapper withRestTemplateFactory( return this; } + public ConfigServerBootstrapper withObservationRegistry(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + return this; + } + public ConfigServerBootstrapper withLoaderInterceptor(LoaderInterceptor loaderInterceptor) { this.loaderInterceptor = loaderInterceptor; return this; @@ -53,6 +62,10 @@ public ConfigServerBootstrapper withLoaderInterceptor(LoaderInterceptor loaderIn @Override public void initialize(BootstrapRegistry registry) { + + if (observationRegistry != null) { + registry.register(ObservationRegistry.class, InstanceSupplier.of(observationRegistry)); + } if (restTemplateFactory != null) { registry.register(RestTemplate.class, restTemplateFactory::apply); } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java index 5e94ab0a6..07fdd6387 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java @@ -21,6 +21,7 @@ import java.util.Properties; import java.util.function.Supplier; +import io.micrometer.observation.ObservationRegistry; import org.apache.commons.logging.Log; import org.springframework.boot.bootstrap.BootstrapRegistry; @@ -241,7 +242,8 @@ public List resolveProfileSpecific( event.getBootstrapContext().get(ConfigClientProperties.class))); bootstrapContext.registerIfAbsent(ConfigClientRequestTemplateFactory.class, - context -> new ConfigClientRequestTemplateFactory(log, context.get(ConfigClientProperties.class))); + context -> new ConfigClientRequestTemplateFactory(log, context.get(ConfigClientProperties.class), + context.getOrElse(ObservationRegistry.class, ObservationRegistry.NOOP))); bootstrapContext.registerIfAbsent(RestTemplate.class, context -> { ConfigClientRequestTemplateFactory factory = context.get(ConfigClientRequestTemplateFactory.class); diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryTests.java new file mode 100644 index 000000000..7c6e7a32f --- /dev/null +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2026-present 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.cloud.config.client; + +import io.micrometer.observation.ObservationRegistry; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.Test; + +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Luccas Asaphe + * + */ +public class ConfigClientRequestTemplateFactoryTests { + + @Test + void shouldInstrumentRestTemplateWhenObservationRegistryProvided() { + // 1. montar a factory + ConfigClientProperties properties = new ConfigClientProperties(); + ObservationRegistry registry = ObservationRegistry.create(); + ConfigClientRequestTemplateFactory factory = new ConfigClientRequestTemplateFactory( + LogFactory.getLog(getClass()), properties, registry); + + // 2. criar o RestTemplate + RestTemplate restTemplate = factory.create(); + + // 3. verificar os interceptors + assertThat(restTemplate.getObservationRegistry()).isEqualTo(registry); + } + + @Test + void shouldNotInstrumentRestTemplateWhenObservationRegistryNotProvided() { + // 1. montar a factory + ConfigClientProperties properties = new ConfigClientProperties(); + ConfigClientRequestTemplateFactory factory = new ConfigClientRequestTemplateFactory( + LogFactory.getLog(getClass()), properties); + + // 2. criar o RestTemplate + RestTemplate restTemplate = factory.create(); + + // 3. verificar os interceptors + assertThat(restTemplate.getObservationRegistry()).isEqualTo(ObservationRegistry.NOOP); + } + + @Test + void shouldNotInstrumentRestTemplateWhenObservationRegistryIsNoop() { + // 1. montar a factory + ConfigClientProperties properties = new ConfigClientProperties(); + ObservationRegistry registry = ObservationRegistry.NOOP; + ConfigClientRequestTemplateFactory factory = new ConfigClientRequestTemplateFactory( + LogFactory.getLog(getClass()), properties, ObservationRegistry.NOOP); + + // 2. criar o RestTemplate + RestTemplate restTemplate = factory.create(); + + // 3. verificar os interceptors + assertThat(restTemplate.getObservationRegistry()).isEqualTo(ObservationRegistry.NOOP); + } + +} From 4cecc0cfed5a32c6f0964f8d8f45054cec3a9a67 Mon Sep 17 00:00:00 2001 From: Luccas Asaphe <167210535+LuccasAps@users.noreply.github.com> Date: Wed, 29 Apr 2026 01:24:39 -0300 Subject: [PATCH 2/6] Address review feedback Signed-off-by: Luccas Asaphe <167210535+LuccasAps@users.noreply.github.com> --- .../ConfigClientRequestTemplateFactory.java | 14 ++++++---- ...nfigClientRequestTemplateFactoryTests.java | 20 ++++++------- ...nfigDataCustomizationIntegrationTests.java | 28 +++++++++++++++++++ 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java index e6926054a..8beb1355d 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java @@ -57,12 +57,12 @@ public class ConfigClientRequestTemplateFactory { private final ConfigClientProperties properties; - private final ObservationRegistry observationRegistry; + private final ObservationRestTemplateCustomizer observationRestTemplateCustomizer; public ConfigClientRequestTemplateFactory(Log log, ConfigClientProperties properties) { this.log = log; this.properties = properties; - this.observationRegistry = ObservationRegistry.NOOP; + this.observationRestTemplateCustomizer = null; } @@ -70,7 +70,10 @@ public ConfigClientRequestTemplateFactory(Log log, ConfigClientProperties proper ObservationRegistry observationRegistry) { this.log = log; this.properties = properties; - this.observationRegistry = observationRegistry; + this.observationRestTemplateCustomizer = observationRegistry != ObservationRegistry.NOOP + ? new ObservationRestTemplateCustomizer(observationRegistry, + new DefaultClientRequestObservationConvention()) + : null; } public Log getLog() { @@ -97,9 +100,8 @@ public RestTemplate create() { template.setInterceptors(Arrays.asList(new GenericRequestHeaderInterceptor(headers))); } - if (observationRegistry != null && observationRegistry != ObservationRegistry.NOOP) { - new ObservationRestTemplateCustomizer(observationRegistry, new DefaultClientRequestObservationConvention()) - .customize(template); + if (observationRestTemplateCustomizer != null) { + observationRestTemplateCustomizer.customize(template); } return template; diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryTests.java index 7c6e7a32f..6e7daed72 100644 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryTests.java +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryTests.java @@ -32,45 +32,45 @@ public class ConfigClientRequestTemplateFactoryTests { @Test void shouldInstrumentRestTemplateWhenObservationRegistryProvided() { - // 1. montar a factory + // 1. set up the factory ConfigClientProperties properties = new ConfigClientProperties(); ObservationRegistry registry = ObservationRegistry.create(); ConfigClientRequestTemplateFactory factory = new ConfigClientRequestTemplateFactory( LogFactory.getLog(getClass()), properties, registry); - // 2. criar o RestTemplate + // 2. create the RestTemplate RestTemplate restTemplate = factory.create(); - // 3. verificar os interceptors + // 3. verify the observation registry assertThat(restTemplate.getObservationRegistry()).isEqualTo(registry); } @Test void shouldNotInstrumentRestTemplateWhenObservationRegistryNotProvided() { - // 1. montar a factory + // 1. set up the factory ConfigClientProperties properties = new ConfigClientProperties(); ConfigClientRequestTemplateFactory factory = new ConfigClientRequestTemplateFactory( LogFactory.getLog(getClass()), properties); - // 2. criar o RestTemplate + // 2. create the RestTemplate RestTemplate restTemplate = factory.create(); - // 3. verificar os interceptors + // 3. verify the observation registry assertThat(restTemplate.getObservationRegistry()).isEqualTo(ObservationRegistry.NOOP); } @Test void shouldNotInstrumentRestTemplateWhenObservationRegistryIsNoop() { - // 1. montar a factory + // 1. set up the factory ConfigClientProperties properties = new ConfigClientProperties(); ObservationRegistry registry = ObservationRegistry.NOOP; ConfigClientRequestTemplateFactory factory = new ConfigClientRequestTemplateFactory( - LogFactory.getLog(getClass()), properties, ObservationRegistry.NOOP); + LogFactory.getLog(getClass()), properties, registry); - // 2. criar o RestTemplate + // 2. create the RestTemplate RestTemplate restTemplate = factory.create(); - // 3. verificar os interceptors + // 3. verify the observation registry assertThat(restTemplate.getObservationRegistry()).isEqualTo(ObservationRegistry.NOOP); } diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataCustomizationIntegrationTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataCustomizationIntegrationTests.java index 17aea6ed5..1f1f99549 100644 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataCustomizationIntegrationTests.java +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataCustomizationIntegrationTests.java @@ -18,6 +18,7 @@ import java.util.Optional; +import io.micrometer.observation.ObservationRegistry; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -91,6 +92,33 @@ void customizableRestTemplate() { } } + @Test + void customizableObservationRegistry() { + ConfigurableApplicationContext context = null; + try { + ObservationRegistry registry = ObservationRegistry.create(); + context = new SpringApplicationBuilder(TestConfig.class) + .addBootstrapRegistryInitializer(ConfigServerBootstrapper.create().withObservationRegistry(registry)) + .addBootstrapRegistryInitializer(reg -> reg.addCloseListener(event -> { + BootstrapContext bootstrapContext = event.getBootstrapContext(); + ConfigurableListableBeanFactory beanFactory = event.getApplicationContext().getBeanFactory(); + + RestTemplate restTemplate = bootstrapContext.get(RestTemplate.class); + beanFactory.registerSingleton("holder", new RestTemplateHolder(restTemplate)); + })) + .run("--spring.config.import=optional:configserver:"); + + RestTemplateHolder holder = context.getBean(RestTemplateHolder.class); + assertThat(holder).isNotNull(); + assertThat(holder.restTemplate.getObservationRegistry()).isEqualTo(registry); + } + finally { + if (context != null) { + context.close(); + } + } + } + CustomRestTemplate restTemplate(BootstrapContext context) { ConfigClientProperties properties = context.get(ConfigClientProperties.class); String custom = context.get(Binder.class).bind("custom.prop", String.class).orElse("default-custom-prop"); From 4eb48eeb1fd2be8ef9edee5ec4ddc9fe57c546dc Mon Sep 17 00:00:00 2001 From: Luccas Asaphe <167210535+LuccasAps@users.noreply.github.com> Date: Wed, 29 Apr 2026 02:20:55 -0300 Subject: [PATCH 3/6] Refactor to use subclasses for Micrometer observation support Signed-off-by: Luccas Asaphe <167210535+LuccasAps@users.noreply.github.com> --- .../ConfigClientRequestTemplateFactory.java | 21 ------- .../client/ConfigServerBootstrapper.java | 13 ----- ...onfigServerConfigDataLocationResolver.java | 11 ++-- ...ionConfigClientRequestTemplateFactory.java | 55 +++++++++++++++++++ .../ObservationConfigServerBootstrapper.java | 44 +++++++++++++++ ...nfigClientRequestTemplateFactoryTests.java | 4 +- ...nfigDataCustomizationIntegrationTests.java | 3 +- 7 files changed, 110 insertions(+), 41 deletions(-) create mode 100644 spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ObservationConfigClientRequestTemplateFactory.java create mode 100644 spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ObservationConfigServerBootstrapper.java diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java index 8beb1355d..6ceca390f 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java @@ -26,7 +26,6 @@ import javax.net.ssl.SSLContext; -import io.micrometer.observation.ObservationRegistry; import org.apache.commons.logging.Log; import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; @@ -36,7 +35,6 @@ import org.apache.hc.core5.http.io.SocketConfig; import org.apache.hc.core5.util.Timeout; -import org.springframework.boot.restclient.observation.ObservationRestTemplateCustomizer; import org.springframework.cloud.configuration.SSLContextFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpRequest; @@ -46,7 +44,6 @@ import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; -import org.springframework.http.client.observation.DefaultClientRequestObservationConvention; import org.springframework.web.client.RestTemplate; import static org.springframework.cloud.config.client.ConfigClientProperties.AUTHORIZATION; @@ -57,23 +54,9 @@ public class ConfigClientRequestTemplateFactory { private final ConfigClientProperties properties; - private final ObservationRestTemplateCustomizer observationRestTemplateCustomizer; - public ConfigClientRequestTemplateFactory(Log log, ConfigClientProperties properties) { this.log = log; this.properties = properties; - this.observationRestTemplateCustomizer = null; - - } - - public ConfigClientRequestTemplateFactory(Log log, ConfigClientProperties properties, - ObservationRegistry observationRegistry) { - this.log = log; - this.properties = properties; - this.observationRestTemplateCustomizer = observationRegistry != ObservationRegistry.NOOP - ? new ObservationRestTemplateCustomizer(observationRegistry, - new DefaultClientRequestObservationConvention()) - : null; } public Log getLog() { @@ -100,10 +83,6 @@ public RestTemplate create() { template.setInterceptors(Arrays.asList(new GenericRequestHeaderInterceptor(headers))); } - if (observationRestTemplateCustomizer != null) { - observationRestTemplateCustomizer.customize(template); - } - return template; } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerBootstrapper.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerBootstrapper.java index 721d114a7..a95a26275 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerBootstrapper.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerBootstrapper.java @@ -19,8 +19,6 @@ import java.util.function.BiFunction; import java.util.function.Function; -import io.micrometer.observation.ObservationRegistry; - import org.springframework.boot.bootstrap.BootstrapContext; import org.springframework.boot.bootstrap.BootstrapRegistry; import org.springframework.boot.bootstrap.BootstrapRegistry.InstanceSupplier; @@ -37,8 +35,6 @@ public class ConfigServerBootstrapper implements BootstrapRegistryInitializer { private LoaderInterceptor loaderInterceptor; - private ObservationRegistry observationRegistry; - static ConfigServerBootstrapper create() { return new ConfigServerBootstrapper(); } @@ -50,11 +46,6 @@ public ConfigServerBootstrapper withRestTemplateFactory( return this; } - public ConfigServerBootstrapper withObservationRegistry(ObservationRegistry observationRegistry) { - this.observationRegistry = observationRegistry; - return this; - } - public ConfigServerBootstrapper withLoaderInterceptor(LoaderInterceptor loaderInterceptor) { this.loaderInterceptor = loaderInterceptor; return this; @@ -62,10 +53,6 @@ public ConfigServerBootstrapper withLoaderInterceptor(LoaderInterceptor loaderIn @Override public void initialize(BootstrapRegistry registry) { - - if (observationRegistry != null) { - registry.register(ObservationRegistry.class, InstanceSupplier.of(observationRegistry)); - } if (restTemplateFactory != null) { registry.register(RestTemplate.class, restTemplateFactory::apply); } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java index 07fdd6387..152b8a49a 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java @@ -21,7 +21,6 @@ import java.util.Properties; import java.util.function.Supplier; -import io.micrometer.observation.ObservationRegistry; import org.apache.commons.logging.Log; import org.springframework.boot.bootstrap.BootstrapRegistry; @@ -241,9 +240,13 @@ public List resolveProfileSpecific( .registerSingleton("configDataConfigClientProperties", event.getBootstrapContext().get(ConfigClientProperties.class))); - bootstrapContext.registerIfAbsent(ConfigClientRequestTemplateFactory.class, - context -> new ConfigClientRequestTemplateFactory(log, context.get(ConfigClientProperties.class), - context.getOrElse(ObservationRegistry.class, ObservationRegistry.NOOP))); + bootstrapContext.registerIfAbsent(ConfigClientRequestTemplateFactory.class, context -> { + ConfigClientProperties props = context.get(ConfigClientProperties.class); + if (ClassUtils.isPresent("io.micrometer.observation.ObservationRegistry", null)) { + return ObservationConfigClientRequestTemplateFactory.createWithObservation(context, log, props); + } + return new ConfigClientRequestTemplateFactory(log, props); + }); bootstrapContext.registerIfAbsent(RestTemplate.class, context -> { ConfigClientRequestTemplateFactory factory = context.get(ConfigClientRequestTemplateFactory.class); diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ObservationConfigClientRequestTemplateFactory.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ObservationConfigClientRequestTemplateFactory.java new file mode 100644 index 000000000..8e3f58ce0 --- /dev/null +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ObservationConfigClientRequestTemplateFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright 2026-present 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.cloud.config.client; + +import io.micrometer.observation.ObservationRegistry; +import org.apache.commons.logging.Log; + +import org.springframework.boot.bootstrap.BootstrapContext; +import org.springframework.boot.restclient.observation.ObservationRestTemplateCustomizer; +import org.springframework.http.client.observation.DefaultClientRequestObservationConvention; +import org.springframework.web.client.RestTemplate; + +public class ObservationConfigClientRequestTemplateFactory extends ConfigClientRequestTemplateFactory { + + private final ObservationRestTemplateCustomizer observationRestTemplateCustomizer; + + public ObservationConfigClientRequestTemplateFactory(Log log, ConfigClientProperties properties, + ObservationRegistry observationRegistry) { + super(log, properties); + this.observationRestTemplateCustomizer = observationRegistry != ObservationRegistry.NOOP + ? new ObservationRestTemplateCustomizer(observationRegistry, + new DefaultClientRequestObservationConvention()) + : null; + } + + @Override + public RestTemplate create() { + RestTemplate template = super.create(); + if (observationRestTemplateCustomizer != null) { + observationRestTemplateCustomizer.customize(template); + } + return template; + } + + static ConfigClientRequestTemplateFactory createWithObservation(BootstrapContext context, Log log, + ConfigClientProperties props) { + ObservationRegistry registry = context.getOrElse(ObservationRegistry.class, ObservationRegistry.NOOP); + return new ObservationConfigClientRequestTemplateFactory(log, props, registry); + } + +} diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ObservationConfigServerBootstrapper.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ObservationConfigServerBootstrapper.java new file mode 100644 index 000000000..a214ccd41 --- /dev/null +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ObservationConfigServerBootstrapper.java @@ -0,0 +1,44 @@ +/* + * Copyright 2026-present 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.cloud.config.client; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.boot.bootstrap.BootstrapRegistry; + +public class ObservationConfigServerBootstrapper extends ConfigServerBootstrapper { + + private ObservationRegistry observationRegistry; + + static ObservationConfigServerBootstrapper create() { + return new ObservationConfigServerBootstrapper(); + } + + public ObservationConfigServerBootstrapper withObservationRegistry(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + return this; + } + + @Override + public void initialize(BootstrapRegistry registry) { + super.initialize(registry); + if (observationRegistry != null) { + registry.register(ObservationRegistry.class, BootstrapRegistry.InstanceSupplier.of(observationRegistry)); + } + } + +} diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryTests.java index 6e7daed72..464d9fb27 100644 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryTests.java +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryTests.java @@ -35,7 +35,7 @@ void shouldInstrumentRestTemplateWhenObservationRegistryProvided() { // 1. set up the factory ConfigClientProperties properties = new ConfigClientProperties(); ObservationRegistry registry = ObservationRegistry.create(); - ConfigClientRequestTemplateFactory factory = new ConfigClientRequestTemplateFactory( + ObservationConfigClientRequestTemplateFactory factory = new ObservationConfigClientRequestTemplateFactory( LogFactory.getLog(getClass()), properties, registry); // 2. create the RestTemplate @@ -64,7 +64,7 @@ void shouldNotInstrumentRestTemplateWhenObservationRegistryIsNoop() { // 1. set up the factory ConfigClientProperties properties = new ConfigClientProperties(); ObservationRegistry registry = ObservationRegistry.NOOP; - ConfigClientRequestTemplateFactory factory = new ConfigClientRequestTemplateFactory( + ObservationConfigClientRequestTemplateFactory factory = new ObservationConfigClientRequestTemplateFactory( LogFactory.getLog(getClass()), properties, registry); // 2. create the RestTemplate diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataCustomizationIntegrationTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataCustomizationIntegrationTests.java index 1f1f99549..b207afa57 100644 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataCustomizationIntegrationTests.java +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataCustomizationIntegrationTests.java @@ -98,7 +98,8 @@ void customizableObservationRegistry() { try { ObservationRegistry registry = ObservationRegistry.create(); context = new SpringApplicationBuilder(TestConfig.class) - .addBootstrapRegistryInitializer(ConfigServerBootstrapper.create().withObservationRegistry(registry)) + .addBootstrapRegistryInitializer( + ObservationConfigServerBootstrapper.create().withObservationRegistry(registry)) .addBootstrapRegistryInitializer(reg -> reg.addCloseListener(event -> { BootstrapContext bootstrapContext = event.getBootstrapContext(); ConfigurableListableBeanFactory beanFactory = event.getApplicationContext().getBeanFactory(); From 8097f33912875bc297be3fa70604499f6ded5508 Mon Sep 17 00:00:00 2001 From: Luccas Asaphe <167210535+LuccasAps@users.noreply.github.com> Date: Wed, 29 Apr 2026 18:05:34 -0300 Subject: [PATCH 4/6] Add check for ObservationRestTemplateCustomizer Signed-off-by: Luccas Asaphe <167210535+LuccasAps@users.noreply.github.com> --- .../config/client/ConfigServerConfigDataLocationResolver.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java index 152b8a49a..9eb8e50df 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java @@ -242,7 +242,8 @@ public List resolveProfileSpecific( bootstrapContext.registerIfAbsent(ConfigClientRequestTemplateFactory.class, context -> { ConfigClientProperties props = context.get(ConfigClientProperties.class); - if (ClassUtils.isPresent("io.micrometer.observation.ObservationRegistry", null)) { + if (ClassUtils.isPresent("io.micrometer.observation.ObservationRegistry", null) && ClassUtils + .isPresent("org.springframework.boot.restclient.observation.ObservationRestTemplateCustomizer", null)) { return ObservationConfigClientRequestTemplateFactory.createWithObservation(context, log, props); } return new ConfigClientRequestTemplateFactory(log, props); From f4e56271c93e08b73d2cf4f9b6150d6a805be949 Mon Sep 17 00:00:00 2001 From: Luccas Asaphe <167210535+LuccasAps@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:39:50 -0300 Subject: [PATCH 5/6] Add test for context startup without spring-boot-restclient on classpath Signed-off-by: Luccas Asaphe <167210535+LuccasAps@users.noreply.github.com> --- .../client/ConfigServerBootstrapper.java | 2 +- ...onfigServerConfigDataLocationResolver.java | 2 +- .../ObservationConfigServerBootstrapper.java | 2 +- ...erverConfigDataWithoutMicrometerTests.java | 52 +++++++++++++++++++ 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataWithoutMicrometerTests.java diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerBootstrapper.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerBootstrapper.java index a95a26275..6f1d30e32 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerBootstrapper.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerBootstrapper.java @@ -35,7 +35,7 @@ public class ConfigServerBootstrapper implements BootstrapRegistryInitializer { private LoaderInterceptor loaderInterceptor; - static ConfigServerBootstrapper create() { + public static ConfigServerBootstrapper create() { return new ConfigServerBootstrapper(); } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java index 9eb8e50df..62111d566 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java @@ -242,7 +242,7 @@ public List resolveProfileSpecific( bootstrapContext.registerIfAbsent(ConfigClientRequestTemplateFactory.class, context -> { ConfigClientProperties props = context.get(ConfigClientProperties.class); - if (ClassUtils.isPresent("io.micrometer.observation.ObservationRegistry", null) && ClassUtils + if (ClassUtils .isPresent("org.springframework.boot.restclient.observation.ObservationRestTemplateCustomizer", null)) { return ObservationConfigClientRequestTemplateFactory.createWithObservation(context, log, props); } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ObservationConfigServerBootstrapper.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ObservationConfigServerBootstrapper.java index a214ccd41..5c8dbf7e7 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ObservationConfigServerBootstrapper.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ObservationConfigServerBootstrapper.java @@ -24,7 +24,7 @@ public class ObservationConfigServerBootstrapper extends ConfigServerBootstrappe private ObservationRegistry observationRegistry; - static ObservationConfigServerBootstrapper create() { + public static ObservationConfigServerBootstrapper create() { return new ObservationConfigServerBootstrapper(); } diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataWithoutMicrometerTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataWithoutMicrometerTests.java new file mode 100644 index 000000000..8f1848ba7 --- /dev/null +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataWithoutMicrometerTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2026-present 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.cloud.config.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.test.ClassPathExclusions; +import org.springframework.context.ConfigurableApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Luccas Asaphe + * + */ +@ClassPathExclusions({ "spring-boot-starter-actuator-*.jar", "spring-boot-restclient-*.jar" }) +public class ConfigServerConfigDataWithoutMicrometerTests { + + @Test + void contextStartsWithoutMicrometer() { + try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestConfig.class) + .web(WebApplicationType.NONE) + .run("--spring.config.import=optional:configserver:")) { + assertThat(context).isNotNull(); + } + } + + @SpringBootConfiguration + @EnableAutoConfiguration + static class TestConfig { + + } + +} From b41d10803476ba9bd64f3b03860830ecc2e60dcf Mon Sep 17 00:00:00 2001 From: Luccas Asaphe <167210535+LuccasAps@users.noreply.github.com> Date: Fri, 1 May 2026 01:05:52 -0300 Subject: [PATCH 6/6] Update test to verify correct bean is created without restclient on classpath Signed-off-by: Luccas Asaphe <167210535+LuccasAps@users.noreply.github.com> --- ...nfigServerConfigDataWithoutMicrometerTests.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataWithoutMicrometerTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataWithoutMicrometerTests.java index 8f1848ba7..8281fc7d2 100644 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataWithoutMicrometerTests.java +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataWithoutMicrometerTests.java @@ -18,9 +18,11 @@ import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.bootstrap.BootstrapContext; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.test.ClassPathExclusions; import org.springframework.context.ConfigurableApplicationContext; @@ -31,15 +33,25 @@ * @author Luccas Asaphe * */ -@ClassPathExclusions({ "spring-boot-starter-actuator-*.jar", "spring-boot-restclient-*.jar" }) +@ClassPathExclusions({ "spring-boot-restclient-*.jar" }) public class ConfigServerConfigDataWithoutMicrometerTests { @Test void contextStartsWithoutMicrometer() { try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestConfig.class) .web(WebApplicationType.NONE) + .addBootstrapRegistryInitializer(registry -> registry.addCloseListener(event -> { + BootstrapContext bootstrapContext = event.getBootstrapContext(); + ConfigurableListableBeanFactory beanFactory = event.getApplicationContext().getBeanFactory(); + + ConfigClientRequestTemplateFactory templateFactory = bootstrapContext + .get(ConfigClientRequestTemplateFactory.class); + beanFactory.registerSingleton("factory", templateFactory); + })) .run("--spring.config.import=optional:configserver:")) { assertThat(context).isNotNull(); + + assertThat(context.getBean("factory")).isNotInstanceOf(ObservationConfigClientRequestTemplateFactory.class); } }