Skip to content

Commit 8b04ace

Browse files
doumdoumwilkinsona
authored andcommitted
Add @MeterTag support to existing @timed and @counted support
Signed-off-by: Dominique Villard <[email protected]> See gh-46007
1 parent a59cf66 commit 8b04ace

File tree

6 files changed

+159
-32
lines changed

6 files changed

+159
-32
lines changed

spring-boot-project/spring-boot-metrics/src/main/java/org/springframework/boot/metrics/autoconfigure/MetricsAspectsAutoConfiguration.java

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
package org.springframework.boot.metrics.autoconfigure;
1818

1919
import io.micrometer.core.aop.CountedAspect;
20+
import io.micrometer.core.aop.CountedMeterTagAnnotationHandler;
2021
import io.micrometer.core.aop.MeterTagAnnotationHandler;
2122
import io.micrometer.core.aop.TimedAspect;
2223
import io.micrometer.core.instrument.MeterRegistry;
2324
import org.aspectj.weaver.Advice;
2425

25-
import org.springframework.beans.factory.ObjectProvider;
26+
import org.springframework.beans.factory.BeanFactory;
2627
import org.springframework.boot.autoconfigure.AutoConfiguration;
2728
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2829
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -36,6 +37,7 @@
3637
* aspects.
3738
*
3839
* @author Jonatan Ivanov
40+
* @author Dominique Villard
3941
* @since 4.0.0
4042
*/
4143
@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class })
@@ -46,17 +48,40 @@ public class MetricsAspectsAutoConfiguration {
4648

4749
@Bean
4850
@ConditionalOnMissingBean
49-
CountedAspect countedAspect(MeterRegistry registry) {
50-
return new CountedAspect(registry);
51+
CountedAspect countedAspect(MeterRegistry registry,
52+
CountedMeterTagAnnotationHandler countedMeterTagAnnotationHandler) {
53+
CountedAspect countedAspect = new CountedAspect(registry);
54+
countedAspect.setMeterTagAnnotationHandler(countedMeterTagAnnotationHandler);
55+
return countedAspect;
5156
}
5257

5358
@Bean
5459
@ConditionalOnMissingBean
55-
TimedAspect timedAspect(MeterRegistry registry,
56-
ObjectProvider<MeterTagAnnotationHandler> meterTagAnnotationHandler) {
60+
TimedAspect timedAspect(MeterRegistry registry, MeterTagAnnotationHandler meterTagAnnotationHandler) {
5761
TimedAspect timedAspect = new TimedAspect(registry);
58-
meterTagAnnotationHandler.ifAvailable(timedAspect::setMeterTagAnnotationHandler);
62+
timedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler);
5963
return timedAspect;
6064
}
6165

66+
@Bean
67+
@ConditionalOnMissingBean
68+
CountedMeterTagAnnotationHandler countedMeterTagAnnotationHandler(BeanFactory beanFactory,
69+
SpelTagValueExpressionResolver metricsTagValueExpressionResolver) {
70+
return new CountedMeterTagAnnotationHandler(beanFactory::getBean,
71+
(ignored) -> metricsTagValueExpressionResolver);
72+
}
73+
74+
@Bean
75+
@ConditionalOnMissingBean
76+
MeterTagAnnotationHandler meterTagAnnotationHandler(BeanFactory beanFactory,
77+
SpelTagValueExpressionResolver meterTagValueExpressionResolver) {
78+
return new MeterTagAnnotationHandler(beanFactory::getBean, (ignored) -> meterTagValueExpressionResolver);
79+
}
80+
81+
@Bean
82+
@ConditionalOnMissingBean
83+
SpelTagValueExpressionResolver meterTagValueExpressionResolver() {
84+
return new SpelTagValueExpressionResolver();
85+
}
86+
6287
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.metrics.autoconfigure;
18+
19+
import io.micrometer.common.annotation.ValueExpressionResolver;
20+
import io.micrometer.core.aop.MeterTag;
21+
import io.micrometer.tracing.annotation.SpanTag;
22+
23+
import org.springframework.expression.Expression;
24+
import org.springframework.expression.ExpressionParser;
25+
import org.springframework.expression.spel.standard.SpelExpressionParser;
26+
import org.springframework.expression.spel.support.SimpleEvaluationContext;
27+
28+
/**
29+
* Evaluates a Spel expression applied to a parameter for use in {@link MeterTag}
30+
* {@link SpanTag} Micrometer annotations.
31+
*
32+
* @author Dominique Villard
33+
* @since 4.0.0
34+
*/
35+
public class SpelTagValueExpressionResolver implements ValueExpressionResolver {
36+
37+
@Override
38+
public String resolve(String expression, Object parameter) {
39+
try {
40+
SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
41+
ExpressionParser expressionParser = new SpelExpressionParser();
42+
Expression expressionToEvaluate = expressionParser.parseExpression(expression);
43+
return expressionToEvaluate.getValue(context, parameter, String.class);
44+
}
45+
catch (Exception ex) {
46+
throw new IllegalStateException("Unable to evaluate SpEL expression '%s'".formatted(expression), ex);
47+
}
48+
}
49+
50+
}

spring-boot-project/spring-boot-metrics/src/test/java/org/springframework/boot/metrics/autoconfigure/MetricsAspectsAutoConfigurationTests.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.boot.metrics.autoconfigure;
1818

1919
import io.micrometer.core.aop.CountedAspect;
20+
import io.micrometer.core.aop.CountedMeterTagAnnotationHandler;
2021
import io.micrometer.core.aop.MeterTagAnnotationHandler;
2122
import io.micrometer.core.aop.TimedAspect;
2223
import io.micrometer.core.instrument.MeterRegistry;
@@ -65,12 +66,21 @@ void shouldConfigureAspects() {
6566
@Test
6667
void shouldConfigureMeterTagAnnotationHandler() {
6768
this.contextRunner.withUserConfiguration(MeterTagAnnotationHandlerConfiguration.class).run((context) -> {
68-
assertThat(context).hasSingleBean(CountedAspect.class);
69+
assertThat(context).hasSingleBean(TimedAspect.class);
6970
assertThat(ReflectionTestUtils.getField(context.getBean(TimedAspect.class), "meterTagAnnotationHandler"))
7071
.isSameAs(context.getBean(MeterTagAnnotationHandler.class));
7172
});
7273
}
7374

75+
@Test
76+
void shouldConfigureCounterMeterTagAnnotationHandler() {
77+
this.contextRunner.withUserConfiguration(MeterTagAnnotationHandlerConfiguration.class).run((context) -> {
78+
assertThat(context).hasSingleBean(CountedAspect.class);
79+
assertThat(ReflectionTestUtils.getField(context.getBean(CountedAspect.class), "meterTagAnnotationHandler"))
80+
.isSameAs(context.getBean(CountedMeterTagAnnotationHandler.class));
81+
});
82+
}
83+
7484
@Test
7585
void shouldNotConfigureAspectsIfMicrometerIsMissing() {
7686
this.contextRunner.withClassLoader(new FilteredClassLoader(MeterRegistry.class)).run((context) -> {
@@ -128,6 +138,11 @@ MeterTagAnnotationHandler meterTagAnnotationHandler() {
128138
return new MeterTagAnnotationHandler(null, null);
129139
}
130140

141+
@Bean
142+
CountedMeterTagAnnotationHandler countedMeterTagAnnotationHandler() {
143+
return new CountedMeterTagAnnotationHandler(null, null);
144+
}
145+
131146
}
132147

133148
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.metrics.autoconfigure;
18+
19+
import java.util.Map;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.data.util.Pair;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
27+
28+
class SpelTagValueExpressionResolverTests {
29+
30+
final SpelTagValueExpressionResolver resolver = new SpelTagValueExpressionResolver();
31+
32+
@Test
33+
void checkValidExpression() {
34+
var value = Map.of("foo", Pair.of(1, 2));
35+
assertThat(this.resolver.resolve("['foo'].first", value)).isEqualTo("1");
36+
}
37+
38+
@Test
39+
void checkInvalidExpression() {
40+
var value = Map.of("foo", Pair.of(1, 2));
41+
assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve("['bar'].first", value));
42+
}
43+
44+
}

spring-boot-project/spring-boot-tracing/src/main/java/org/springframework/boot/tracing/autoconfigure/MicrometerTracingAutoConfiguration.java

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

1717
package org.springframework.boot.tracing.autoconfigure;
1818

19-
import io.micrometer.common.annotation.ValueExpressionResolver;
2019
import io.micrometer.tracing.Tracer;
2120
import io.micrometer.tracing.annotation.DefaultNewSpanParser;
2221
import io.micrometer.tracing.annotation.ImperativeMethodInvocationProcessor;
@@ -38,15 +37,12 @@
3837
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
3938
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
4039
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
40+
import org.springframework.boot.metrics.autoconfigure.SpelTagValueExpressionResolver;
4141
import org.springframework.boot.observation.autoconfigure.ObservationHandlerGroup;
4242
import org.springframework.context.annotation.Bean;
4343
import org.springframework.context.annotation.Configuration;
4444
import org.springframework.core.Ordered;
4545
import org.springframework.core.annotation.Order;
46-
import org.springframework.expression.Expression;
47-
import org.springframework.expression.ExpressionParser;
48-
import org.springframework.expression.spel.standard.SpelExpressionParser;
49-
import org.springframework.expression.spel.support.SimpleEvaluationContext;
5046
import org.springframework.util.ClassUtils;
5147

5248
/**
@@ -122,9 +118,15 @@ DefaultNewSpanParser newSpanParser() {
122118

123119
@Bean
124120
@ConditionalOnMissingBean
125-
SpanTagAnnotationHandler spanTagAnnotationHandler(BeanFactory beanFactory) {
126-
ValueExpressionResolver valueExpressionResolver = new SpelTagValueExpressionResolver();
127-
return new SpanTagAnnotationHandler(beanFactory::getBean, (ignored) -> valueExpressionResolver);
121+
SpelTagValueExpressionResolver spanTagValueExpressionResolver() {
122+
return new SpelTagValueExpressionResolver();
123+
}
124+
125+
@Bean
126+
@ConditionalOnMissingBean
127+
SpanTagAnnotationHandler spanTagAnnotationHandler(BeanFactory beanFactory,
128+
SpelTagValueExpressionResolver spanTagValueExpressionResolver) {
129+
return new SpanTagAnnotationHandler(beanFactory::getBean, (ignored) -> spanTagValueExpressionResolver);
128130
}
129131

130132
@Bean
@@ -142,21 +144,4 @@ SpanAspect spanAspect(MethodInvocationProcessor methodInvocationProcessor) {
142144

143145
}
144146

145-
private static final class SpelTagValueExpressionResolver implements ValueExpressionResolver {
146-
147-
@Override
148-
public String resolve(String expression, Object parameter) {
149-
try {
150-
SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
151-
ExpressionParser expressionParser = new SpelExpressionParser();
152-
Expression expressionToEvaluate = expressionParser.parseExpression(expression);
153-
return expressionToEvaluate.getValue(context, parameter, String.class);
154-
}
155-
catch (Exception ex) {
156-
throw new IllegalStateException("Unable to evaluate SpEL expression '%s'".formatted(expression), ex);
157-
}
158-
}
159-
160-
}
161-
162147
}

spring-boot-project/spring-boot-tracing/src/test/java/org/springframework/boot/tracing/autoconfigure/MicrometerTracingAutoConfigurationTests.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.junit.jupiter.api.Test;
3939

4040
import org.springframework.boot.autoconfigure.AutoConfigurations;
41+
import org.springframework.boot.metrics.autoconfigure.SpelTagValueExpressionResolver;
4142
import org.springframework.boot.observation.autoconfigure.ObservationHandlerGroup;
4243
import org.springframework.boot.test.context.FilteredClassLoader;
4344
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -110,6 +111,8 @@ void shouldBackOffOnCustomBeans() {
110111
assertThat(context).hasSingleBean(SpanAspect.class);
111112
assertThat(context).hasBean("customSpanTagAnnotationHandler");
112113
assertThat(context).hasSingleBean(SpanTagAnnotationHandler.class);
114+
assertThat(context).hasBean("customMetricsTagValueExpressionResolver");
115+
assertThat(context).hasSingleBean(SpelTagValueExpressionResolver.class);
113116
});
114117
}
115118

@@ -267,6 +270,11 @@ SpanTagAnnotationHandler customSpanTagAnnotationHandler() {
267270
(aClass) -> mock(ValueExpressionResolver.class));
268271
}
269272

273+
@Bean
274+
SpelTagValueExpressionResolver customMetricsTagValueExpressionResolver() {
275+
return mock(SpelTagValueExpressionResolver.class);
276+
}
277+
270278
}
271279

272280
@Configuration(proxyBeanMethods = false)

0 commit comments

Comments
 (0)