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/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 5e94ab0a6..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
@@ -240,8 +240,14 @@ public List resolveProfileSpecific(
.registerSingleton("configDataConfigClientProperties",
event.getBootstrapContext().get(ConfigClientProperties.class)));
- bootstrapContext.registerIfAbsent(ConfigClientRequestTemplateFactory.class,
- context -> new ConfigClientRequestTemplateFactory(log, context.get(ConfigClientProperties.class)));
+ bootstrapContext.registerIfAbsent(ConfigClientRequestTemplateFactory.class, context -> {
+ ConfigClientProperties props = context.get(ConfigClientProperties.class);
+ if (ClassUtils
+ .isPresent("org.springframework.boot.restclient.observation.ObservationRestTemplateCustomizer", 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..5c8dbf7e7
--- /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;
+
+ public 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
new file mode 100644
index 000000000..464d9fb27
--- /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. set up the factory
+ ConfigClientProperties properties = new ConfigClientProperties();
+ ObservationRegistry registry = ObservationRegistry.create();
+ ObservationConfigClientRequestTemplateFactory factory = new ObservationConfigClientRequestTemplateFactory(
+ LogFactory.getLog(getClass()), properties, registry);
+
+ // 2. create the RestTemplate
+ RestTemplate restTemplate = factory.create();
+
+ // 3. verify the observation registry
+ assertThat(restTemplate.getObservationRegistry()).isEqualTo(registry);
+ }
+
+ @Test
+ void shouldNotInstrumentRestTemplateWhenObservationRegistryNotProvided() {
+ // 1. set up the factory
+ ConfigClientProperties properties = new ConfigClientProperties();
+ ConfigClientRequestTemplateFactory factory = new ConfigClientRequestTemplateFactory(
+ LogFactory.getLog(getClass()), properties);
+
+ // 2. create the RestTemplate
+ RestTemplate restTemplate = factory.create();
+
+ // 3. verify the observation registry
+ assertThat(restTemplate.getObservationRegistry()).isEqualTo(ObservationRegistry.NOOP);
+ }
+
+ @Test
+ void shouldNotInstrumentRestTemplateWhenObservationRegistryIsNoop() {
+ // 1. set up the factory
+ ConfigClientProperties properties = new ConfigClientProperties();
+ ObservationRegistry registry = ObservationRegistry.NOOP;
+ ObservationConfigClientRequestTemplateFactory factory = new ObservationConfigClientRequestTemplateFactory(
+ LogFactory.getLog(getClass()), properties, registry);
+
+ // 2. create the RestTemplate
+ RestTemplate restTemplate = factory.create();
+
+ // 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..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
@@ -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,34 @@ void customizableRestTemplate() {
}
}
+ @Test
+ void customizableObservationRegistry() {
+ ConfigurableApplicationContext context = null;
+ try {
+ ObservationRegistry registry = ObservationRegistry.create();
+ context = new SpringApplicationBuilder(TestConfig.class)
+ .addBootstrapRegistryInitializer(
+ ObservationConfigServerBootstrapper.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");
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..8281fc7d2
--- /dev/null
+++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataWithoutMicrometerTests.java
@@ -0,0 +1,64 @@
+/*
+ * 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.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;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Luccas Asaphe
+ *
+ */
+@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);
+ }
+ }
+
+ @SpringBootConfiguration
+ @EnableAutoConfiguration
+ static class TestConfig {
+
+ }
+
+}