Skip to content

Commit ca15966

Browse files
xiang17trask
andauthored
Fix Spring Boot 4 Micrometer auto-configuration and add actuator metrics smoke coverage (#4672)
Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com>
1 parent fcc7be6 commit ca15966

10 files changed

Lines changed: 207 additions & 7 deletions

File tree

agent/instrumentation/micrometer-1.0/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/ai/ActuatorInstrumentation.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616

1717
public final class ActuatorInstrumentation implements TypeInstrumentation {
1818

19+
private static final String SPRING_BOOT_3_METRICS_AUTO_CONFIGURATION =
20+
"org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration";
21+
private static final String SPRING_BOOT_4_METRICS_AUTO_CONFIGURATION =
22+
"org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration";
23+
1924
@Override
2025
public ElementMatcher<TypeDescription> typeMatcher() {
2126
return named("org.springframework.boot.autoconfigure.AutoConfigurationImportSelector");
@@ -36,8 +41,11 @@ public static class GetCandidateConfigurationsAdvice {
3641

3742
@Advice.OnMethodExit(suppress = Throwable.class)
3843
public static void onExit(@Advice.Return(readOnly = false) List<String> configurations) {
39-
if (configurations.contains(
40-
"org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration")) {
44+
// guard against re-adding AzureMonitorAutoConfiguration if this advice runs more than once
45+
// on the same list (e.g. nested/repeated invocations of getCandidateConfigurations)
46+
if ((configurations.contains(SPRING_BOOT_3_METRICS_AUTO_CONFIGURATION)
47+
|| configurations.contains(SPRING_BOOT_4_METRICS_AUTO_CONFIGURATION))
48+
&& !configurations.contains(AzureMonitorAutoConfiguration.class.getName())) {
4149
List<String> configs = new ArrayList<>(configurations.size() + 1);
4250
configs.addAll(configurations);
4351
// using class reference here so that muzzle will consider it a dependency of this advice

agent/instrumentation/micrometer-1.0/src/main/java/io/opentelemetry/javaagent/instrumentation/micrometer/ai/AzureMonitorAutoConfiguration.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44
package io.opentelemetry.javaagent.instrumentation.micrometer.ai;
55

66
import io.micrometer.core.instrument.Clock;
7-
import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration;
8-
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
9-
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
107
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
118
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
129
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -15,12 +12,22 @@
1512
import org.springframework.context.annotation.Configuration;
1613

1714
@Configuration(proxyBeanMethods = false)
18-
@AutoConfigureBefore(CompositeMeterRegistryAutoConfiguration.class)
15+
@AutoConfigureBefore(
16+
name = {
17+
"org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration",
18+
"org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration"
19+
})
1920
// configure after SimpleMeterRegistry is registered, otherwise SimpleMeterRegistry will be
2021
// suppressed by the existence of the MeterRegistry created here, which can alter the spring boot
2122
// actuator scraping endpoint behavior (since AzureMonitorMeterRegistry is a delta
2223
// StepMeterRegistry, while SimpleMeterRegistry is cumulative)
23-
@AutoConfigureAfter({MetricsAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class})
24+
@AutoConfigureAfter(
25+
name = {
26+
"org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration",
27+
"org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration",
28+
"org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration",
29+
"org.springframework.boot.micrometer.metrics.autoconfigure.export.simple.SimpleMetricsExportAutoConfiguration"
30+
})
2431
@ConditionalOnBean(Clock.class)
2532
@ConditionalOnClass(AzureMonitorMeterRegistry.class)
2633
public class AzureMonitorAutoConfiguration {

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ include(":smoke-tests:framework")
5959

6060
// TODO (trask) consider un-hiding these and running smoke tests against the latest versions
6161
hideFromDependabot(":smoke-tests:apps:ActuatorMetrics")
62+
hideFromDependabot(":smoke-tests:apps:ActuatorMetricsSpringBoot4")
6263
hideFromDependabot(":smoke-tests:apps:AutoPerfCounters")
6364
hideFromDependabot(":smoke-tests:apps:AzureSdk")
6465
hideFromDependabot(":smoke-tests:apps:AzureFunctions")
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import com.microsoft.applicationinsights.gradle.AiSmokeTestExtension
2+
3+
plugins {
4+
id("ai.smoke-test")
5+
id("org.springframework.boot") version "4.0.0"
6+
}
7+
8+
// The ai.smoke-test convention plugin applies `resolutionStrategy.force` on every
9+
// configuration to pin logback-classic to 1.2.12 and the slf4j bridges (slf4j-api,
10+
// log4j-over-slf4j, jcl-over-slf4j, jul-to-slf4j) to 1.7.36 (needed by the older
11+
// Spring Boot smoke-test apps). Spring Boot 4 requires logback 1.5.x and slf4j 2.x
12+
// and fails at startup with AbstractMethodError against SB4's RootLogLevelConfigurator
13+
// otherwise. Re-force the newer versions here to override the convention's force,
14+
// including all slf4j bridges so they stay binary-compatible with slf4j-api 2.x.
15+
configurations.configureEach {
16+
resolutionStrategy.force(
17+
"ch.qos.logback:logback-classic:1.5.21",
18+
"ch.qos.logback:logback-core:1.5.21",
19+
"org.slf4j:slf4j-api:2.0.17",
20+
"org.slf4j:log4j-over-slf4j:2.0.17",
21+
"org.slf4j:jcl-over-slf4j:2.0.17",
22+
"org.slf4j:jul-to-slf4j:2.0.17"
23+
)
24+
}
25+
26+
dependencies {
27+
implementation("org.springframework.boot:spring-boot-starter-web:4.0.0")
28+
implementation("org.springframework.boot:spring-boot-starter-actuator:4.0.0")
29+
implementation("org.springframework.boot:spring-boot-starter-micrometer-metrics:4.0.0")
30+
}
31+
32+
val aiSmokeTest = extensions.getByType(AiSmokeTestExtension::class)
33+
aiSmokeTest.testAppArtifactDir.set(tasks.bootJar.flatMap { it.destinationDirectory })
34+
aiSmokeTest.testAppArtifactFilename.set(tasks.bootJar.flatMap { it.archiveFileName })
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.applicationinsights.smoketestapp;
5+
6+
import org.springframework.boot.SpringApplication;
7+
import org.springframework.boot.autoconfigure.SpringBootApplication;
8+
9+
@SpringBootApplication
10+
public class SpringBootApp {
11+
12+
public static void main(String[] args) {
13+
SpringApplication.run(SpringBootApp.class, args);
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.applicationinsights.smoketestapp;
5+
6+
import io.micrometer.core.instrument.Counter;
7+
import io.micrometer.core.instrument.MeterRegistry;
8+
import org.springframework.web.bind.annotation.GetMapping;
9+
import org.springframework.web.bind.annotation.RestController;
10+
11+
@RestController
12+
public class TestController {
13+
14+
private final Counter counter;
15+
16+
public TestController(MeterRegistry meterRegistry) {
17+
this.counter =
18+
Counter.builder("demo.requests.total").tag("endpoint", "test").register(meterRegistry);
19+
}
20+
21+
@GetMapping("/")
22+
public String root() {
23+
return "OK";
24+
}
25+
26+
@GetMapping("/test")
27+
public String test() {
28+
counter.increment();
29+
return "OK!";
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.applicationinsights.smoketest;
5+
6+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.JAVA_17;
7+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.JAVA_17_OPENJ9;
8+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.JAVA_21;
9+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.JAVA_21_OPENJ9;
10+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.JAVA_25;
11+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.JAVA_25_OPENJ9;
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
14+
import com.microsoft.applicationinsights.smoketest.schemav2.Data;
15+
import com.microsoft.applicationinsights.smoketest.schemav2.DataPoint;
16+
import com.microsoft.applicationinsights.smoketest.schemav2.Envelope;
17+
import com.microsoft.applicationinsights.smoketest.schemav2.MetricData;
18+
import java.util.List;
19+
import java.util.concurrent.TimeUnit;
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.api.extension.RegisterExtension;
22+
23+
@UseAgent
24+
abstract class ActuatorMetricsSpringBoot4Test {
25+
26+
@RegisterExtension static final SmokeTestExtension testing = SmokeTestExtension.create();
27+
28+
@Test
29+
@TargetUri("/test")
30+
void shouldCaptureCustomMetricRegisteredOnSpringMeterRegistry() throws Exception {
31+
testing.getTelemetry(0);
32+
33+
List<Envelope> metricItems =
34+
testing.mockedIngestion.waitForItems(
35+
ActuatorMetricsSpringBoot4Test::isTargetMetric, 1, 20, TimeUnit.SECONDS);
36+
37+
MetricData data = (MetricData) ((Data<?>) metricItems.get(0).getData()).getBaseData();
38+
List<DataPoint> points = data.getMetrics();
39+
40+
assertThat(points)
41+
.anySatisfy(point -> assertThat(point.getName()).isEqualTo("demo_requests_total"));
42+
assertThat(data.getProperties()).containsEntry("endpoint", "test");
43+
}
44+
45+
static boolean isTargetMetric(Envelope input) {
46+
if (!input.getData().getBaseType().equals("MetricData")) {
47+
return false;
48+
}
49+
MetricData data = (MetricData) ((Data<?>) input.getData()).getBaseData();
50+
for (DataPoint point : data.getMetrics()) {
51+
if ("demo_requests_total".equals(point.getName()) && point.getValue() >= 1) {
52+
return true;
53+
}
54+
}
55+
return false;
56+
}
57+
58+
@Environment(JAVA_17)
59+
static class Java17Test extends ActuatorMetricsSpringBoot4Test {}
60+
61+
@Environment(JAVA_17_OPENJ9)
62+
static class Java17OpenJ9Test extends ActuatorMetricsSpringBoot4Test {}
63+
64+
@Environment(JAVA_21)
65+
static class Java21Test extends ActuatorMetricsSpringBoot4Test {}
66+
67+
@Environment(JAVA_21_OPENJ9)
68+
static class Java21OpenJ9Test extends ActuatorMetricsSpringBoot4Test {}
69+
70+
@Environment(JAVA_25)
71+
static class Java25Test extends ActuatorMetricsSpringBoot4Test {}
72+
73+
@Environment(JAVA_25_OPENJ9)
74+
static class Java25OpenJ9Test extends ActuatorMetricsSpringBoot4Test {}
75+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"role": {
3+
"name": "testrolename",
4+
"instance": "testroleinstance"
5+
},
6+
"sampling": {
7+
"percentage": 100
8+
},
9+
"metricIntervalSeconds": 5
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<configuration>
3+
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
4+
<encoder>
5+
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern>
6+
</encoder>
7+
</appender>
8+
<root level="warn">
9+
<appender-ref ref="CONSOLE"/>
10+
</root>
11+
</configuration>

smoke-tests/apps/Micrometer/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/MicrometerTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.JAVA_17_OPENJ9;
1010
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.JAVA_21;
1111
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.JAVA_21_OPENJ9;
12+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.JAVA_25;
13+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.JAVA_25_OPENJ9;
1214
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.JAVA_8;
1315
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.JAVA_8_OPENJ9;
1416
import static org.assertj.core.api.Assertions.assertThat;
@@ -99,4 +101,10 @@ static class Java21Test extends MicrometerTest {}
99101

100102
@Environment(JAVA_21_OPENJ9)
101103
static class Java21OpenJ9Test extends MicrometerTest {}
104+
105+
@Environment(JAVA_25)
106+
static class Java25Test extends MicrometerTest {}
107+
108+
@Environment(JAVA_25_OPENJ9)
109+
static class Java25OpenJ9Test extends MicrometerTest {}
102110
}

0 commit comments

Comments
 (0)