diff --git a/spring-boot-project/spring-boot-metrics/src/main/java/org/springframework/boot/metrics/autoconfigure/MetricsAspectsAutoConfiguration.java b/spring-boot-project/spring-boot-metrics/src/main/java/org/springframework/boot/metrics/autoconfigure/MetricsAspectsAutoConfiguration.java index 245f4e4fb18..5b1061551e9 100644 --- a/spring-boot-project/spring-boot-metrics/src/main/java/org/springframework/boot/metrics/autoconfigure/MetricsAspectsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-metrics/src/main/java/org/springframework/boot/metrics/autoconfigure/MetricsAspectsAutoConfiguration.java @@ -17,12 +17,13 @@ package org.springframework.boot.metrics.autoconfigure; import io.micrometer.core.aop.CountedAspect; +import io.micrometer.core.aop.CountedMeterTagAnnotationHandler; import io.micrometer.core.aop.MeterTagAnnotationHandler; import io.micrometer.core.aop.TimedAspect; import io.micrometer.core.instrument.MeterRegistry; import org.aspectj.weaver.Advice; -import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.BeanFactory; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -36,6 +37,7 @@ import org.springframework.context.annotation.Bean; * aspects. * * @author Jonatan Ivanov + * @author Dominique Villard * @since 4.0.0 */ @AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) @@ -46,17 +48,40 @@ public class MetricsAspectsAutoConfiguration { @Bean @ConditionalOnMissingBean - CountedAspect countedAspect(MeterRegistry registry) { - return new CountedAspect(registry); + CountedAspect countedAspect(MeterRegistry registry, + CountedMeterTagAnnotationHandler countedMeterTagAnnotationHandler) { + CountedAspect countedAspect = new CountedAspect(registry); + countedAspect.setMeterTagAnnotationHandler(countedMeterTagAnnotationHandler); + return countedAspect; } @Bean @ConditionalOnMissingBean - TimedAspect timedAspect(MeterRegistry registry, - ObjectProvider meterTagAnnotationHandler) { + TimedAspect timedAspect(MeterRegistry registry, MeterTagAnnotationHandler meterTagAnnotationHandler) { TimedAspect timedAspect = new TimedAspect(registry); - meterTagAnnotationHandler.ifAvailable(timedAspect::setMeterTagAnnotationHandler); + timedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler); return timedAspect; } + @Bean + @ConditionalOnMissingBean + CountedMeterTagAnnotationHandler countedMeterTagAnnotationHandler(BeanFactory beanFactory, + SpelTagValueExpressionResolver metricsTagValueExpressionResolver) { + return new CountedMeterTagAnnotationHandler(beanFactory::getBean, + (ignored) -> metricsTagValueExpressionResolver); + } + + @Bean + @ConditionalOnMissingBean + MeterTagAnnotationHandler meterTagAnnotationHandler(BeanFactory beanFactory, + SpelTagValueExpressionResolver meterTagValueExpressionResolver) { + return new MeterTagAnnotationHandler(beanFactory::getBean, (ignored) -> meterTagValueExpressionResolver); + } + + @Bean + @ConditionalOnMissingBean + SpelTagValueExpressionResolver meterTagValueExpressionResolver() { + return new SpelTagValueExpressionResolver(); + } + } diff --git a/spring-boot-project/spring-boot-metrics/src/main/java/org/springframework/boot/metrics/autoconfigure/SpelTagValueExpressionResolver.java b/spring-boot-project/spring-boot-metrics/src/main/java/org/springframework/boot/metrics/autoconfigure/SpelTagValueExpressionResolver.java new file mode 100644 index 00000000000..ca0722d97c1 --- /dev/null +++ b/spring-boot-project/spring-boot-metrics/src/main/java/org/springframework/boot/metrics/autoconfigure/SpelTagValueExpressionResolver.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2025 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.boot.metrics.autoconfigure; + +import io.micrometer.common.annotation.ValueExpressionResolver; +import io.micrometer.core.aop.MeterTag; +import io.micrometer.tracing.annotation.SpanTag; + +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.SimpleEvaluationContext; + +/** + * Evaluates a Spel expression applied to a parameter for use in {@link MeterTag} + * {@link SpanTag} Micrometer annotations. + * + * @author Dominique Villard + * @since 4.0.0 + */ +public class SpelTagValueExpressionResolver implements ValueExpressionResolver { + + @Override + public String resolve(String expression, Object parameter) { + try { + SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + ExpressionParser expressionParser = new SpelExpressionParser(); + Expression expressionToEvaluate = expressionParser.parseExpression(expression); + return expressionToEvaluate.getValue(context, parameter, String.class); + } + catch (Exception ex) { + throw new IllegalStateException("Unable to evaluate SpEL expression '%s'".formatted(expression), ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-metrics/src/test/java/org/springframework/boot/metrics/autoconfigure/MetricsAspectsAutoConfigurationTests.java b/spring-boot-project/spring-boot-metrics/src/test/java/org/springframework/boot/metrics/autoconfigure/MetricsAspectsAutoConfigurationTests.java index 3bae5c1f970..5364a5cd49b 100644 --- a/spring-boot-project/spring-boot-metrics/src/test/java/org/springframework/boot/metrics/autoconfigure/MetricsAspectsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-metrics/src/test/java/org/springframework/boot/metrics/autoconfigure/MetricsAspectsAutoConfigurationTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.metrics.autoconfigure; import io.micrometer.core.aop.CountedAspect; +import io.micrometer.core.aop.CountedMeterTagAnnotationHandler; import io.micrometer.core.aop.MeterTagAnnotationHandler; import io.micrometer.core.aop.TimedAspect; import io.micrometer.core.instrument.MeterRegistry; @@ -65,12 +66,21 @@ class MetricsAspectsAutoConfigurationTests { @Test void shouldConfigureMeterTagAnnotationHandler() { this.contextRunner.withUserConfiguration(MeterTagAnnotationHandlerConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(CountedAspect.class); + assertThat(context).hasSingleBean(TimedAspect.class); assertThat(ReflectionTestUtils.getField(context.getBean(TimedAspect.class), "meterTagAnnotationHandler")) .isSameAs(context.getBean(MeterTagAnnotationHandler.class)); }); } + @Test + void shouldConfigureCounterMeterTagAnnotationHandler() { + this.contextRunner.withUserConfiguration(MeterTagAnnotationHandlerConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(CountedAspect.class); + assertThat(ReflectionTestUtils.getField(context.getBean(CountedAspect.class), "meterTagAnnotationHandler")) + .isSameAs(context.getBean(CountedMeterTagAnnotationHandler.class)); + }); + } + @Test void shouldNotConfigureAspectsIfMicrometerIsMissing() { this.contextRunner.withClassLoader(new FilteredClassLoader(MeterRegistry.class)).run((context) -> { @@ -128,6 +138,11 @@ class MetricsAspectsAutoConfigurationTests { return new MeterTagAnnotationHandler(null, null); } + @Bean + CountedMeterTagAnnotationHandler countedMeterTagAnnotationHandler() { + return new CountedMeterTagAnnotationHandler(null, null); + } + } } diff --git a/spring-boot-project/spring-boot-metrics/src/test/java/org/springframework/boot/metrics/autoconfigure/SpelTagValueExpressionResolverTests.java b/spring-boot-project/spring-boot-metrics/src/test/java/org/springframework/boot/metrics/autoconfigure/SpelTagValueExpressionResolverTests.java new file mode 100644 index 00000000000..b2e85d55e33 --- /dev/null +++ b/spring-boot-project/spring-boot-metrics/src/test/java/org/springframework/boot/metrics/autoconfigure/SpelTagValueExpressionResolverTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2025 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.boot.metrics.autoconfigure; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.data.util.Pair; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +class SpelTagValueExpressionResolverTests { + + final SpelTagValueExpressionResolver resolver = new SpelTagValueExpressionResolver(); + + @Test + void checkValidExpression() { + var value = Map.of("foo", Pair.of(1, 2)); + assertThat(this.resolver.resolve("['foo'].first", value)).isEqualTo("1"); + } + + @Test + void checkInvalidExpression() { + var value = Map.of("foo", Pair.of(1, 2)); + assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve("['bar'].first", value)); + } + +} diff --git a/spring-boot-project/spring-boot-tracing/src/main/java/org/springframework/boot/tracing/autoconfigure/MicrometerTracingAutoConfiguration.java b/spring-boot-project/spring-boot-tracing/src/main/java/org/springframework/boot/tracing/autoconfigure/MicrometerTracingAutoConfiguration.java index b4a8db76201..547ffe472d5 100644 --- a/spring-boot-project/spring-boot-tracing/src/main/java/org/springframework/boot/tracing/autoconfigure/MicrometerTracingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-tracing/src/main/java/org/springframework/boot/tracing/autoconfigure/MicrometerTracingAutoConfiguration.java @@ -16,7 +16,6 @@ package org.springframework.boot.tracing.autoconfigure; -import io.micrometer.common.annotation.ValueExpressionResolver; import io.micrometer.tracing.Tracer; import io.micrometer.tracing.annotation.DefaultNewSpanParser; import io.micrometer.tracing.annotation.ImperativeMethodInvocationProcessor; @@ -38,15 +37,12 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.metrics.autoconfigure.SpelTagValueExpressionResolver; import org.springframework.boot.observation.autoconfigure.ObservationHandlerGroup; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; -import org.springframework.expression.Expression; -import org.springframework.expression.ExpressionParser; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.util.ClassUtils; /** @@ -122,9 +118,15 @@ public class MicrometerTracingAutoConfiguration { @Bean @ConditionalOnMissingBean - SpanTagAnnotationHandler spanTagAnnotationHandler(BeanFactory beanFactory) { - ValueExpressionResolver valueExpressionResolver = new SpelTagValueExpressionResolver(); - return new SpanTagAnnotationHandler(beanFactory::getBean, (ignored) -> valueExpressionResolver); + SpelTagValueExpressionResolver spanTagValueExpressionResolver() { + return new SpelTagValueExpressionResolver(); + } + + @Bean + @ConditionalOnMissingBean + SpanTagAnnotationHandler spanTagAnnotationHandler(BeanFactory beanFactory, + SpelTagValueExpressionResolver spanTagValueExpressionResolver) { + return new SpanTagAnnotationHandler(beanFactory::getBean, (ignored) -> spanTagValueExpressionResolver); } @Bean @@ -142,21 +144,4 @@ public class MicrometerTracingAutoConfiguration { } - private static final class SpelTagValueExpressionResolver implements ValueExpressionResolver { - - @Override - public String resolve(String expression, Object parameter) { - try { - SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); - ExpressionParser expressionParser = new SpelExpressionParser(); - Expression expressionToEvaluate = expressionParser.parseExpression(expression); - return expressionToEvaluate.getValue(context, parameter, String.class); - } - catch (Exception ex) { - throw new IllegalStateException("Unable to evaluate SpEL expression '%s'".formatted(expression), ex); - } - } - - } - } diff --git a/spring-boot-project/spring-boot-tracing/src/test/java/org/springframework/boot/tracing/autoconfigure/MicrometerTracingAutoConfigurationTests.java b/spring-boot-project/spring-boot-tracing/src/test/java/org/springframework/boot/tracing/autoconfigure/MicrometerTracingAutoConfigurationTests.java index bd096ab08b1..b3dee4df106 100644 --- a/spring-boot-project/spring-boot-tracing/src/test/java/org/springframework/boot/tracing/autoconfigure/MicrometerTracingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-tracing/src/test/java/org/springframework/boot/tracing/autoconfigure/MicrometerTracingAutoConfigurationTests.java @@ -38,6 +38,7 @@ import org.aspectj.weaver.Advice; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.metrics.autoconfigure.SpelTagValueExpressionResolver; import org.springframework.boot.observation.autoconfigure.ObservationHandlerGroup; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -110,6 +111,8 @@ class MicrometerTracingAutoConfigurationTests { assertThat(context).hasSingleBean(SpanAspect.class); assertThat(context).hasBean("customSpanTagAnnotationHandler"); assertThat(context).hasSingleBean(SpanTagAnnotationHandler.class); + assertThat(context).hasBean("customMetricsTagValueExpressionResolver"); + assertThat(context).hasSingleBean(SpelTagValueExpressionResolver.class); }); } @@ -267,6 +270,11 @@ class MicrometerTracingAutoConfigurationTests { (aClass) -> mock(ValueExpressionResolver.class)); } + @Bean + SpelTagValueExpressionResolver customMetricsTagValueExpressionResolver() { + return mock(SpelTagValueExpressionResolver.class); + } + } @Configuration(proxyBeanMethods = false)