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

See gh-46007
This commit is contained in:
Andy Wilkinson 2025-07-02 14:33:30 +01:00
parent 8b04ace139
commit 88f4af7577
8 changed files with 124 additions and 83 deletions

View File

@ -16,6 +16,7 @@
package org.springframework.boot.metrics.autoconfigure;
import io.micrometer.common.annotation.ValueExpressionResolver;
import io.micrometer.core.aop.CountedAspect;
import io.micrometer.core.aop.CountedMeterTagAnnotationHandler;
import io.micrometer.core.aop.MeterTagAnnotationHandler;
@ -24,12 +25,14 @@ import io.micrometer.core.instrument.MeterRegistry;
import org.aspectj.weaver.Advice;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
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.observation.autoconfigure.ObservationAutoConfiguration;
import org.springframework.context.annotation.Bean;
/**
@ -40,7 +43,8 @@ import org.springframework.context.annotation.Bean;
* @author Dominique Villard
* @since 4.0.0
*/
@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class })
@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class,
ObservationAutoConfiguration.class })
@ConditionalOnClass({ MeterRegistry.class, Advice.class })
@ConditionalOnBooleanProperty("management.observations.annotations.enabled")
@ConditionalOnBean(MeterRegistry.class)
@ -49,39 +53,38 @@ public class MetricsAspectsAutoConfiguration {
@Bean
@ConditionalOnMissingBean
CountedAspect countedAspect(MeterRegistry registry,
CountedMeterTagAnnotationHandler countedMeterTagAnnotationHandler) {
ObjectProvider<CountedMeterTagAnnotationHandler> countedMeterTagAnnotationHandler) {
CountedAspect countedAspect = new CountedAspect(registry);
countedAspect.setMeterTagAnnotationHandler(countedMeterTagAnnotationHandler);
countedMeterTagAnnotationHandler.ifAvailable(countedAspect::setMeterTagAnnotationHandler);
return countedAspect;
}
@Bean
@ConditionalOnMissingBean
TimedAspect timedAspect(MeterRegistry registry, MeterTagAnnotationHandler meterTagAnnotationHandler) {
TimedAspect timedAspect(MeterRegistry registry,
ObjectProvider<MeterTagAnnotationHandler> meterTagAnnotationHandler) {
TimedAspect timedAspect = new TimedAspect(registry);
timedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler);
meterTagAnnotationHandler.ifAvailable(timedAspect::setMeterTagAnnotationHandler);
return timedAspect;
}
@Bean
@ConditionalOnMissingBean
CountedMeterTagAnnotationHandler countedMeterTagAnnotationHandler(BeanFactory beanFactory,
SpelTagValueExpressionResolver metricsTagValueExpressionResolver) {
return new CountedMeterTagAnnotationHandler(beanFactory::getBean,
(ignored) -> metricsTagValueExpressionResolver);
}
@ConditionalOnBean(ValueExpressionResolver.class)
static class TagAnnotationHandlersConfiguration {
@Bean
@ConditionalOnMissingBean
MeterTagAnnotationHandler meterTagAnnotationHandler(BeanFactory beanFactory,
SpelTagValueExpressionResolver meterTagValueExpressionResolver) {
return new MeterTagAnnotationHandler(beanFactory::getBean, (ignored) -> meterTagValueExpressionResolver);
}
@Bean
@ConditionalOnMissingBean
CountedMeterTagAnnotationHandler countedMeterTagAnnotationHandler(BeanFactory beanFactory,
ValueExpressionResolver valueExpressionResolver) {
return new CountedMeterTagAnnotationHandler(beanFactory::getBean, (ignored) -> valueExpressionResolver);
}
@Bean
@ConditionalOnMissingBean
MeterTagAnnotationHandler meterTagAnnotationHandler(BeanFactory beanFactory,
ValueExpressionResolver valueExpressionResolver) {
return new MeterTagAnnotationHandler(beanFactory::getBean, (ignored) -> valueExpressionResolver);
}
@Bean
@ConditionalOnMissingBean
SpelTagValueExpressionResolver meterTagValueExpressionResolver() {
return new SpelTagValueExpressionResolver();
}
}

View File

@ -16,6 +16,7 @@
package org.springframework.boot.metrics.autoconfigure;
import io.micrometer.common.annotation.ValueExpressionResolver;
import io.micrometer.core.aop.CountedAspect;
import io.micrometer.core.aop.CountedMeterTagAnnotationHandler;
import io.micrometer.core.aop.MeterTagAnnotationHandler;
@ -33,6 +34,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link MetricsAspectsAutoConfiguration}.
@ -64,21 +66,53 @@ class MetricsAspectsAutoConfigurationTests {
}
@Test
void shouldConfigureMeterTagAnnotationHandler() {
this.contextRunner.withUserConfiguration(MeterTagAnnotationHandlerConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(TimedAspect.class);
assertThat(ReflectionTestUtils.getField(context.getBean(TimedAspect.class), "meterTagAnnotationHandler"))
.isSameAs(context.getBean(MeterTagAnnotationHandler.class));
});
void shouldAutoConfigureMeterTagAnnotationHandlerWhenValueExpressionResolverIsAvailable() {
this.contextRunner.withBean(ValueExpressionResolver.class, () -> mock(ValueExpressionResolver.class))
.run((context) -> {
assertThat(context).hasSingleBean(TimedAspect.class).hasSingleBean(MeterTagAnnotationHandler.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));
});
void shouldUseUserDefinedMeterTagAnnotationHandler() {
this.contextRunner
.withBean("customMeterTagAnnotationHandler", MeterTagAnnotationHandler.class,
() -> new MeterTagAnnotationHandler(null, null))
.run((context) -> {
assertThat(context).hasSingleBean(TimedAspect.class).hasSingleBean(MeterTagAnnotationHandler.class);
assertThat(
ReflectionTestUtils.getField(context.getBean(TimedAspect.class), "meterTagAnnotationHandler"))
.isSameAs(context.getBean("customMeterTagAnnotationHandler"));
});
}
@Test
void shouldAutoConfigureCountedMeterTagAnnotationHandlerWhenValueExpressionResolverIsAvailable() {
this.contextRunner.withBean(ValueExpressionResolver.class, () -> mock(ValueExpressionResolver.class))
.run((context) -> {
assertThat(context).hasSingleBean(CountedAspect.class)
.hasSingleBean(CountedMeterTagAnnotationHandler.class);
assertThat(
ReflectionTestUtils.getField(context.getBean(CountedAspect.class), "meterTagAnnotationHandler"))
.isSameAs(context.getBean(CountedMeterTagAnnotationHandler.class));
});
}
@Test
void shouldUseUserDefinedCountedMeterTagAnnotationHandler() {
this.contextRunner
.withBean("customCountedMeterTagAnnotationHandler", CountedMeterTagAnnotationHandler.class,
() -> new CountedMeterTagAnnotationHandler(null, null))
.run((context) -> {
assertThat(context).hasSingleBean(CountedAspect.class)
.hasSingleBean(CountedMeterTagAnnotationHandler.class);
assertThat(
ReflectionTestUtils.getField(context.getBean(CountedAspect.class), "meterTagAnnotationHandler"))
.isSameAs(context.getBean(CountedMeterTagAnnotationHandler.class));
});
}
@Test
@ -130,19 +164,4 @@ class MetricsAspectsAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class MeterTagAnnotationHandlerConfiguration {
@Bean
MeterTagAnnotationHandler meterTagAnnotationHandler() {
return new MeterTagAnnotationHandler(null, null);
}
@Bean
CountedMeterTagAnnotationHandler countedMeterTagAnnotationHandler() {
return new CountedMeterTagAnnotationHandler(null, null);
}
}
}

View File

@ -16,6 +16,7 @@
package org.springframework.boot.observation.autoconfigure;
import io.micrometer.common.annotation.ValueExpressionResolver;
import io.micrometer.observation.GlobalObservationConvention;
import io.micrometer.observation.ObservationFilter;
import io.micrometer.observation.ObservationHandler;
@ -73,6 +74,12 @@ public class ObservationAutoConfiguration {
return new PropertiesObservationFilterPredicate(properties);
}
@Bean
@ConditionalOnMissingBean(ValueExpressionResolver.class)
SpelValueExpressionResolver spelValueExpressionResolver() {
return new SpelValueExpressionResolver();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Advice.class)
@ConditionalOnBooleanProperty("management.observations.annotations.enabled")

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2025 the original author or authors.
* Copyright 2012-present 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.
@ -14,11 +14,9 @@
* limitations under the License.
*/
package org.springframework.boot.metrics.autoconfigure;
package org.springframework.boot.observation.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;
@ -26,13 +24,11 @@ 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.
* A {@link Expression SpEL}-based {@link ValueExpressionResolver}.
*
* @author Dominique Villard
* @since 4.0.0
*/
public class SpelTagValueExpressionResolver implements ValueExpressionResolver {
class SpelValueExpressionResolver implements ValueExpressionResolver {
@Override
public String resolve(String expression, Object parameter) {

View File

@ -18,6 +18,7 @@ package org.springframework.boot.observation.autoconfigure;
import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import io.micrometer.common.annotation.ValueExpressionResolver;
import io.micrometer.observation.GlobalObservationConvention;
import io.micrometer.observation.Observation;
import io.micrometer.observation.Observation.Context;
@ -39,6 +40,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ObservationAutoConfiguration}.
@ -163,6 +165,18 @@ class ObservationAutoConfigurationTests {
});
}
@Test
void autoConfiguresValueExpressionResolver() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(SpelValueExpressionResolver.class));
}
@Test
void allowsUserDefinedValueExpressionResolver() {
this.contextRunner.withBean(ValueExpressionResolver.class, () -> mock(ValueExpressionResolver.class))
.run((context) -> assertThat(context).hasSingleBean(ValueExpressionResolver.class)
.doesNotHaveBean(SpelValueExpressionResolver.class));
}
@Configuration(proxyBeanMethods = false)
static class ObservationPredicates {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2025 the original author or authors.
* Copyright 2012-present 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.
@ -14,20 +14,23 @@
* limitations under the License.
*/
package org.springframework.boot.metrics.autoconfigure;
package org.springframework.boot.observation.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 {
/**
* Tests for {@link SpelValueExpressionResolver}.
*
* @author Dominique Villard
*/
class SpelValueExpressionResolverTests {
final SpelTagValueExpressionResolver resolver = new SpelTagValueExpressionResolver();
final SpelValueExpressionResolver resolver = new SpelValueExpressionResolver();
@Test
void checkValidExpression() {
@ -41,4 +44,12 @@ class SpelTagValueExpressionResolverTests {
assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve("['bar'].first", value));
}
record Pair(int first, int second) {
static Pair of(int first, int second) {
return new Pair(first, second);
}
}
}

View File

@ -16,6 +16,7 @@
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;
@ -31,13 +32,14 @@ import io.micrometer.tracing.propagation.Propagator;
import org.aspectj.weaver.Advice;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
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.ObservationAutoConfiguration;
import org.springframework.boot.observation.autoconfigure.ObservationHandlerGroup;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -52,7 +54,7 @@ import org.springframework.util.ClassUtils;
* @author Jonatan Ivanov
* @since 4.0.0
*/
@AutoConfiguration
@AutoConfiguration(after = ObservationAutoConfiguration.class)
@ConditionalOnBean(Tracer.class)
public class MicrometerTracingAutoConfiguration {
@ -118,22 +120,18 @@ public class MicrometerTracingAutoConfiguration {
@Bean
@ConditionalOnMissingBean
SpelTagValueExpressionResolver spanTagValueExpressionResolver() {
return new SpelTagValueExpressionResolver();
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(ValueExpressionResolver.class)
SpanTagAnnotationHandler spanTagAnnotationHandler(BeanFactory beanFactory,
SpelTagValueExpressionResolver spanTagValueExpressionResolver) {
return new SpanTagAnnotationHandler(beanFactory::getBean, (ignored) -> spanTagValueExpressionResolver);
ValueExpressionResolver valueExpressionResolver) {
return new SpanTagAnnotationHandler(beanFactory::getBean, (ignored) -> valueExpressionResolver);
}
@Bean
@ConditionalOnMissingBean(MethodInvocationProcessor.class)
ImperativeMethodInvocationProcessor imperativeMethodInvocationProcessor(NewSpanParser newSpanParser,
Tracer tracer, SpanTagAnnotationHandler spanTagAnnotationHandler) {
return new ImperativeMethodInvocationProcessor(newSpanParser, tracer, spanTagAnnotationHandler);
Tracer tracer, ObjectProvider<SpanTagAnnotationHandler> spanTagAnnotationHandler) {
return new ImperativeMethodInvocationProcessor(newSpanParser, tracer,
spanTagAnnotationHandler.getIfAvailable());
}
@Bean

View File

@ -38,7 +38,6 @@ 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;
@ -59,6 +58,7 @@ class MicrometerTracingAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("management.observations.annotations.enabled=true")
.withBean(ValueExpressionResolver.class, () -> mock(ValueExpressionResolver.class))
.withConfiguration(AutoConfigurations.of(MicrometerTracingAutoConfiguration.class));
@Test
@ -111,8 +111,6 @@ 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);
});
}
@ -270,11 +268,6 @@ class MicrometerTracingAutoConfigurationTests {
(aClass) -> mock(ValueExpressionResolver.class));
}
@Bean
SpelTagValueExpressionResolver customMetricsTagValueExpressionResolver() {
return mock(SpelTagValueExpressionResolver.class);
}
}
@Configuration(proxyBeanMethods = false)