diff --git a/docs/modules/ROOT/pages/observation/components.adoc b/docs/modules/ROOT/pages/observation/components.adoc index cf2151161d..7c099174f5 100644 --- a/docs/modules/ROOT/pages/observation/components.adoc +++ b/docs/modules/ROOT/pages/observation/components.adoc @@ -8,6 +8,7 @@ In this section we will describe main components related to Micrometer Observati * <> * <> * <> +* <> *Micrometer Observation basic flow* @@ -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. @@ -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] +----- diff --git a/docs/src/test/java/io/micrometer/docs/metrics/SpelValueExpressionResolver.java b/docs/src/test/java/io/micrometer/docs/SpelValueExpressionResolver.java similarity index 94% rename from docs/src/test/java/io/micrometer/docs/metrics/SpelValueExpressionResolver.java rename to docs/src/test/java/io/micrometer/docs/SpelValueExpressionResolver.java index d35ae00bf2..21baf6b23b 100644 --- a/docs/src/test/java/io/micrometer/docs/metrics/SpelValueExpressionResolver.java +++ b/docs/src/test/java/io/micrometer/docs/SpelValueExpressionResolver.java @@ -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; @@ -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); diff --git a/docs/src/test/java/io/micrometer/docs/metrics/CountedAspectTest.java b/docs/src/test/java/io/micrometer/docs/metrics/CountedAspectTest.java index 033e21bdc7..5a6617caab 100644 --- a/docs/src/test/java/io/micrometer/docs/metrics/CountedAspectTest.java +++ b/docs/src/test/java/io/micrometer/docs/metrics/CountedAspectTest.java @@ -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; diff --git a/docs/src/test/java/io/micrometer/docs/metrics/TimedAspectTest.java b/docs/src/test/java/io/micrometer/docs/metrics/TimedAspectTest.java index 3f721f8aac..72b05f9d42 100644 --- a/docs/src/test/java/io/micrometer/docs/metrics/TimedAspectTest.java +++ b/docs/src/test/java/io/micrometer/docs/metrics/TimedAspectTest.java @@ -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; diff --git a/docs/src/test/java/io/micrometer/docs/observation/ObservationHandlerTests.java b/docs/src/test/java/io/micrometer/docs/observation/ObservationHandlerTests.java index 19deed1f4b..50293c7cd0 100644 --- a/docs/src/test/java/io/micrometer/docs/observation/ObservationHandlerTests.java +++ b/docs/src/test/java/io/micrometer/docs/observation/ObservationHandlerTests.java @@ -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; @@ -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() { } @@ -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[] + } diff --git a/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTag.java b/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTag.java index f6243224a7..87c4115053 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTag.java +++ b/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTag.java @@ -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 there’s 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 diff --git a/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTagSupport.java b/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTagSupport.java index 0b561d38e3..06c1fce5c5 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTagSupport.java +++ b/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTagSupport.java @@ -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, ? extends ValueResolver> resolverProvider, Function, ? extends ValueExpressionResolver> expressionResolverProvider) { diff --git a/micrometer-observation/src/main/java/io/micrometer/observation/annotation/ObservationKeyValue.java b/micrometer-observation/src/main/java/io/micrometer/observation/annotation/ObservationKeyValue.java new file mode 100644 index 0000000000..ed3fbf9dd4 --- /dev/null +++ b/micrometer-observation/src/main/java/io/micrometer/observation/annotation/ObservationKeyValue.java @@ -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 resolver() default NoOpValueResolver.class; + + /** + * Cardinality of the key-value. + * @return {@link Cardinality} class + */ + Cardinality cardinality() default Cardinality.HIGH; + +} diff --git a/micrometer-observation/src/main/java/io/micrometer/observation/annotation/ObservationKeyValues.java b/micrometer-observation/src/main/java/io/micrometer/observation/annotation/ObservationKeyValues.java new file mode 100644 index 0000000000..3ff7230ac8 --- /dev/null +++ b/micrometer-observation/src/main/java/io/micrometer/observation/annotation/ObservationKeyValues.java @@ -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(); + +} diff --git a/micrometer-observation/src/main/java/io/micrometer/observation/aop/Cardinality.java b/micrometer-observation/src/main/java/io/micrometer/observation/aop/Cardinality.java new file mode 100644 index 0000000000..a862255d74 --- /dev/null +++ b/micrometer-observation/src/main/java/io/micrometer/observation/aop/Cardinality.java @@ -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. + * + * @author Seungyong Hong + * @author Jonatan Ivanov + */ +public enum Cardinality { + + HIGH, LOW + +} diff --git a/micrometer-observation/src/main/java/io/micrometer/observation/aop/ObservationKeyValueAnnotationHandler.java b/micrometer-observation/src/main/java/io/micrometer/observation/aop/ObservationKeyValueAnnotationHandler.java new file mode 100644 index 0000000000..902749245a --- /dev/null +++ b/micrometer-observation/src/main/java/io/micrometer/observation/aop/ObservationKeyValueAnnotationHandler.java @@ -0,0 +1,98 @@ +/* + * 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; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.annotation.AnnotationHandler; +import io.micrometer.common.annotation.ValueExpressionResolver; +import io.micrometer.common.annotation.ValueResolver; +import io.micrometer.observation.Observation; +import io.micrometer.observation.annotation.ObservationKeyValue; + +import java.lang.annotation.Annotation; +import java.util.function.Function; + +import static io.micrometer.observation.aop.ObservationKeyValueSupport.resolveKey; +import static io.micrometer.observation.aop.ObservationKeyValueSupport.resolveValue; + +/** + * Annotation handler for {@link ObservationKeyValue}. To add support for + * {@link ObservationKeyValue} on {@link ObservedAspect} check the + * {@link ObservedAspect#setObservationKeyValueAnnotationHandler(ObservationKeyValueAnnotationHandler)} + * method. + * + * @author Seungyong Hong + * @author Jonatan Ivanov + */ +public class ObservationKeyValueAnnotationHandler extends AnnotationHandler { + + public ObservationKeyValueAnnotationHandler( + Function, ? extends ValueResolver> resolverProvider, + Function, ? extends ValueExpressionResolver> expressionResolverProvider) { + super(ObservationKeyValueAnnotationHandler::addKeyValue, resolverProvider, expressionResolverProvider, + ObservationKeyValue.class, + (annotation, object) -> toKeyValue(annotation, object, resolverProvider, expressionResolverProvider)); + } + + private static void addKeyValue(KeyValue keyValue, Observation.Context context) { + if (keyValue instanceof KeyValueWithCardinality) { + KeyValueWithCardinality keyValueWithCardinality = (KeyValueWithCardinality) keyValue; + if (keyValueWithCardinality.cardinality == Cardinality.LOW) { + context.addLowCardinalityKeyValue(keyValueWithCardinality.getDelegate()); + } + else { + context.addHighCardinalityKeyValue(keyValueWithCardinality.getDelegate()); + } + } + } + + private static KeyValueWithCardinality toKeyValue(Annotation annotation, Object object, + Function, ? extends ValueResolver> resolverProvider, + Function, ? extends ValueExpressionResolver> expressionResolverProvider) { + ObservationKeyValue observationKeyValue = (ObservationKeyValue) annotation; + KeyValue keyValue = KeyValue.of(resolveKey(observationKeyValue), + resolveValue(observationKeyValue, object, resolverProvider, expressionResolverProvider)); + return new KeyValueWithCardinality(keyValue, observationKeyValue.cardinality()); + } + + private static class KeyValueWithCardinality implements KeyValue { + + private final KeyValue keyValue; + + private final Cardinality cardinality; + + private KeyValueWithCardinality(KeyValue keyValue, Cardinality cardinality) { + this.keyValue = keyValue; + this.cardinality = cardinality; + } + + @Override + public String getKey() { + return this.keyValue.getKey(); + } + + @Override + public String getValue() { + return this.keyValue.getValue(); + } + + private KeyValue getDelegate() { + return this.keyValue; + } + + } + +} diff --git a/micrometer-observation/src/main/java/io/micrometer/observation/aop/ObservationKeyValueSupport.java b/micrometer-observation/src/main/java/io/micrometer/observation/aop/ObservationKeyValueSupport.java new file mode 100644 index 0000000000..cdaefff933 --- /dev/null +++ b/micrometer-observation/src/main/java/io/micrometer/observation/aop/ObservationKeyValueSupport.java @@ -0,0 +1,64 @@ +/* + * 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; + +import io.micrometer.common.annotation.NoOpValueResolver; +import io.micrometer.common.annotation.ValueExpressionResolver; +import io.micrometer.common.annotation.ValueResolver; +import io.micrometer.common.util.StringUtils; +import io.micrometer.observation.annotation.ObservationKeyValue; +import org.jspecify.annotations.Nullable; + +import java.util.function.Function; + +/** + * Support for {@link ObservationKeyValue}. + * + * @author Seungyong Hong + */ +class ObservationKeyValueSupport { + + private ObservationKeyValueSupport() { + } + + static String resolveKey(ObservationKeyValue observationKeyValue) { + return StringUtils.isNotBlank(observationKeyValue.value()) ? observationKeyValue.value() + : observationKeyValue.key(); + } + + /** + * Similar to {@code MeterTagSupport.resolveTagValue}. The two logics are similar, so + * if one is modified, probably the other one should be modified too. + */ + static String resolveValue(ObservationKeyValue annotation, @Nullable Object argument, + Function, ? extends ValueResolver> resolverProvider, + Function, ? extends ValueExpressionResolver> expressionResolverProvider) { + String value = null; + if (annotation.resolver() != NoOpValueResolver.class) { + ValueResolver valueResolver = resolverProvider.apply(annotation.resolver()); + value = valueResolver.resolve(argument); + } + else if (StringUtils.isNotBlank(annotation.expression())) { + value = expressionResolverProvider.apply(ValueExpressionResolver.class) + .resolve(annotation.expression(), argument); + } + else if (argument != null) { + value = argument.toString(); + } + return value == null ? "" : value; + } + +} diff --git a/micrometer-observation/src/main/java/io/micrometer/observation/aop/ObservedAspect.java b/micrometer-observation/src/main/java/io/micrometer/observation/aop/ObservedAspect.java index 581ed0913d..68561afb2a 100644 --- a/micrometer-observation/src/main/java/io/micrometer/observation/aop/ObservedAspect.java +++ b/micrometer-observation/src/main/java/io/micrometer/observation/aop/ObservedAspect.java @@ -20,6 +20,7 @@ import io.micrometer.observation.Observations; import io.micrometer.observation.annotation.Observed; import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.annotation.ObservationKeyValue; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @@ -68,10 +69,15 @@ * } * * + * To add support for {@link ObservationKeyValue} annotations, set the + * {@link ObservationKeyValueAnnotationHandler} via + * {@link ObservedAspect#setObservationKeyValueAnnotationHandler(ObservationKeyValueAnnotationHandler)} + * * @author Jonatan Ivanov * @author Yanming Zhou * @author Jeonggi Kim * @author Maksim Petelin + * @author Seungyong Hong * @since 1.10.0 */ @Aspect @@ -85,6 +91,8 @@ public class ObservedAspect { private final Predicate shouldSkip; + private @Nullable ObservationKeyValueAnnotationHandler observationKeyValueAnnotationHandler; + /** * Create an {@code ObservedAspect} with {@link Observations#getGlobalRegistry()}. * @@ -142,22 +150,26 @@ public ObservedAspect(ObservationRegistry registry, private @NullUnmarked Object observe(ProceedingJoinPoint pjp, Method method, Observed observed) throws Throwable { Observation observation = ObservedAspectObservationDocumentation.of(pjp, observed, this.registry, this.observationConvention); + if (observationKeyValueAnnotationHandler != null) { + observationKeyValueAnnotationHandler.addAnnotatedParameters(observation.getContext(), pjp); + } + if (CompletionStage.class.isAssignableFrom(method.getReturnType())) { observation.start(); Observation.Scope scope = observation.openScope(); try { Object result = pjp.proceed(); if (result == null) { - stopObservation(observation, null); - return result; + stopObservation(observation, pjp, null, null); + return null; } else { CompletionStage stage = (CompletionStage) result; - return stage.whenComplete((res, error) -> stopObservation(observation, error)); + return stage.whenComplete((res, error) -> stopObservation(observation, pjp, res, error)); } } catch (Throwable error) { - stopObservation(observation, error); + stopObservation(observation, pjp, null, error); throw error; } finally { @@ -165,7 +177,20 @@ public ObservedAspect(ObservationRegistry registry, } } else { - return observation.observeChecked(() -> pjp.proceed()); + observation.start(); + Observation.Scope scope = observation.openScope(); + try { + Object result = pjp.proceed(); + stopObservation(observation, pjp, result, null); + return result; + } + catch (Throwable error) { + stopObservation(observation, pjp, null, error); + throw error; + } + finally { + scope.close(); + } } } @@ -188,7 +213,11 @@ private Method getMethod(ProceedingJoinPoint pjp) throws NoSuchMethodException { return method; } - private void stopObservation(Observation observation, @Nullable Throwable error) { + private void stopObservation(Observation observation, ProceedingJoinPoint pjp, @Nullable Object result, + @Nullable Throwable error) { + if (observationKeyValueAnnotationHandler != null) { + observationKeyValueAnnotationHandler.addAnnotatedMethodResult(observation.getContext(), pjp, result); + } if (error != null) { observation.error(error); } @@ -209,4 +238,13 @@ public ProceedingJoinPoint getProceedingJoinPoint() { } + /** + * Setting this enables support for {@link ObservationKeyValue}. + * @param observationKeyValueAnnotationHandler annotation handler + */ + public void setObservationKeyValueAnnotationHandler( + ObservationKeyValueAnnotationHandler observationKeyValueAnnotationHandler) { + this.observationKeyValueAnnotationHandler = observationKeyValueAnnotationHandler; + } + } diff --git a/samples/micrometer-samples-spring-framework6/build.gradle b/samples/micrometer-samples-spring-framework6/build.gradle index 77bdb931cc..bb319705c7 100644 --- a/samples/micrometer-samples-spring-framework6/build.gradle +++ b/samples/micrometer-samples-spring-framework6/build.gradle @@ -10,7 +10,6 @@ if (!javaLanguageVersion.canCompileOrRun(17)) { dependencies { implementation project(":micrometer-core") - implementation project(":micrometer-observation") testImplementation project(":micrometer-observation-test") testImplementation project(":micrometer-test") diff --git a/samples/micrometer-samples-spring-framework6/src/test/java/io/micrometer/samples/spring6/aop/ObservedAspectTests.java b/samples/micrometer-samples-spring-framework6/src/test/java/io/micrometer/samples/spring6/aop/ObservedAspectTests.java index cf76831a5e..6401c8fcdf 100644 --- a/samples/micrometer-samples-spring-framework6/src/test/java/io/micrometer/samples/spring6/aop/ObservedAspectTests.java +++ b/samples/micrometer-samples-spring-framework6/src/test/java/io/micrometer/samples/spring6/aop/ObservedAspectTests.java @@ -16,6 +16,8 @@ package io.micrometer.samples.spring6.aop; import io.micrometer.common.KeyValues; +import io.micrometer.common.annotation.ValueExpressionResolver; +import io.micrometer.common.annotation.ValueResolver; import io.micrometer.context.ContextRegistry; import io.micrometer.context.ContextSnapshot; import io.micrometer.context.ContextSnapshotFactory; @@ -24,16 +26,25 @@ import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.ObservationTextPublisher; 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.tck.TestObservationRegistry; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.springframework.aop.aspectj.annotation.AspectJProxyFactory; import java.time.Duration; +import java.util.Locale; import java.util.concurrent.*; import java.util.function.Predicate; import java.util.function.Supplier; @@ -47,7 +58,7 @@ */ class ObservedAspectTests { - TestObservationRegistry registry = TestObservationRegistry.create(); + private final TestObservationRegistry registry = TestObservationRegistry.create(); @Test void annotatedCallShouldBeObserved() { @@ -395,6 +406,872 @@ void annotatedAsyncClassCallWithNullShouldBeObserved() { .doesNotHaveError(); } + @Nested + class ObservationKeyValueTests { + + ValueResolver valueResolver = parameter -> "Value from myCustomValueResolver [" + parameter + "]"; + + ValueExpressionResolver valueExpressionResolver = new SpelValueExpressionResolver(); + + ObservationKeyValueAnnotationHandler observationKeyValueAnnotationHandler = new ObservationKeyValueAnnotationHandler( + aClass -> valueResolver, aClass -> valueExpressionResolver); + + TestObservationRegistry registry; + + ObservedAspect observedAspect; + + @BeforeEach + void setup() { + registry = TestObservationRegistry.create(); + observedAspect = new ObservedAspect(registry); + observedAspect.setObservationKeyValueAnnotationHandler(observationKeyValueAnnotationHandler); + } + + @Test + void observationKeyValuesWithTextLowCardinality() { + ObservationKeyValueCardinalityClass service = getProxyWithObservedAspect( + new ObservationKeyValueCardinalityClass()); + + service.lowCardinality("value"); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasLowCardinalityKeyValue("test", "value") + .doesNotHaveError(); + } + + @Test + void observationKeyValuesWithTextHighCardinality() { + ObservationKeyValueCardinalityClass service = getProxyWithObservedAspect( + new ObservationKeyValueCardinalityClass()); + + service.highCardinlity("value"); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("test", "value") + .doesNotHaveError(); + } + + @ParameterizedTest + @EnumSource + void observationKeyValuesWithText(AnnotatedTestClass annotatedClass) { + ObservationKeyValueClassInterface service = getProxyWithObservedAspect(annotatedClass.newInstance()); + + service.getAnnotationForArgumentToString(15L); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("test", "15") + .doesNotHaveError(); + } + + @ParameterizedTest + @EnumSource + void observationKeyValuesWithResolver(AnnotatedTestClass annotatedClass) { + ObservationKeyValueClassInterface service = getProxyWithObservedAspect(annotatedClass.newInstance()); + + service.getAnnotationForValueResolver("foo"); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("test", "Value from myCustomValueResolver [foo]") + .doesNotHaveError(); + } + + @ParameterizedTest + @EnumSource + void observationKeyValuesWithExpression(AnnotatedTestClass annotatedClass) { + ObservationKeyValueClassInterface service = getProxyWithObservedAspect(annotatedClass.newInstance()); + + service.getAnnotationForValueExpression("15L"); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("test", "hello characters.overridden") + .doesNotHaveError(); + } + + @ParameterizedTest + @EnumSource + void multipleObservationKeyValuesWithExpression(AnnotatedTestClass annotatedClass) { + ObservationKeyValueClassInterface service = getProxyWithObservedAspect(annotatedClass.newInstance()); + + service.getMultipleAnnotationsForValueExpression(new DataHolder("zxe", "qwe")); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("value1", "value1: zxe") + .hasHighCardinalityKeyValue("value2", "value2.overridden: qwe") + .doesNotHaveError(); + } + + @ParameterizedTest + @EnumSource + void multipleObservationKeyValuesWithinContainerWithExpression(AnnotatedTestClass annotatedClass) { + ObservationKeyValueClassInterface service = getProxyWithObservedAspect(annotatedClass.newInstance()); + + service.getMultipleAnnotationsWithContainerForValueExpression(new DataHolder("zxe", "qwe")); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("value1", "value1: zxe") + .hasHighCardinalityKeyValue("value2", "value2: qwe") + .hasHighCardinalityKeyValue("value3", "value3.overridden: ZXEQWE") + .doesNotHaveError(); + } + + @Test + void observationKeyValueOnPackagePrivateMethod() { + AspectJProxyFactory pf = new AspectJProxyFactory(new ObservationKeyValueClass()); + pf.setProxyTargetClass(true); + pf.addAspect(observedAspect); + + ObservationKeyValueClass service = pf.getProxy(); + + service.getAnnotationForPackagePrivateMethod("bar"); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("foo", "bar") + .doesNotHaveError(); + } + + @Test + void observationKeyValueOnSuperClass() { + ObservationKeyValueSub service = getProxyWithObservedAspect(new ObservationKeyValueSub()); + + service.superMethod("someValue"); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("superKey", "someValue") + .doesNotHaveError(); + } + + @Test + void observationKeyValueOnReturnValueWhenException() { + ObservationKeyValueExceptionClass service = getProxyWithObservedAspect( + new ObservationKeyValueExceptionClass()); + + assertThatThrownBy(service::exceptionReturnValue).isInstanceOf(RuntimeException.class) + .hasMessage("exceptionReturnValue"); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("test", "") + .hasError(); + } + + @Test + void observationKeyValueOnParameterWhenException() { + ObservationKeyValueExceptionClass service = getProxyWithObservedAspect( + new ObservationKeyValueExceptionClass()); + + assertThatThrownBy(() -> service.exceptionParameter("value")).isInstanceOf(RuntimeException.class) + .hasMessage("exceptionParameter"); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("test", "value") + .hasError(); + } + + @ParameterizedTest + @EnumSource + void observationKeyValueOnReturnValueWithText(AnnotatedTestClass annotatedClass) { + ObservationKeyValueClassInterface service = getProxyWithObservedAspect(annotatedClass.newInstance()); + + Long value = service.getAnnotationForReturnValueToString(); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("test", value.toString()) + .doesNotHaveError(); + } + + @ParameterizedTest + @EnumSource + void observationKeyValueOnReturnValueWithResolver(AnnotatedTestClass annotatedClass) { + ObservationKeyValueClassInterface service = getProxyWithObservedAspect(annotatedClass.newInstance()); + + String value = service.getReturnValueAnnotationForValueResolver(); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("test", String.format("Value from myCustomValueResolver [%s]", value)) + .doesNotHaveError(); + } + + @ParameterizedTest + @EnumSource + void observationKeyValueOnReturnValueWithExpression(AnnotatedTestClass annotatedClass) { + ObservationKeyValueClassInterface service = getProxyWithObservedAspect(annotatedClass.newInstance()); + + service.getReturnValueAnnotationForValueExpression(); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("test", "hello characters. overridden") + .doesNotHaveError(); + } + + @ParameterizedTest + @EnumSource + void multipleobservationKeyValueOnReturnValueWithExpression(AnnotatedTestClass annotatedClass) { + ObservationKeyValueClassInterface service = getProxyWithObservedAspect(annotatedClass.newInstance()); + + DataHolder value = service.getMultipleAnnotationsOnReturnValueForValueExpression(); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("value1", "value1: " + value.value1) + .hasHighCardinalityKeyValue("value2", "value2. overridden: " + value.value2) + .doesNotHaveError(); + } + + @ParameterizedTest + @EnumSource + void multipleobservationKeyValueOnReturnValueWithinContainerWithExpression(AnnotatedTestClass annotatedClass) { + ObservationKeyValueClassInterface service = getProxyWithObservedAspect(annotatedClass.newInstance()); + + DataHolder value = service.getMultipleAnnotationsOnReturnValueWithContainerForValueExpression(); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("value1", "value1: " + value.value1) + .hasHighCardinalityKeyValue("value2", "value2: " + value.value2) + .hasHighCardinalityKeyValue("value3", + "value3. overridden: " + value.value1.toUpperCase(Locale.ROOT) + + value.value2.toUpperCase(Locale.ROOT)) + .doesNotHaveError(); + } + + @Test + void observationKeyValueOnReturnValueOnPackagePrivateMethod() { + AspectJProxyFactory pf = new AspectJProxyFactory(new ObservationKeyValueClass()); + pf.setProxyTargetClass(true); + pf.addAspect(observedAspect); + + ObservationKeyValueClass service = pf.getProxy(); + + String value = service.getReturnValueAnnotationForPackagePrivateMethod(); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("foo", value) + .doesNotHaveError(); + } + + @Test + void observationKeyValueOnReturnValueOnSuperClass() { + ObservationKeyValueSub service = getProxyWithObservedAspect(new ObservationKeyValueSub()); + + String value = service.superMethod(); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("superKey", value) + .doesNotHaveError(); + } + + @Test + void observationKeyValuesWithTextAsync() { + ObservationKeyValueAsyncClass service = getProxyWithObservedAspect(new ObservationKeyValueAsyncClass()); + + service.getAnnotationForArgumentToString(15L); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("test", "15") + .doesNotHaveError(); + } + + @Test + void observationKeyValuesWithResolverAsync() { + ObservationKeyValueAsyncClass service = getProxyWithObservedAspect(new ObservationKeyValueAsyncClass()); + + service.getAnnotationForValueResolver("foo"); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("test", "Value from myCustomValueResolver [foo]") + .doesNotHaveError(); + } + + @Test + void observationKeyValuesWithExpressionAsync() { + ObservationKeyValueAsyncClass service = getProxyWithObservedAspect(new ObservationKeyValueAsyncClass()); + + service.getAnnotationForValueExpression("15L"); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("test", "hello characters.overridden") + .doesNotHaveError(); + } + + @Test + void multipleobservationKeyValuesWithExpressionAsync() { + ObservationKeyValueAsyncClass service = getProxyWithObservedAspect(new ObservationKeyValueAsyncClass()); + + service.getMultipleAnnotationsForValueExpression(new DataHolder("zxe", "qwe")); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("value1", "value1: zxe") + .hasHighCardinalityKeyValue("value2", "value2.overridden: qwe") + .doesNotHaveError(); + } + + @Test + void multipleobservationKeyValuesWithinContainerWithExpressionAsync() { + ObservationKeyValueAsyncClass service = getProxyWithObservedAspect(new ObservationKeyValueAsyncClass()); + + service.getMultipleAnnotationsWithContainerForValueExpression(new DataHolder("zxe", "qwe")); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("value1", "value1: zxe") + .hasHighCardinalityKeyValue("value2", "value2: qwe") + .hasHighCardinalityKeyValue("value3", "value3.overridden: ZXEQWE") + .doesNotHaveError(); + } + + @Test + void observationKeyValueOnPackagePrivateMethodAsync() { + ObservationKeyValueAsyncClass service = getProxyWithObservedAspect(new ObservationKeyValueAsyncClass()); + + service.getAnnotationForPackagePrivateMethod("bar"); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("foo", "bar") + .doesNotHaveError(); + } + + @Test + void observationKeyValueOnReturnValueWithTextAsync() throws ExecutionException, InterruptedException { + ObservationKeyValueAsyncClass service = getProxyWithObservedAspect(new ObservationKeyValueAsyncClass()); + + Long value = service.getAnnotationForReturnValueToString().get(); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("test", value.toString()) + .doesNotHaveError(); + } + + @Test + void observationKeyValueOnReturnValueWithResolverAsync() throws ExecutionException, InterruptedException { + ObservationKeyValueAsyncClass service = getProxyWithObservedAspect(new ObservationKeyValueAsyncClass()); + + String value = service.getReturnValueAnnotationForValueResolver().get(); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("test", String.format("Value from myCustomValueResolver [%s]", value)) + .doesNotHaveError(); + } + + @Test + void observationKeyValueOnReturnValueWithExpressionAsync() { + ObservationKeyValueAsyncClass service = getProxyWithObservedAspect(new ObservationKeyValueAsyncClass()); + + service.getReturnValueAnnotationForValueExpression(); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("test", "hello characters. overridden") + .doesNotHaveError(); + } + + @Test + void multipleobservationKeyValueOnReturnValueWithExpressionAsync() + throws ExecutionException, InterruptedException { + ObservationKeyValueAsyncClass service = getProxyWithObservedAspect(new ObservationKeyValueAsyncClass()); + + DataHolder value = service.getMultipleAnnotationsOnReturnValueForValueExpression().get(); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("value1", "value1: " + value.value1) + .hasHighCardinalityKeyValue("value2", "value2. overridden: " + value.value2) + .doesNotHaveError(); + } + + @Test + void multipleobservationKeyValueOnReturnValueWithinContainerWithExpressionAsync() + throws ExecutionException, InterruptedException { + ObservationKeyValueAsyncClass service = getProxyWithObservedAspect(new ObservationKeyValueAsyncClass()); + + DataHolder value = service.getMultipleAnnotationsOnReturnValueWithContainerForValueExpression().get(); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("value1", "value1: " + value.value1) + .hasHighCardinalityKeyValue("value2", "value2: " + value.value2) + .hasHighCardinalityKeyValue("value3", + "value3. overridden: " + value.value1.toUpperCase(Locale.ROOT) + + value.value2.toUpperCase(Locale.ROOT)) + .doesNotHaveError(); + } + + @Test + void observationKeyValueOnReturnValueOnPackagePrivateMethodAsync() + throws ExecutionException, InterruptedException { + ObservationKeyValueAsyncClass service = getProxyWithObservedAspect(new ObservationKeyValueAsyncClass()); + + String value = service.getReturnValueAnnotationForPackagePrivateMethod().get(); + + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasBeenStopped() + .hasNameEqualTo("method.observed") + .hasHighCardinalityKeyValue("foo", value) + .doesNotHaveError(); + } + + private T getProxyWithObservedAspect(T object) { + AspectJProxyFactory pf = new AspectJProxyFactory(object); + pf.addAspect(observedAspect); + return pf.getProxy(); + } + + } + + enum AnnotatedTestClass { + + CLASS_WITHOUT_INTERFACE(ObservationKeyValueClass.class), + CLASS_WITH_INTERFACE(ObservationKeyValueClassChild.class); + + private final Class clazz; + + AnnotatedTestClass(Class clazz) { + this.clazz = clazz; + } + + @SuppressWarnings("unchecked") + T newInstance() { + try { + return (T) clazz.getDeclaredConstructor().newInstance(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + } + + static class ObservationKeyValueAsyncClass { + + @Observed + public CompletableFuture getAnnotationForValueResolver( + @ObservationKeyValue(key = "test", resolver = ValueResolver.class) String test) { + return CompletableFuture.completedFuture(null); + } + + @Observed + @ObservationKeyValue(key = "test", resolver = ValueResolver.class) + public CompletableFuture getReturnValueAnnotationForValueResolver() { + return CompletableFuture.completedFuture("foo"); + } + + @Observed + public CompletableFuture getAnnotationForValueExpression( + @ObservationKeyValue(key = "test", expression = "'hello' + ' characters.overridden'") String test) { + return CompletableFuture.completedFuture(null); + } + + @Observed + @ObservationKeyValue(key = "test", expression = "'hello' + ' characters. overridden'") + public CompletableFuture getReturnValueAnnotationForValueExpression() { + return CompletableFuture.completedFuture("foo"); + } + + @Observed + public CompletableFuture getAnnotationForArgumentToString(@ObservationKeyValue("test") Long param) { + return CompletableFuture.completedFuture(null); + } + + @Observed + @ObservationKeyValue("test") + public CompletableFuture getAnnotationForReturnValueToString() { + return CompletableFuture.completedFuture(15L); + } + + @Observed + CompletableFuture getAnnotationForPackagePrivateMethod(@ObservationKeyValue("foo") String foo) { + return CompletableFuture.completedFuture(null); + } + + @Observed + @ObservationKeyValue("foo") + CompletableFuture getReturnValueAnnotationForPackagePrivateMethod() { + return CompletableFuture.completedFuture("bar"); + } + + @Observed + public CompletableFuture getMultipleAnnotationsForValueExpression( + @ObservationKeyValue(key = "value1", expression = "'value1: ' + value1") @ObservationKeyValue( + key = "value2", expression = "'value2.overridden: ' + value2") DataHolder param) { + return CompletableFuture.completedFuture(null); + } + + @Observed + @ObservationKeyValue(key = "value1", expression = "'value1: ' + value1") + @ObservationKeyValue(key = "value2", expression = "'value2. overridden: ' + value2") + public CompletableFuture getMultipleAnnotationsOnReturnValueForValueExpression() { + return CompletableFuture.completedFuture(new DataHolder("zxe", "qwe")); + } + + @Observed + public CompletableFuture getMultipleAnnotationsWithContainerForValueExpression(@ObservationKeyValues({ + @ObservationKeyValue(key = "value1", expression = "'value1: ' + value1"), + @ObservationKeyValue(key = "value2", expression = "'value2: ' + value2"), + @ObservationKeyValue(key = "value3", + expression = "'value3.overridden: ' + value1.toUpperCase + value2.toUpperCase") }) DataHolder param) { + return CompletableFuture.completedFuture(null); + } + + @Observed + @ObservationKeyValues({ @ObservationKeyValue(key = "value1", expression = "'value1: ' + value1"), + @ObservationKeyValue(key = "value2", expression = "'value2: ' + value2"), + @ObservationKeyValue(key = "value3", + expression = "'value3. overridden: ' + value1.toUpperCase + value2.toUpperCase") }) + public CompletableFuture getMultipleAnnotationsOnReturnValueWithContainerForValueExpression() { + return CompletableFuture.completedFuture(new DataHolder("zxe", "qwe")); + } + + } + + static class ObservationKeyValueExceptionClass { + + @Observed + @ObservationKeyValue(key = "test") + public String exceptionReturnValue() { + throw new RuntimeException("exceptionReturnValue"); + } + + @Observed + public String exceptionParameter(@ObservationKeyValue(key = "test") String param) { + throw new RuntimeException("exceptionParameter"); + } + + } + + static class ObservationKeyValueCardinalityClass { + + @Observed + void lowCardinality(@ObservationKeyValue(key = "test", cardinality = Cardinality.LOW) String test) { + } + + @Observed + void highCardinlity(@ObservationKeyValue(key = "test", cardinality = Cardinality.HIGH) String test) { + } + + } + + interface ObservationKeyValueClassInterface { + + @Observed + void getAnnotationForValueResolver( + @ObservationKeyValue(key = "test", resolver = ValueResolver.class) String test); + + @Observed + @ObservationKeyValue(key = "test", resolver = ValueResolver.class) + String getReturnValueAnnotationForValueResolver(); + + @Observed + void getAnnotationForValueExpression( + @ObservationKeyValue(key = "test", expression = "'hello' + ' characters'") String test); + + @Observed + @ObservationKeyValue(key = "test", expression = "'hello' + ' characters'") + String getReturnValueAnnotationForValueExpression(); + + @Observed + void getAnnotationForArgumentToString(@ObservationKeyValue("test") Long param); + + @Observed + @ObservationKeyValue("test") + Long getAnnotationForReturnValueToString(); + + @Observed + void getMultipleAnnotationsForValueExpression( + @ObservationKeyValue(key = "value1", expression = "'value1: ' + value1") @ObservationKeyValue( + key = "value2", expression = "'value2: ' + value2") DataHolder param); + + @Observed + @ObservationKeyValue(key = "value1", expression = "'value1: ' + value1") + @ObservationKeyValue(key = "value2", expression = "'value2: ' + value2") + DataHolder getMultipleAnnotationsOnReturnValueForValueExpression(); + + @Observed + void getMultipleAnnotationsWithContainerForValueExpression(@ObservationKeyValues({ + @ObservationKeyValue(key = "value1", expression = "'value1: ' + value1"), + @ObservationKeyValue(key = "value2", expression = "'value2: ' + value2"), + @ObservationKeyValue(key = "value3", + expression = "'value3: ' + value1.toUpperCase + value2.toUpperCase") }) DataHolder param); + + @Observed + @ObservationKeyValues({ @ObservationKeyValue(key = "value1", expression = "'value1: ' + value1"), + @ObservationKeyValue(key = "value2", expression = "'value2: ' + value2"), @ObservationKeyValue( + key = "value3", expression = "'value3: ' + value1.toUpperCase + value2.toUpperCase") }) + DataHolder getMultipleAnnotationsOnReturnValueWithContainerForValueExpression(); + + } + + static class ObservationKeyValueClass implements ObservationKeyValueClassInterface { + + @Observed + @Override + public void getAnnotationForValueResolver( + @ObservationKeyValue(key = "test", resolver = ValueResolver.class) String test) { + } + + @Observed + @ObservationKeyValue(key = "test", resolver = ValueResolver.class) + @Override + public String getReturnValueAnnotationForValueResolver() { + return "foo"; + } + + @Observed + @Override + public void getAnnotationForValueExpression( + @ObservationKeyValue(key = "test", expression = "'hello' + ' characters.overridden'") String test) { + } + + @Observed + @ObservationKeyValue(key = "test", expression = "'hello' + ' characters. overridden'") + @Override + public String getReturnValueAnnotationForValueExpression() { + return "foo"; + } + + @Observed + @Override + public void getAnnotationForArgumentToString(@ObservationKeyValue("test") Long param) { + } + + @Observed + @ObservationKeyValue("test") + @Override + public Long getAnnotationForReturnValueToString() { + return 15L; + } + + @Observed + void getAnnotationForPackagePrivateMethod(@ObservationKeyValue("foo") String foo) { + } + + @Observed + @ObservationKeyValue("foo") + String getReturnValueAnnotationForPackagePrivateMethod() { + return "bar"; + } + + @Observed + @Override + public void getMultipleAnnotationsForValueExpression( + @ObservationKeyValue(key = "value1", expression = "'value1: ' + value1") @ObservationKeyValue( + key = "value2", expression = "'value2.overridden: ' + value2") DataHolder param) { + + } + + @Observed + @ObservationKeyValue(key = "value1", expression = "'value1: ' + value1") + @ObservationKeyValue(key = "value2", expression = "'value2. overridden: ' + value2") + @Override + public DataHolder getMultipleAnnotationsOnReturnValueForValueExpression() { + return new DataHolder("zxe", "qwe"); + } + + @Observed + @Override + public void getMultipleAnnotationsWithContainerForValueExpression(@ObservationKeyValues({ + @ObservationKeyValue(key = "value1", expression = "'value1: ' + value1"), + @ObservationKeyValue(key = "value2", expression = "'value2: ' + value2"), + @ObservationKeyValue(key = "value3", + expression = "'value3.overridden: ' + value1.toUpperCase + value2.toUpperCase") }) DataHolder param) { + } + + @Observed + @ObservationKeyValues({ @ObservationKeyValue(key = "value1", expression = "'value1: ' + value1"), + @ObservationKeyValue(key = "value2", expression = "'value2: ' + value2"), + @ObservationKeyValue(key = "value3", + expression = "'value3. overridden: ' + value1.toUpperCase + value2.toUpperCase") }) + @Override + public DataHolder getMultipleAnnotationsOnReturnValueWithContainerForValueExpression() { + return new DataHolder("zxe", "qwe"); + } + + } + + static class ObservationKeyValueClassChild implements ObservationKeyValueClassInterface { + + @Observed + @Override + public void getAnnotationForValueResolver(String test) { + } + + @Observed + @Override + public String getReturnValueAnnotationForValueResolver() { + return "foo"; + } + + @Observed + @Override + public void getAnnotationForValueExpression( + @ObservationKeyValue(key = "test", expression = "'hello' + ' characters.overridden'") String test) { + } + + @Observed + @ObservationKeyValue(key = "test", expression = "'hello' + ' characters. overridden'") + @Override + public String getReturnValueAnnotationForValueExpression() { + return "foo"; + } + + @Observed + @Override + public void getAnnotationForArgumentToString(Long param) { + } + + @Observed + @Override + public Long getAnnotationForReturnValueToString() { + return 15L; + } + + @Observed + @Override + public void getMultipleAnnotationsForValueExpression( + @ObservationKeyValue(key = "value2", expression = "'value2.overridden: ' + value2") DataHolder param) { + } + + @Observed + @ObservationKeyValue(key = "value2", expression = "'value2. overridden: ' + value2") + @Override + public DataHolder getMultipleAnnotationsOnReturnValueForValueExpression() { + return new DataHolder("zxe", "qwe"); + } + + @Observed + @Override + public void getMultipleAnnotationsWithContainerForValueExpression(@ObservationKeyValue(key = "value3", + expression = "'value3.overridden: ' + value1.toUpperCase + value2.toUpperCase") DataHolder param) { + } + + @Observed + @ObservationKeyValue(key = "value3", + expression = "'value3. overridden: ' + value1.toUpperCase + value2.toUpperCase") + @Override + public DataHolder getMultipleAnnotationsOnReturnValueWithContainerForValueExpression() { + return new DataHolder("zxe", "qwe"); + } + + } + + static class ObservationKeyValueSuper { + + @Observed + public void superMethod(@ObservationKeyValue("superKey") String foo) { + } + + @Observed + @ObservationKeyValue("superKey") + public String superMethod() { + return "someValue"; + } + + } + + static class ObservationKeyValueSub extends ObservationKeyValueSuper { + + } + + static class DataHolder { + + private final String value1; + + private final String value2; + + private DataHolder(String value1, String value2) { + this.value1 = value1; + this.value2 = value2; + } + + public String getValue1() { + return value1; + } + + public String getValue2() { + return value2; + } + + } + static class ObservedService { @Observed(name = "test.call", contextualName = "test#call",