Skip to content

Commit 5e58769

Browse files
Add support for @ObservedKeyValueTag to ObservedAspect (#6667)
This allows declaring dynamic key values to use with the `Observation` created by `ObservedAspect` via a new annotation `@ObservedKeyValueTag`. This is similar to the support available in `TimedAspect` for declaring tags via annotations and resolvers. Closes gh-4030 Co-authored-by: Jonatan Ivanov <[email protected]>
1 parent bbdc1d7 commit 5e58769

File tree

15 files changed

+1323
-12
lines changed

15 files changed

+1323
-12
lines changed

docs/modules/ROOT/pages/observation/components.adoc

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ In this section we will describe main components related to Micrometer Observati
88
* <<micrometer-observation-events, Signaling Errors and Arbitrary Events>>
99
* <<micrometer-observation-convention-example, Observation Convention>>
1010
* <<micrometer-observation-predicates-filters, Observation Predicates and Filters>>
11+
* <<micrometer-observation-annotations, Using Annotations With @Observed and @ObservationKeyValue>>
1112

1213
*Micrometer Observation basic flow*
1314

@@ -164,7 +165,7 @@ include::{include-java}/observation/ObservationConfiguringTests.java[tags=predic
164165
-----
165166

166167
[[micrometer-observation-annotations]]
167-
== Using Annotations With @Observed
168+
== Using Annotations With @Observed and @ObservationKeyValue
168169

169170
If you have turned on Aspect Oriented Programming (for example, by using `org.aspectj:aspectjweaver`), you can use the `@Observed` annotation to create observations. You can put that annotation either on a method to observe it or on a class to observe all the methods in it.
170171

@@ -181,3 +182,19 @@ The following test asserts whether the proper observation gets created when a pr
181182
-----
182183
include::{include-java}/observation/ObservationHandlerTests.java[tags=observed_aop,indent=0]
183184
-----
185+
186+
Also, you can use `@ObservationKeyValue` annotation to add tags via method parameters.
187+
188+
The following example shows an `ObservedServiceWithParameter` that has an annotation on a method:
189+
190+
[source,java,subs=+attributes]
191+
-----
192+
include::{include-java}/observation/ObservationHandlerTests.java[tags=observed_service_with_parameter,indent=0]
193+
-----
194+
195+
The following test asserts whether the proper observation gets created when a proxied `ObservedServiceWithParameter` instance gets called:
196+
197+
[source,java,subs=+attributes]
198+
-----
199+
include::{include-java}/observation/ObservationHandlerTests.java[tags=observed_aop_with_parameter,indent=0]
200+
-----

docs/src/test/java/io/micrometer/docs/metrics/SpelValueExpressionResolver.java renamed to docs/src/test/java/io/micrometer/docs/SpelValueExpressionResolver.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package io.micrometer.docs.metrics;
16+
package io.micrometer.docs;
1717

1818
import io.micrometer.common.annotation.ValueExpressionResolver;
1919
import io.micrometer.common.util.internal.logging.InternalLogger;
@@ -25,7 +25,7 @@
2525
import org.springframework.expression.spel.standard.SpelExpressionParser;
2626
import org.springframework.expression.spel.support.SimpleEvaluationContext;
2727

28-
class SpelValueExpressionResolver implements ValueExpressionResolver {
28+
public class SpelValueExpressionResolver implements ValueExpressionResolver {
2929

3030
private static final InternalLogger log = InternalLoggerFactory.getInstance(SpelValueExpressionResolver.class);
3131

docs/src/test/java/io/micrometer/docs/metrics/CountedAspectTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import io.micrometer.core.aop.MeterTag;
2424
import io.micrometer.core.instrument.MeterRegistry;
2525
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
26+
import io.micrometer.docs.SpelValueExpressionResolver;
27+
2628
import org.junit.jupiter.params.ParameterizedTest;
2729
import org.junit.jupiter.params.provider.EnumSource;
2830
import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;

docs/src/test/java/io/micrometer/docs/metrics/TimedAspectTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import io.micrometer.core.aop.TimedAspect;
2424
import io.micrometer.core.instrument.MeterRegistry;
2525
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
26+
import io.micrometer.docs.SpelValueExpressionResolver;
27+
2628
import org.junit.jupiter.params.ParameterizedTest;
2729
import org.junit.jupiter.params.provider.EnumSource;
2830
import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;

docs/src/test/java/io/micrometer/docs/observation/ObservationHandlerTests.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,21 @@
1717

1818
import io.micrometer.common.KeyValue;
1919
import io.micrometer.common.KeyValues;
20+
import io.micrometer.common.annotation.ValueExpressionResolver;
21+
import io.micrometer.common.annotation.ValueResolver;
2022
import io.micrometer.common.docs.KeyName;
2123
import io.micrometer.core.instrument.MeterRegistry;
2224
import io.micrometer.core.instrument.Timer;
2325
import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler;
2426
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
27+
import io.micrometer.docs.SpelValueExpressionResolver;
2528
import io.micrometer.observation.*;
2629
import io.micrometer.observation.annotation.Observed;
30+
import io.micrometer.observation.annotation.ObservationKeyValue;
31+
import io.micrometer.observation.annotation.ObservationKeyValues;
32+
import io.micrometer.observation.aop.Cardinality;
2733
import io.micrometer.observation.aop.ObservedAspect;
34+
import io.micrometer.observation.aop.ObservationKeyValueAnnotationHandler;
2835
import io.micrometer.observation.docs.ObservationDocumentation;
2936
import io.micrometer.observation.tck.TestObservationRegistry;
3037
import org.jspecify.annotations.Nullable;
@@ -171,6 +178,46 @@ void annotatedCallShouldBeObserved() {
171178
// @formatter:on
172179
}
173180

181+
@Test
182+
void annotatedCallShouldBeObservedWithParameter() {
183+
// @formatter:off
184+
// tag::observed_aop_with_parameter[]
185+
// create a test registry
186+
TestObservationRegistry registry = TestObservationRegistry.create();
187+
// add a system out printing handler
188+
registry.observationConfig().observationHandler(new ObservationTextPublisher());
189+
190+
// create a proxy around the observed service
191+
AspectJProxyFactory pf = new AspectJProxyFactory(new ObservedServiceWithParameter());
192+
ObservedAspect observedAspect = new ObservedAspect(registry);
193+
ValueResolver valueResolver = parameter -> "Value from myCustomTagValueResolver [" + parameter + "]";
194+
ValueExpressionResolver valueExpressionResolver = new SpelValueExpressionResolver();
195+
observedAspect.setObservationKeyValueAnnotationHandler(
196+
new ObservationKeyValueAnnotationHandler(
197+
aClass -> valueResolver, aClass -> valueExpressionResolver)
198+
);
199+
200+
pf.addAspect(observedAspect);
201+
202+
// make a call
203+
ObservedServiceWithParameter service = pf.getProxy();
204+
service.call("foo");
205+
206+
// assert that observation has been properly created
207+
assertThat(registry)
208+
.hasSingleObservationThat()
209+
.hasBeenStopped()
210+
.hasNameEqualTo("test.call")
211+
.hasHighCardinalityKeyValue("key0", "foo")
212+
.hasHighCardinalityKeyValue("key1", "foo")
213+
.hasHighCardinalityKeyValue("key2", "key2: FOO")
214+
.hasHighCardinalityKeyValue("key3", "Value from myCustomTagValueResolver [foo]")
215+
.hasLowCardinalityKeyValue("key4", "foo")
216+
.doesNotHaveError();
217+
// end::observed_aop_with_parameter[]
218+
// @formatter:on
219+
}
220+
174221
private void doSomeWorkHere() {
175222

176223
}
@@ -459,4 +506,19 @@ void call() {
459506
}
460507
// end::observed_service[]
461508

509+
// tag::observed_service_with_parameter[]
510+
static class ObservedServiceWithParameter {
511+
512+
@Observed(name = "test.call")
513+
@ObservationKeyValue(key = "key4", cardinality = Cardinality.LOW)
514+
String call(@ObservationKeyValues({ @ObservationKeyValue(key = "key0", cardinality = Cardinality.HIGH),
515+
@ObservationKeyValue(key = "key1"),
516+
@ObservationKeyValue(key = "key2", expression = "'key2: ' + toUpperCase"),
517+
@ObservationKeyValue(key = "key3", resolver = ValueResolver.class) }) String param) {
518+
return param;
519+
}
520+
521+
}
522+
// end::observed_service_with_parameter[]
523+
462524
}

micrometer-core/src/main/java/io/micrometer/core/aop/MeterTag.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
/**
2525
* There are 3 different ways to add tags to a meter. All of them are controlled by the
2626
* annotation values. Precedence is to first try with the {@link ValueResolver}. If the
27-
* value of the resolver wasn't set, try to evaluate an expression. If theres no
27+
* value of the resolver wasn't set, try to evaluate an expression. If there's no
2828
* expression just return a {@code toString()} value of the parameter.
2929
*
3030
* IMPORTANT: Provided tag values MUST BE of LOW-CARDINALITY. If you fail to provide

micrometer-core/src/main/java/io/micrometer/core/aop/MeterTagSupport.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,21 @@
2828
*
2929
* @author Marcin Grzejszczak
3030
* @author Johnny Lim
31+
* @author Seungyong Hong
3132
*/
3233
final class MeterTagSupport {
3334

35+
private MeterTagSupport() {
36+
}
37+
3438
static String resolveTagKey(MeterTag annotation) {
3539
return StringUtils.isNotBlank(annotation.value()) ? annotation.value() : annotation.key();
3640
}
3741

42+
/**
43+
* Similar to {@code ObservationKeyValueSupport.resolveTagValue}. The two logics are
44+
* similar, so if one is modified, probably the other one should be modified too.
45+
*/
3846
static String resolveTagValue(MeterTag annotation, @Nullable Object argument,
3947
Function<Class<? extends ValueResolver>, ? extends ValueResolver> resolverProvider,
4048
Function<Class<? extends ValueExpressionResolver>, ? extends ValueExpressionResolver> expressionResolverProvider) {
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2025 VMware, Inc.
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+
package io.micrometer.observation.annotation;
17+
18+
import io.micrometer.common.annotation.NoOpValueResolver;
19+
import io.micrometer.common.annotation.ValueExpressionResolver;
20+
import io.micrometer.common.annotation.ValueResolver;
21+
import io.micrometer.observation.aop.Cardinality;
22+
import io.micrometer.observation.aop.ObservationKeyValueAnnotationHandler;
23+
24+
import java.lang.annotation.*;
25+
26+
/**
27+
* There are 3 different ways to add key-values to an observation. All of them are
28+
* controlled by the annotation values. Precedence is to first try with the
29+
* {@link ValueResolver}. If the value of the resolver wasn't set, try to evaluate an
30+
* expression. If there's no expression just return a {@code toString()} value of the
31+
* parameter. {@link Cardinality} also can be set by {@link #cardinality()}. default value
32+
* is {@link Cardinality#HIGH}.
33+
*
34+
* @author Seungyong Hong
35+
*/
36+
@Retention(RetentionPolicy.RUNTIME)
37+
@Inherited
38+
@Target({ ElementType.PARAMETER, ElementType.METHOD })
39+
@Repeatable(ObservationKeyValues.class)
40+
public @interface ObservationKeyValue {
41+
42+
/**
43+
* The name of the key of the key-value which should be created. This is an alias for
44+
* {@link #key()}.
45+
* @return the key-value key name
46+
*/
47+
String value() default "";
48+
49+
/**
50+
* The name of the key of the key-value which should be created.
51+
* @return the key-value key name
52+
*/
53+
String key() default "";
54+
55+
/**
56+
* Execute this expression to calculate the key-value value. Will be evaluated if no
57+
* value of the {@link #resolver()} was set. You need to have a
58+
* {@link ValueExpressionResolver} registered on the
59+
* {@link ObservationKeyValueAnnotationHandler} to provide the expression resolution
60+
* engine.
61+
* @return an expression
62+
*/
63+
String expression() default "";
64+
65+
/**
66+
* Use this object to resolve the key-value value. Has the highest precedence.
67+
* @return {@link ValueResolver} class
68+
*/
69+
Class<? extends ValueResolver> resolver() default NoOpValueResolver.class;
70+
71+
/**
72+
* Cardinality of the key-value.
73+
* @return {@link Cardinality} class
74+
*/
75+
Cardinality cardinality() default Cardinality.HIGH;
76+
77+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2025 VMware, Inc.
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+
package io.micrometer.observation.annotation;
17+
18+
import java.lang.annotation.*;
19+
20+
/**
21+
* Container annotation that aggregates several {@link ObservationKeyValue} annotations.
22+
*
23+
* Can be used natively, declaring several nested {@link ObservationKeyValue} annotations.
24+
* Can also be used in conjunction with Java 8's support for repeatable annotations, where
25+
* {@link ObservationKeyValue} can simply be declared several times on the same parameter,
26+
* implicitly generating this container annotation.
27+
*
28+
* @author Seungyong Hong
29+
*/
30+
@Retention(RetentionPolicy.RUNTIME)
31+
@Inherited
32+
@Target({ ElementType.PARAMETER, ElementType.METHOD })
33+
@Documented
34+
public @interface ObservationKeyValues {
35+
36+
ObservationKeyValue[] value();
37+
38+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2025 VMware, Inc.
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+
package io.micrometer.observation.aop;
17+
18+
/**
19+
* Represents the cardinality of a key-value. There are two types of cardinality and
20+
* treated in different ways.
21+
*
22+
* @author Seungyong Hong
23+
* @author Jonatan Ivanov
24+
*/
25+
public enum Cardinality {
26+
27+
HIGH, LOW
28+
29+
}

0 commit comments

Comments
 (0)