Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion docs/modules/ROOT/pages/observation/components.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ In this section we will describe main components related to Micrometer Observati
* <<micrometer-observation-events, Signaling Errors and Arbitrary Events>>
* <<micrometer-observation-convention-example, Observation Convention>>
* <<micrometer-observation-predicates-filters, Observation Predicates and Filters>>
* <<micrometer-observation-annotations, Using Annotations With @Observed and @ObservationKeyValue>>

*Micrometer Observation basic flow*

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

[[micrometer-observation-annotations]]
== Using Annotations With @Observed
== Using Annotations With @Observed and @ObservationKeyValue

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.

Expand All @@ -181,3 +182,19 @@ The following test asserts whether the proper observation gets created when a pr
-----
include::{include-java}/observation/ObservationHandlerTests.java[tags=observed_aop,indent=0]
-----

Also, you can use `@ObservationKeyValue` annotation to add tags via method parameters.

The following example shows an `ObservedServiceWithParameter` that has an annotation on a method:

[source,java,subs=+attributes]
-----
include::{include-java}/observation/ObservationHandlerTests.java[tags=observed_service_with_parameter,indent=0]
-----

The following test asserts whether the proper observation gets created when a proxied `ObservedServiceWithParameter` instance gets called:

[source,java,subs=+attributes]
-----
include::{include-java}/observation/ObservationHandlerTests.java[tags=observed_aop_with_parameter,indent=0]
-----
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micrometer.docs.metrics;
package io.micrometer.docs;

import io.micrometer.common.annotation.ValueExpressionResolver;
import io.micrometer.common.util.internal.logging.InternalLogger;
Expand All @@ -25,7 +25,7 @@
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;

class SpelValueExpressionResolver implements ValueExpressionResolver {
public class SpelValueExpressionResolver implements ValueExpressionResolver {

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import io.micrometer.core.aop.MeterTag;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.micrometer.docs.SpelValueExpressionResolver;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import io.micrometer.core.aop.TimedAspect;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.micrometer.docs.SpelValueExpressionResolver;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,21 @@

import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import io.micrometer.common.annotation.ValueExpressionResolver;
import io.micrometer.common.annotation.ValueResolver;
import io.micrometer.common.docs.KeyName;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.micrometer.docs.SpelValueExpressionResolver;
import io.micrometer.observation.*;
import io.micrometer.observation.annotation.Observed;
import io.micrometer.observation.annotation.ObservationKeyValue;
import io.micrometer.observation.annotation.ObservationKeyValues;
import io.micrometer.observation.aop.Cardinality;
import io.micrometer.observation.aop.ObservedAspect;
import io.micrometer.observation.aop.ObservationKeyValueAnnotationHandler;
import io.micrometer.observation.docs.ObservationDocumentation;
import io.micrometer.observation.tck.TestObservationRegistry;
import org.jspecify.annotations.Nullable;
Expand Down Expand Up @@ -171,6 +178,46 @@ void annotatedCallShouldBeObserved() {
// @formatter:on
}

@Test
void annotatedCallShouldBeObservedWithParameter() {
// @formatter:off
// tag::observed_aop_with_parameter[]
// create a test registry
TestObservationRegistry registry = TestObservationRegistry.create();
// add a system out printing handler
registry.observationConfig().observationHandler(new ObservationTextPublisher());

// create a proxy around the observed service
AspectJProxyFactory pf = new AspectJProxyFactory(new ObservedServiceWithParameter());
ObservedAspect observedAspect = new ObservedAspect(registry);
ValueResolver valueResolver = parameter -> "Value from myCustomTagValueResolver [" + parameter + "]";
ValueExpressionResolver valueExpressionResolver = new SpelValueExpressionResolver();
observedAspect.setObservationKeyValueAnnotationHandler(
new ObservationKeyValueAnnotationHandler(
aClass -> valueResolver, aClass -> valueExpressionResolver)
);

pf.addAspect(observedAspect);

// make a call
ObservedServiceWithParameter service = pf.getProxy();
service.call("foo");

// assert that observation has been properly created
assertThat(registry)
.hasSingleObservationThat()
.hasBeenStopped()
.hasNameEqualTo("test.call")
.hasHighCardinalityKeyValue("key0", "foo")
.hasHighCardinalityKeyValue("key1", "foo")
.hasHighCardinalityKeyValue("key2", "key2: FOO")
.hasHighCardinalityKeyValue("key3", "Value from myCustomTagValueResolver [foo]")
.hasLowCardinalityKeyValue("key4", "foo")
.doesNotHaveError();
// end::observed_aop_with_parameter[]
// @formatter:on
}

private void doSomeWorkHere() {

}
Expand Down Expand Up @@ -459,4 +506,19 @@ void call() {
}
// end::observed_service[]

// tag::observed_service_with_parameter[]
static class ObservedServiceWithParameter {

@Observed(name = "test.call")
@ObservationKeyValue(key = "key4", cardinality = Cardinality.LOW)
String call(@ObservationKeyValues({ @ObservationKeyValue(key = "key0", cardinality = Cardinality.HIGH),
@ObservationKeyValue(key = "key1"),
@ObservationKeyValue(key = "key2", expression = "'key2: ' + toUpperCase"),
@ObservationKeyValue(key = "key3", resolver = ValueResolver.class) }) String param) {
return param;
}

}
// end::observed_service_with_parameter[]

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
/**
* There are 3 different ways to add tags to a meter. All of them are controlled by the
* annotation values. Precedence is to first try with the {@link ValueResolver}. If the
* value of the resolver wasn't set, try to evaluate an expression. If theres no
* value of the resolver wasn't set, try to evaluate an expression. If there's no
* expression just return a {@code toString()} value of the parameter.
*
* IMPORTANT: Provided tag values MUST BE of LOW-CARDINALITY. If you fail to provide
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,21 @@
*
* @author Marcin Grzejszczak
* @author Johnny Lim
* @author Seungyong Hong
*/
final class MeterTagSupport {

private MeterTagSupport() {
}

static String resolveTagKey(MeterTag annotation) {
return StringUtils.isNotBlank(annotation.value()) ? annotation.value() : annotation.key();
}

/**
* Similar to {@code ObservationKeyValueSupport.resolveTagValue}. The two logics are
* similar, so if one is modified, probably the other one should be modified too.
*/
static String resolveTagValue(MeterTag annotation, @Nullable Object argument,
Function<Class<? extends ValueResolver>, ? extends ValueResolver> resolverProvider,
Function<Class<? extends ValueExpressionResolver>, ? extends ValueExpressionResolver> expressionResolverProvider) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2025 VMware, Inc.
*
* 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 io.micrometer.observation.annotation;

import io.micrometer.common.annotation.NoOpValueResolver;
import io.micrometer.common.annotation.ValueExpressionResolver;
import io.micrometer.common.annotation.ValueResolver;
import io.micrometer.observation.aop.Cardinality;
import io.micrometer.observation.aop.ObservationKeyValueAnnotationHandler;

import java.lang.annotation.*;

/**
* There are 3 different ways to add key-values to an observation. All of them are
* controlled by the annotation values. Precedence is to first try with the
* {@link ValueResolver}. If the value of the resolver wasn't set, try to evaluate an
* expression. If there's no expression just return a {@code toString()} value of the
* parameter. {@link Cardinality} also can be set by {@link #cardinality()}. default value
* is {@link Cardinality#HIGH}.
*
* @author Seungyong Hong
*/
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Repeatable(ObservationKeyValues.class)
public @interface ObservationKeyValue {

/**
* The name of the key of the key-value which should be created. This is an alias for
* {@link #key()}.
* @return the key-value key name
*/
String value() default "";

/**
* The name of the key of the key-value which should be created.
* @return the key-value key name
*/
String key() default "";

/**
* Execute this expression to calculate the key-value value. Will be evaluated if no
* value of the {@link #resolver()} was set. You need to have a
* {@link ValueExpressionResolver} registered on the
* {@link ObservationKeyValueAnnotationHandler} to provide the expression resolution
* engine.
* @return an expression
*/
String expression() default "";

/**
* Use this object to resolve the key-value value. Has the highest precedence.
* @return {@link ValueResolver} class
*/
Class<? extends ValueResolver> resolver() default NoOpValueResolver.class;

/**
* Cardinality of the key-value.
* @return {@link Cardinality} class
*/
Cardinality cardinality() default Cardinality.HIGH;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2025 VMware, Inc.
*
* 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 io.micrometer.observation.annotation;

import java.lang.annotation.*;

/**
* Container annotation that aggregates several {@link ObservationKeyValue} annotations.
*
* Can be used natively, declaring several nested {@link ObservationKeyValue} annotations.
* Can also be used in conjunction with Java 8's support for repeatable annotations, where
* {@link ObservationKeyValue} can simply be declared several times on the same parameter,
* implicitly generating this container annotation.
*
* @author Seungyong Hong
*/
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Documented
public @interface ObservationKeyValues {

ObservationKeyValue[] value();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2025 VMware, Inc.
*
* 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 io.micrometer.observation.aop;

/**
* Represents the cardinality of a key-value. There are two types of cardinality and
* treated in different ways.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth clarifying this is for use with the annotation, specifically. There's no way to use it with KeyValue otherwise. We can polish this post-merge.

*
* @author Seungyong Hong
* @author Jonatan Ivanov
*/
public enum Cardinality {

HIGH, LOW

}
Loading