diff --git a/micrometer-core/src/main/java/io/micrometer/core/aop/CountedAspect.java b/micrometer-core/src/main/java/io/micrometer/core/aop/CountedAspect.java index d0682e98eb..9420f11f2b 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/aop/CountedAspect.java +++ b/micrometer-core/src/main/java/io/micrometer/core/aop/CountedAspect.java @@ -111,6 +111,8 @@ public class CountedAspect { */ private final Predicate shouldSkip; + private CountedMeterTagAnnotationHandler meterTagAnnotationHandler; + /** * Creates a {@code CountedAspect} instance with {@link Metrics#globalRegistry}. * @@ -267,7 +269,18 @@ private Counter.Builder counter(ProceedingJoinPoint pjp, Counted counted) { if (!description.isEmpty()) { builder.description(description); } + if (meterTagAnnotationHandler != null) { + meterTagAnnotationHandler.addAnnotatedParameters(builder, pjp); + } return builder; } + /** + * Setting this enables support for {@link MeterTag}. + * @param meterTagAnnotationHandler meter tag annotation handler + */ + public void setMeterTagAnnotationHandler(CountedMeterTagAnnotationHandler meterTagAnnotationHandler) { + this.meterTagAnnotationHandler = meterTagAnnotationHandler; + } + } diff --git a/micrometer-core/src/main/java/io/micrometer/core/aop/CountedMeterTagAnnotationHandler.java b/micrometer-core/src/main/java/io/micrometer/core/aop/CountedMeterTagAnnotationHandler.java new file mode 100644 index 0000000000..d8155c125e --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/aop/CountedMeterTagAnnotationHandler.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024 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.core.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.core.instrument.Counter; + +import java.util.function.Function; + +/** + * Annotation handler for {@link MeterTag}. To add support for {@link MeterTag} on + * {@link CountedAspect} check the + * {@link CountedAspect#setMeterTagAnnotationHandler(CountedMeterTagAnnotationHandler)} + * method. + * + * @author Marcin Grzejszczak + * @author Johnny Lim + */ +public class CountedMeterTagAnnotationHandler extends AnnotationHandler { + + /** + * Creates a new instance of {@link CountedMeterTagAnnotationHandler}. + * @param resolverProvider function to retrieve a {@link ValueResolver} + * @param expressionResolverProvider function to retrieve a + * {@link ValueExpressionResolver} + */ + public CountedMeterTagAnnotationHandler( + Function, ? extends ValueResolver> resolverProvider, + Function, ? extends ValueExpressionResolver> expressionResolverProvider) { + super((keyValue, builder) -> builder.tag(keyValue.getKey(), keyValue.getValue()), resolverProvider, + expressionResolverProvider, MeterTag.class, (annotation, o) -> { + if (!(annotation instanceof MeterTag)) { + return null; + } + MeterTag meterTag = (MeterTag) annotation; + return KeyValue.of(MeterTagSupport.resolveTagKey(meterTag), + MeterTagSupport.resolveTagValue(meterTag, o, resolverProvider, expressionResolverProvider)); + }); + } + +} diff --git a/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTagAnnotationHandler.java b/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTagAnnotationHandler.java index 1ba5bf997f..53d9ff6288 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTagAnnotationHandler.java +++ b/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTagAnnotationHandler.java @@ -16,11 +16,9 @@ package io.micrometer.core.aop; import io.micrometer.common.KeyValue; -import io.micrometer.common.annotation.NoOpValueResolver; import io.micrometer.common.annotation.AnnotationHandler; import io.micrometer.common.annotation.ValueExpressionResolver; import io.micrometer.common.annotation.ValueResolver; -import io.micrometer.common.util.StringUtils; import io.micrometer.core.instrument.Timer; import java.util.function.Function; @@ -31,6 +29,7 @@ * {@link TimedAspect#setMeterTagAnnotationHandler(MeterTagAnnotationHandler)} method. * * @since 1.11.0 + * @author Marcin Grzejszczak */ public class MeterTagAnnotationHandler extends AnnotationHandler { @@ -48,31 +47,9 @@ public MeterTagAnnotationHandler(Function, ? exte return null; } MeterTag meterTag = (MeterTag) annotation; - return KeyValue.of(resolveTagKey(meterTag), - resolveTagValue(meterTag, o, resolverProvider, expressionResolverProvider)); + return KeyValue.of(MeterTagSupport.resolveTagKey(meterTag), + MeterTagSupport.resolveTagValue(meterTag, o, resolverProvider, expressionResolverProvider)); }); } - private static String resolveTagKey(MeterTag annotation) { - return StringUtils.isNotBlank(annotation.value()) ? annotation.value() : annotation.key(); - } - - static String resolveTagValue(MeterTag annotation, 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-core/src/main/java/io/micrometer/core/aop/MeterTagSupport.java b/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTagSupport.java new file mode 100644 index 0000000000..a7af6e5dbd --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTagSupport.java @@ -0,0 +1,55 @@ +/* + * Copyright 2024 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.core.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 java.util.function.Function; + +/** + * Support for {@link MeterTag}. + * + * @author Marcin Grzejszczak + * @author Johnny Lim + */ +final class MeterTagSupport { + + static String resolveTagKey(MeterTag annotation) { + return StringUtils.isNotBlank(annotation.value()) ? annotation.value() : annotation.key(); + } + + static String resolveTagValue(MeterTag annotation, 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-core/src/test/java/io/micrometer/core/aop/CountedAspectTest.java b/micrometer-core/src/test/java/io/micrometer/core/aop/CountedAspectTest.java index a2241250cc..da4e437451 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/aop/CountedAspectTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/aop/CountedAspectTest.java @@ -15,6 +15,8 @@ */ package io.micrometer.core.aop; +import io.micrometer.common.annotation.ValueExpressionResolver; +import io.micrometer.common.annotation.ValueResolver; import io.micrometer.core.annotation.Counted; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; @@ -22,6 +24,8 @@ import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.aspectj.lang.ProceedingJoinPoint; 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.util.concurrent.CompletableFuture; @@ -363,4 +367,166 @@ String greet() { } + static class MeterTagsTests { + + ValueResolver valueResolver = parameter -> "Value from myCustomTagValueResolver [" + parameter + "]"; + + ValueExpressionResolver valueExpressionResolver = new SpelValueExpressionResolver(); + + CountedMeterTagAnnotationHandler meterTagAnnotationHandler = new CountedMeterTagAnnotationHandler( + aClass -> valueResolver, aClass -> valueExpressionResolver); + + @ParameterizedTest + @EnumSource(AnnotatedTestClass.class) + void meterTagsWithText(AnnotatedTestClass annotatedClass) { + MeterRegistry registry = new SimpleMeterRegistry(); + CountedAspect countedAspect = new CountedAspect(registry); + countedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler); + + AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); + pf.addAspect(countedAspect); + + MeterTagClassInterface service = pf.getProxy(); + + service.getAnnotationForArgumentToString(15L); + + assertThat(registry.get("method.counted").tag("test", "15").counter().count()).isEqualTo(1); + } + + @ParameterizedTest + @EnumSource(AnnotatedTestClass.class) + void meterTagsWithResolver(AnnotatedTestClass annotatedClass) { + MeterRegistry registry = new SimpleMeterRegistry(); + CountedAspect countedAspect = new CountedAspect(registry); + countedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler); + + AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); + pf.addAspect(countedAspect); + + MeterTagClassInterface service = pf.getProxy(); + + service.getAnnotationForTagValueResolver("foo"); + + assertThat(registry.get("method.counted") + .tag("test", "Value from myCustomTagValueResolver [foo]") + .counter() + .count()).isEqualTo(1); + } + + @ParameterizedTest + @EnumSource(AnnotatedTestClass.class) + void meterTagsWithExpression(AnnotatedTestClass annotatedClass) { + MeterRegistry registry = new SimpleMeterRegistry(); + CountedAspect countedAspect = new CountedAspect(registry); + countedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler); + + AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); + pf.addAspect(countedAspect); + + MeterTagClassInterface service = pf.getProxy(); + + service.getAnnotationForTagValueExpression("15L"); + + assertThat(registry.get("method.counted").tag("test", "hello characters").counter().count()).isEqualTo(1); + } + + @Test + void meterTagOnPackagePrivateMethod() { + MeterRegistry registry = new SimpleMeterRegistry(); + CountedAspect countedAspect = new CountedAspect(registry); + countedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler); + + AspectJProxyFactory pf = new AspectJProxyFactory(new MeterTagClass()); + pf.setProxyTargetClass(true); + pf.addAspect(countedAspect); + + MeterTagClass service = pf.getProxy(); + + service.getAnnotationForPackagePrivateMethod("bar"); + + assertThat(registry.get("method.counted").tag("foo", "bar").counter().count()).isEqualTo(1); + } + + enum AnnotatedTestClass { + + CLASS_WITHOUT_INTERFACE(MeterTagClass.class), CLASS_WITH_INTERFACE(MeterTagClassChild.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); + } + } + + } + + interface MeterTagClassInterface { + + @Counted + void getAnnotationForTagValueResolver(@MeterTag(key = "test", resolver = ValueResolver.class) String test); + + @Counted + void getAnnotationForTagValueExpression( + @MeterTag(key = "test", expression = "'hello' + ' characters'") String test); + + @Counted + void getAnnotationForArgumentToString(@MeterTag("test") Long param); + + } + + static class MeterTagClass implements MeterTagClassInterface { + + @Counted + @Override + public void getAnnotationForTagValueResolver( + @MeterTag(key = "test", resolver = ValueResolver.class) String test) { + } + + @Counted + @Override + public void getAnnotationForTagValueExpression( + @MeterTag(key = "test", expression = "'hello' + ' characters'") String test) { + } + + @Counted + @Override + public void getAnnotationForArgumentToString(@MeterTag("test") Long param) { + } + + @Counted + void getAnnotationForPackagePrivateMethod(@MeterTag("foo") String foo) { + } + + } + + static class MeterTagClassChild implements MeterTagClassInterface { + + @Counted + @Override + public void getAnnotationForTagValueResolver(String test) { + } + + @Counted + @Override + public void getAnnotationForTagValueExpression(String test) { + } + + @Counted + @Override + public void getAnnotationForArgumentToString(Long param) { + } + + } + + } + } diff --git a/micrometer-core/src/test/java/io/micrometer/core/aop/MeterTagAnnotationHandlerTests.java b/micrometer-core/src/test/java/io/micrometer/core/aop/MeterTagSupportTests.java similarity index 83% rename from micrometer-core/src/test/java/io/micrometer/core/aop/MeterTagAnnotationHandlerTests.java rename to micrometer-core/src/test/java/io/micrometer/core/aop/MeterTagSupportTests.java index 6f229a076d..9a338aba9a 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/aop/MeterTagAnnotationHandlerTests.java +++ b/micrometer-core/src/test/java/io/micrometer/core/aop/MeterTagSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 VMware, Inc. + * Copyright 2024 VMware, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,13 @@ import static org.assertj.core.api.Assertions.assertThat; -class MeterTagAnnotationHandlerTests { +/** + * Tests for {@link MeterTagSupport}. + * + * @author Marcin Grzejszczak + * @author Johnny Lim + */ +class MeterTagSupportTests { ValueResolver valueResolver = parameter -> "Value from myCustomTagValueResolver"; @@ -36,8 +42,8 @@ void shouldUseCustomTagValueResolver() throws NoSuchMethodException, SecurityExc Method method = AnnotationMockClass.class.getMethod("getAnnotationForTagValueResolver", String.class); Annotation annotation = method.getParameterAnnotations()[0][0]; assertThat(annotation).isInstanceOf(MeterTag.class); - String resolvedValue = MeterTagAnnotationHandler.resolveTagValue((MeterTag) annotation, "test", - aClass -> valueResolver, aClass -> valueExpressionResolver); + String resolvedValue = MeterTagSupport.resolveTagValue((MeterTag) annotation, "test", aClass -> valueResolver, + aClass -> valueExpressionResolver); assertThat(resolvedValue).isEqualTo("Value from myCustomTagValueResolver"); } @@ -46,7 +52,7 @@ void shouldUseTagValueExpression() throws NoSuchMethodException, SecurityExcepti Method method = AnnotationMockClass.class.getMethod("getAnnotationForTagValueExpression", String.class); Annotation annotation = method.getParameterAnnotations()[0][0]; assertThat(annotation).isInstanceOf(MeterTag.class); - String resolvedValue = MeterTagAnnotationHandler.resolveTagValue((MeterTag) annotation, "test value", + String resolvedValue = MeterTagSupport.resolveTagValue((MeterTag) annotation, "test value", aClass -> valueResolver, aClass -> valueExpressionResolver); assertThat(resolvedValue).isEqualTo("hello test value characters"); } @@ -56,8 +62,8 @@ void shouldReturnArgumentToString() throws NoSuchMethodException, SecurityExcept Method method = AnnotationMockClass.class.getMethod("getAnnotationForArgumentToString", Long.class); Annotation annotation = method.getParameterAnnotations()[0][0]; assertThat(annotation).isInstanceOf(MeterTag.class); - String resolvedValue = MeterTagAnnotationHandler.resolveTagValue((MeterTag) annotation, 15, - aClass -> valueResolver, aClass -> valueExpressionResolver); + String resolvedValue = MeterTagSupport.resolveTagValue((MeterTag) annotation, 15, aClass -> valueResolver, + aClass -> valueExpressionResolver); assertThat(resolvedValue).isEqualTo("15"); } diff --git a/micrometer-core/src/test/java/io/micrometer/core/aop/NullMetricTagAnnotationHandlerTests.java b/micrometer-core/src/test/java/io/micrometer/core/aop/NullMetricTagAnnotationHandlerTests.java index f8052be75c..182b66be5f 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/aop/NullMetricTagAnnotationHandlerTests.java +++ b/micrometer-core/src/test/java/io/micrometer/core/aop/NullMetricTagAnnotationHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 VMware, Inc. + * Copyright 2024 VMware, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,13 @@ import static org.assertj.core.api.Assertions.assertThat; -class NullMeterTagAnnotationHandlerTests { +/** + * Tests for {@link MeterTagSupport} with {@code null}. + * + * @author Marcin Grzejszczak + * @author Johnny Lim + */ +class NullMeterTagSupportTests { ValueResolver valueResolver = parameter -> null; @@ -36,8 +42,8 @@ void shouldUseEmptyStringWhenCustomTagValueResolverReturnsNull() throws NoSuchMe Method method = AnnotationMockClass.class.getMethod("getAnnotationForTagValueResolver", String.class); Annotation annotation = method.getParameterAnnotations()[0][0]; assertThat(annotation).isInstanceOf(MeterTag.class); - String resolvedValue = MeterTagAnnotationHandler.resolveTagValue((MeterTag) annotation, "test", - aClass -> valueResolver, aClass -> valueExpressionResolver); + String resolvedValue = MeterTagSupport.resolveTagValue((MeterTag) annotation, "test", aClass -> valueResolver, + aClass -> valueExpressionResolver); assertThat(resolvedValue).isEmpty(); } @@ -46,8 +52,8 @@ void shouldUseEmptyStringWhenTagValueExpressionReturnNull() throws NoSuchMethodE Method method = AnnotationMockClass.class.getMethod("getAnnotationForTagValueExpression", String.class); Annotation annotation = method.getParameterAnnotations()[0][0]; assertThat(annotation).isInstanceOf(MeterTag.class); - String resolvedValue = MeterTagAnnotationHandler.resolveTagValue((MeterTag) annotation, "test", - aClass -> valueResolver, aClass -> valueExpressionResolver); + String resolvedValue = MeterTagSupport.resolveTagValue((MeterTag) annotation, "test", aClass -> valueResolver, + aClass -> valueExpressionResolver); assertThat(resolvedValue).isEmpty(); } @@ -56,8 +62,8 @@ void shouldUseEmptyStringWhenArgumentIsNull() throws NoSuchMethodException, Secu Method method = AnnotationMockClass.class.getMethod("getAnnotationForArgumentToString", Long.class); Annotation annotation = method.getParameterAnnotations()[0][0]; assertThat(annotation).isInstanceOf(MeterTag.class); - String resolvedValue = MeterTagAnnotationHandler.resolveTagValue((MeterTag) annotation, null, - aClass -> valueResolver, aClass -> valueExpressionResolver); + String resolvedValue = MeterTagSupport.resolveTagValue((MeterTag) annotation, null, aClass -> valueResolver, + aClass -> valueExpressionResolver); assertThat(resolvedValue).isEmpty(); }