Skip to content

Commit 2ba97aa

Browse files
committed
Fix metrics autoconfiguration ordering for Spring Boot 4 (#2758)
1 parent 2a6b190 commit 2ba97aa

4 files changed

Lines changed: 128 additions & 3 deletions

File tree

temporal-spring-boot-autoconfigure/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ dependencies {
4646
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
4747

4848
testImplementation "org.springframework.boot:spring-boot-starter-test"
49+
// For auto-configuration ordering tests (metrics)
50+
testImplementation "org.springframework.boot:spring-boot-starter-actuator"
51+
// For auto-configuration ordering tests (OpenTelemetry)
52+
testImplementation "io.opentelemetry:opentelemetry-sdk"
4953

5054
if (project.hasProperty("springBoot4Test")) {
5155
// Override the SB 2.7 BOM for test classpath so tests run against SB 4.
@@ -55,6 +59,10 @@ dependencies {
5559
// SB4 requires SLF4J 2.x + Logback 1.5+; let the SB4 BOM provide them
5660
// instead of using the root project's pinned versions.
5761
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
62+
// SB4 modularized auto-configs into separate modules that
63+
// spring-boot-starter-actuator no longer transitively includes.
64+
testImplementation "org.springframework.boot:spring-boot-micrometer-metrics"
65+
testImplementation "org.springframework.boot:spring-boot-opentelemetry"
5866
} else {
5967
testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: "${logbackVersion}"
6068
testImplementation('org.slf4j:slf4j-api') {

temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/MetricsScopeAutoConfiguration.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414

1515
@Configuration
1616
@AutoConfigureAfter(
17-
name =
18-
"org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration")
17+
name = {
18+
"org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration",
19+
"org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration"
20+
})
1921
@ConditionalOnBean(MeterRegistry.class)
2022
public class MetricsScopeAutoConfiguration {
2123
@Bean(name = "temporalMetricsScope", destroyMethod = "close")

temporal-spring-boot-autoconfigure/src/main/java/io/temporal/spring/boot/autoconfigure/OpenTracingAutoConfiguration.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@
1414
@Configuration
1515
@ConditionalOnClass(io.opentelemetry.api.OpenTelemetry.class)
1616
@ConditionalOnBean(io.opentelemetry.api.OpenTelemetry.class)
17-
@AutoConfigureAfter(name = "org.springframework.cloud.sleuth.autoconfig.otel.OtelAutoConfiguration")
17+
@AutoConfigureAfter(
18+
name = {
19+
"org.springframework.cloud.sleuth.autoconfig.otel.OtelAutoConfiguration",
20+
"org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration",
21+
"org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration",
22+
"org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration"
23+
})
1824
public class OpenTracingAutoConfiguration {
1925
@ConditionalOnMissingBean(Tracer.class)
2026
@Bean(name = "temporalOtTracer")
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package io.temporal.spring.boot.autoconfigure;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
5+
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
import org.junit.jupiter.api.Test;
9+
import org.springframework.boot.autoconfigure.AutoConfigurations;
10+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
11+
12+
/**
13+
* Tests that {@link MetricsScopeAutoConfiguration} and {@link OpenTracingAutoConfiguration}
14+
* correctly order themselves after their dependency auto-configurations via
15+
* {@code @AutoConfigureAfter}.
16+
*
17+
* <p>Without correct ordering, {@code @ConditionalOnBean} evaluates before the dependency beans
18+
* exist, silently skipping our entire auto-configuration. This happens because our FQCNs ({@code
19+
* io.temporal...}) sort alphabetically before Spring's ({@code org.springframework...}), which is
20+
* the default processing order when no ordering constraints are specified.
21+
*/
22+
class AutoConfigOrderingTest {
23+
24+
/**
25+
* Try to load a class by name, returning the first match found. Returns null if none of the class
26+
* names exist on the classpath.
27+
*/
28+
private static Class<?> findClass(String... classNames) {
29+
for (String name : classNames) {
30+
try {
31+
return Class.forName(name);
32+
} catch (ClassNotFoundException ignored) {
33+
}
34+
}
35+
return null;
36+
}
37+
38+
/**
39+
* Verifies that {@link MetricsScopeAutoConfiguration} runs after {@code
40+
* CompositeMeterRegistryAutoConfiguration} and finds the {@code MeterRegistry} bean it creates.
41+
* The {@code CompositeMeterRegistryAutoConfiguration} class moved packages in Spring Boot 4 —
42+
* both old and new names must be in {@code @AutoConfigureAfter}.
43+
*/
44+
@Test
45+
void metricsScopeCreatedWithAutoConfigOrdering() {
46+
Class<?> compositeClass =
47+
findClass(
48+
// SB 2.7 / 3.x
49+
"org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration",
50+
// SB 4.0
51+
"org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration");
52+
53+
assumeTrue(compositeClass != null, "CompositeMeterRegistryAutoConfiguration not on classpath");
54+
55+
// MetricsAutoConfiguration provides the Clock bean that CompositeMeterRegistryAutoConfiguration
56+
// inner configs need to activate.
57+
List<Class<?>> autoConfigs = new ArrayList<>();
58+
autoConfigs.add(MetricsScopeAutoConfiguration.class);
59+
autoConfigs.add(compositeClass);
60+
Class<?> metricsAutoConfig =
61+
findClass(
62+
"org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration",
63+
"org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration");
64+
if (metricsAutoConfig != null) {
65+
autoConfigs.add(metricsAutoConfig);
66+
}
67+
68+
new ApplicationContextRunner()
69+
.withConfiguration(AutoConfigurations.of(autoConfigs.toArray(new Class<?>[0])))
70+
.run(
71+
context -> {
72+
assertThat(context).hasNotFailed();
73+
assertThat(context).hasBean("temporalMetricsScope");
74+
});
75+
}
76+
77+
/**
78+
* Verifies that {@link OpenTracingAutoConfiguration} runs after the OpenTelemetry
79+
* auto-configuration and finds the {@code OpenTelemetry} bean it creates. The producing class
80+
* moved across Spring Boot versions:
81+
*
82+
* <ul>
83+
* <li>SB 3.2+: {@code actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration}
84+
* <li>SB 4.0: {@code opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration}
85+
* </ul>
86+
*
87+
* <p>On SB 2.7 (no native OTel auto-config) this test is skipped.
88+
*/
89+
@Test
90+
void openTracingTracerCreatedWithAutoConfigOrdering() {
91+
Class<?> otelAutoConfig =
92+
findClass(
93+
// SB 3.2+ (generic OTel auto-config, uses ObjectProvider for optional deps)
94+
"org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration",
95+
// SB 4.0 (relocated to separate module)
96+
"org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration");
97+
98+
assumeTrue(otelAutoConfig != null, "OpenTelemetry auto-configuration not on classpath");
99+
100+
new ApplicationContextRunner()
101+
.withConfiguration(
102+
AutoConfigurations.of(OpenTracingAutoConfiguration.class, otelAutoConfig))
103+
.run(
104+
context -> {
105+
assertThat(context).hasNotFailed();
106+
assertThat(context).hasBean("temporalOtTracer");
107+
});
108+
}
109+
}

0 commit comments

Comments
 (0)