Add @MeterTag support to existing @Timed and @Counted support

Signed-off-by: Dominique Villard <dominique.villard@doctolib.com>

See gh-46007
This commit is contained in:
Dominique Villard 2025-06-18 11:02:30 +02:00 committed by Andy Wilkinson
parent a59cf66555
commit 8b04ace139
6 changed files with 159 additions and 32 deletions

View File

@ -17,12 +17,13 @@
package org.springframework.boot.metrics.autoconfigure; package org.springframework.boot.metrics.autoconfigure;
import io.micrometer.core.aop.CountedAspect; import io.micrometer.core.aop.CountedAspect;
import io.micrometer.core.aop.CountedMeterTagAnnotationHandler;
import io.micrometer.core.aop.MeterTagAnnotationHandler; import io.micrometer.core.aop.MeterTagAnnotationHandler;
import io.micrometer.core.aop.TimedAspect; import io.micrometer.core.aop.TimedAspect;
import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.MeterRegistry;
import org.aspectj.weaver.Advice; 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.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@ -36,6 +37,7 @@ import org.springframework.context.annotation.Bean;
* aspects. * aspects.
* *
* @author Jonatan Ivanov * @author Jonatan Ivanov
* @author Dominique Villard
* @since 4.0.0 * @since 4.0.0
*/ */
@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) @AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class })
@ -46,17 +48,40 @@ public class MetricsAspectsAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
CountedAspect countedAspect(MeterRegistry registry) { CountedAspect countedAspect(MeterRegistry registry,
return new CountedAspect(registry); CountedMeterTagAnnotationHandler countedMeterTagAnnotationHandler) {
CountedAspect countedAspect = new CountedAspect(registry);
countedAspect.setMeterTagAnnotationHandler(countedMeterTagAnnotationHandler);
return countedAspect;
} }
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
TimedAspect timedAspect(MeterRegistry registry, TimedAspect timedAspect(MeterRegistry registry, MeterTagAnnotationHandler meterTagAnnotationHandler) {
ObjectProvider<MeterTagAnnotationHandler> meterTagAnnotationHandler) {
TimedAspect timedAspect = new TimedAspect(registry); TimedAspect timedAspect = new TimedAspect(registry);
meterTagAnnotationHandler.ifAvailable(timedAspect::setMeterTagAnnotationHandler); timedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler);
return timedAspect; 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();
}
} }

View File

@ -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);
}
}
}

View File

@ -17,6 +17,7 @@
package org.springframework.boot.metrics.autoconfigure; package org.springframework.boot.metrics.autoconfigure;
import io.micrometer.core.aop.CountedAspect; import io.micrometer.core.aop.CountedAspect;
import io.micrometer.core.aop.CountedMeterTagAnnotationHandler;
import io.micrometer.core.aop.MeterTagAnnotationHandler; import io.micrometer.core.aop.MeterTagAnnotationHandler;
import io.micrometer.core.aop.TimedAspect; import io.micrometer.core.aop.TimedAspect;
import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.MeterRegistry;
@ -65,12 +66,21 @@ class MetricsAspectsAutoConfigurationTests {
@Test @Test
void shouldConfigureMeterTagAnnotationHandler() { void shouldConfigureMeterTagAnnotationHandler() {
this.contextRunner.withUserConfiguration(MeterTagAnnotationHandlerConfiguration.class).run((context) -> { 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")) assertThat(ReflectionTestUtils.getField(context.getBean(TimedAspect.class), "meterTagAnnotationHandler"))
.isSameAs(context.getBean(MeterTagAnnotationHandler.class)); .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 @Test
void shouldNotConfigureAspectsIfMicrometerIsMissing() { void shouldNotConfigureAspectsIfMicrometerIsMissing() {
this.contextRunner.withClassLoader(new FilteredClassLoader(MeterRegistry.class)).run((context) -> { this.contextRunner.withClassLoader(new FilteredClassLoader(MeterRegistry.class)).run((context) -> {
@ -128,6 +138,11 @@ class MetricsAspectsAutoConfigurationTests {
return new MeterTagAnnotationHandler(null, null); return new MeterTagAnnotationHandler(null, null);
} }
@Bean
CountedMeterTagAnnotationHandler countedMeterTagAnnotationHandler() {
return new CountedMeterTagAnnotationHandler(null, null);
}
} }
} }

View File

@ -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));
}
}

View File

@ -16,7 +16,6 @@
package org.springframework.boot.tracing.autoconfigure; package org.springframework.boot.tracing.autoconfigure;
import io.micrometer.common.annotation.ValueExpressionResolver;
import io.micrometer.tracing.Tracer; import io.micrometer.tracing.Tracer;
import io.micrometer.tracing.annotation.DefaultNewSpanParser; import io.micrometer.tracing.annotation.DefaultNewSpanParser;
import io.micrometer.tracing.annotation.ImperativeMethodInvocationProcessor; 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.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.metrics.autoconfigure.SpelTagValueExpressionResolver;
import org.springframework.boot.observation.autoconfigure.ObservationHandlerGroup; import org.springframework.boot.observation.autoconfigure.ObservationHandlerGroup;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order; 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; import org.springframework.util.ClassUtils;
/** /**
@ -122,9 +118,15 @@ public class MicrometerTracingAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
SpanTagAnnotationHandler spanTagAnnotationHandler(BeanFactory beanFactory) { SpelTagValueExpressionResolver spanTagValueExpressionResolver() {
ValueExpressionResolver valueExpressionResolver = new SpelTagValueExpressionResolver(); return new SpelTagValueExpressionResolver();
return new SpanTagAnnotationHandler(beanFactory::getBean, (ignored) -> valueExpressionResolver); }
@Bean
@ConditionalOnMissingBean
SpanTagAnnotationHandler spanTagAnnotationHandler(BeanFactory beanFactory,
SpelTagValueExpressionResolver spanTagValueExpressionResolver) {
return new SpanTagAnnotationHandler(beanFactory::getBean, (ignored) -> spanTagValueExpressionResolver);
} }
@Bean @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);
}
}
}
} }

View File

@ -38,6 +38,7 @@ import org.aspectj.weaver.Advice;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.metrics.autoconfigure.SpelTagValueExpressionResolver;
import org.springframework.boot.observation.autoconfigure.ObservationHandlerGroup; import org.springframework.boot.observation.autoconfigure.ObservationHandlerGroup;
import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@ -110,6 +111,8 @@ class MicrometerTracingAutoConfigurationTests {
assertThat(context).hasSingleBean(SpanAspect.class); assertThat(context).hasSingleBean(SpanAspect.class);
assertThat(context).hasBean("customSpanTagAnnotationHandler"); assertThat(context).hasBean("customSpanTagAnnotationHandler");
assertThat(context).hasSingleBean(SpanTagAnnotationHandler.class); assertThat(context).hasSingleBean(SpanTagAnnotationHandler.class);
assertThat(context).hasBean("customMetricsTagValueExpressionResolver");
assertThat(context).hasSingleBean(SpelTagValueExpressionResolver.class);
}); });
} }
@ -267,6 +270,11 @@ class MicrometerTracingAutoConfigurationTests {
(aClass) -> mock(ValueExpressionResolver.class)); (aClass) -> mock(ValueExpressionResolver.class));
} }
@Bean
SpelTagValueExpressionResolver customMetricsTagValueExpressionResolver() {
return mock(SpelTagValueExpressionResolver.class);
}
} }
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)