From b8df881204c756a70ba4da4a1c0b911e68fef72b Mon Sep 17 00:00:00 2001 From: Haiden <46227616+hrothwell@users.noreply.github.com> Date: Tue, 28 May 2024 10:17:01 -0500 Subject: [PATCH] `@MetricOptions` to allow filtering `AbstractMethodTagger` beans to apply to metrics (#764) Follow up to #753 New annotation MetricOptions to hold metadata for additional information that might be used in building metrics --- .../metrics/annotation/MetricOptions.java | 49 +++++++++++++++++++ .../intercept/CountedInterceptor.java | 7 ++- .../intercept/TimedInterceptor.java | 8 ++- .../annotation/CountedAnnotationSpec.groovy | 27 ++++++++++ .../annotation/TimeAnnotationSpec.groovy | 26 ++++++++++ .../metrics/annotation/CountedTarget.java | 7 +++ .../metrics/annotation/TimedTarget.java | 7 +++ 7 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 micrometer-core/src/main/java/io/micronaut/configuration/metrics/annotation/MetricOptions.java diff --git a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/annotation/MetricOptions.java b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/annotation/MetricOptions.java new file mode 100644 index 000000000..11afd6832 --- /dev/null +++ b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/annotation/MetricOptions.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2019 original authors + * + * 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.micronaut.configuration.metrics.annotation; + +import io.micronaut.configuration.metrics.aggregator.AbstractMethodTagger; +import io.micronaut.core.annotation.Experimental; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Holds metadata about metric options to apply + * + * @author Haiden Rothwell + * @since 5.6.0 + */ +@Documented +@Experimental +@Retention(RUNTIME) +@Target({METHOD}) +public @interface MetricOptions { + /** + * @return array of {@link io.micronaut.configuration.metrics.aggregator.AbstractMethodTagger} to apply to metrics for method. + * Only utilized for filtering if {@link #filterTaggers()} is true + */ + Class[] taggers() default {}; + + /** + * @return whether to filter taggers using {@link #taggers()} array + */ + boolean filterTaggers() default false; +} diff --git a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/micrometer/intercept/CountedInterceptor.java b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/micrometer/intercept/CountedInterceptor.java index bf6345126..c78befe7d 100644 --- a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/micrometer/intercept/CountedInterceptor.java +++ b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/micrometer/intercept/CountedInterceptor.java @@ -23,6 +23,7 @@ import io.micronaut.aop.MethodInterceptor; import io.micronaut.aop.MethodInvocationContext; import io.micronaut.configuration.metrics.aggregator.AbstractMethodTagger; +import io.micronaut.configuration.metrics.annotation.MetricOptions; import io.micronaut.configuration.metrics.annotation.RequiresMetrics; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Nullable; @@ -34,6 +35,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -148,13 +150,16 @@ public Object intercept(MethodInvocationContext context) { } private void doCount(AnnotationMetadata metadata, String metricName, @Nullable Throwable e, MethodInvocationContext context) { + List> taggers = Arrays.asList(metadata.classValues(MetricOptions.class, "taggers")); + boolean filter = metadata.booleanValue(MetricOptions.class, "filterTaggers").orElse(false); Counter.builder(metricName) .tags(metadata.stringValues(Counted.class, "extraTags")) .tags( methodTaggers.isEmpty() ? Collections.emptyList() : methodTaggers .stream() - .flatMap(b -> b.getTags(context).stream()) + .filter(t -> !filter || taggers.contains(t.getClass())) + .flatMap(t -> t.getTags(context).stream()) .toList() ) .description(metadata.stringValue(Counted.class, "description").orElse(null)) diff --git a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/micrometer/intercept/TimedInterceptor.java b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/micrometer/intercept/TimedInterceptor.java index 35fe90e32..887f40680 100644 --- a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/micrometer/intercept/TimedInterceptor.java +++ b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/micrometer/intercept/TimedInterceptor.java @@ -25,6 +25,7 @@ import io.micronaut.aop.MethodInterceptor; import io.micronaut.aop.MethodInvocationContext; import io.micronaut.configuration.metrics.aggregator.AbstractMethodTagger; +import io.micronaut.configuration.metrics.annotation.MetricOptions; import io.micronaut.configuration.metrics.annotation.RequiresMetrics; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; @@ -43,6 +44,7 @@ import reactor.core.publisher.Mono; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -212,6 +214,9 @@ private void stopTimed(String metricName, Timer.Sample sample, try { final String description = metadata.stringValue("description").orElse(null); final String[] tags = metadata.stringValues("extraTags"); + final AnnotationMetadata annotationMetadata = context.getAnnotationMetadata(); + final List> taggers = Arrays.asList(annotationMetadata.classValues(MetricOptions.class, "taggers")); + final boolean filter = annotationMetadata.booleanValue(MetricOptions.class, "filterTaggers").orElse(false); final double[] percentiles = metadata.doubleValues("percentiles"); final boolean histogram = metadata.isTrue("histogram"); final Timer timer = Timer.builder(metricName) @@ -221,7 +226,8 @@ private void stopTimed(String metricName, Timer.Sample sample, methodTaggers.isEmpty() ? Collections.emptyList() : methodTaggers .stream() - .flatMap(b -> b.getTags(context).stream()) + .filter(t -> !filter || taggers.contains(t.getClass())) + .flatMap(b -> b.getTags(context).stream()) .toList() ) .tags(EXCEPTION_TAG, exceptionClass) diff --git a/micrometer-core/src/test/groovy/io/micronaut/configuration/metrics/annotation/CountedAnnotationSpec.groovy b/micrometer-core/src/test/groovy/io/micronaut/configuration/metrics/annotation/CountedAnnotationSpec.groovy index 70d05d149..bfbefed45 100644 --- a/micrometer-core/src/test/groovy/io/micronaut/configuration/metrics/annotation/CountedAnnotationSpec.groovy +++ b/micrometer-core/src/test/groovy/io/micronaut/configuration/metrics/annotation/CountedAnnotationSpec.groovy @@ -1,12 +1,15 @@ package io.micronaut.configuration.metrics.annotation import io.micrometer.core.instrument.MeterRegistry +import io.micrometer.core.instrument.search.MeterNotFoundException import io.micronaut.context.ApplicationContext import spock.lang.Specification import spock.util.concurrent.PollingConditions import java.util.function.Consumer +import static java.util.concurrent.TimeUnit.MILLISECONDS + class CountedAnnotationSpec extends Specification { void "test counted annotation usage"() { @@ -86,4 +89,28 @@ class CountedAnnotationSpec extends Specification { cleanup: ctx.close() } + + void "taggers are filtered if filter present"(){ + given: + ApplicationContext ctx = ApplicationContext.run() + CountedTarget tt = ctx.getBean(CountedTarget) + MeterRegistry registry = ctx.getBean(MeterRegistry) + + when: + Integer result = tt.maxWithOptions(4, 10) + registry.get("counted.test.maxWithOptions.blocking").tags("method", "maxWithOptions", "parameters", "a b").counter() + + then: + thrown(MeterNotFoundException) + + when: + def timer = registry.get("counted.test.maxWithOptions.blocking").tags("method", "maxWithOptions").counter() + + then: + result == 10 + timer.count() == 1 + + cleanup: + ctx.close() + } } diff --git a/micrometer-core/src/test/groovy/io/micronaut/configuration/metrics/annotation/TimeAnnotationSpec.groovy b/micrometer-core/src/test/groovy/io/micronaut/configuration/metrics/annotation/TimeAnnotationSpec.groovy index d09094e67..c427168fb 100644 --- a/micrometer-core/src/test/groovy/io/micronaut/configuration/metrics/annotation/TimeAnnotationSpec.groovy +++ b/micrometer-core/src/test/groovy/io/micronaut/configuration/metrics/annotation/TimeAnnotationSpec.groovy @@ -2,6 +2,7 @@ package io.micronaut.configuration.metrics.annotation import io.micrometer.core.instrument.MeterRegistry import io.micrometer.core.instrument.Tag +import io.micrometer.core.instrument.search.MeterNotFoundException import io.micronaut.context.ApplicationContext import spock.lang.Specification import spock.util.concurrent.PollingConditions @@ -99,4 +100,29 @@ class TimeAnnotationSpec extends Specification { cleanup: ctx.close() } + + void "taggers are filtered if filter present"(){ + given: + ApplicationContext ctx = ApplicationContext.run() + TimedTarget tt = ctx.getBean(TimedTarget) + MeterRegistry registry = ctx.getBean(MeterRegistry) + + when: + Integer result = tt.maxWithOptions(4, 10) + registry.get("timed.test.maxWithOptions.blocking").tags("method", "maxWithOptions", "parameters", "a b").timer() + + then: + thrown(MeterNotFoundException) + + when: + def timer = registry.get("timed.test.maxWithOptions.blocking").tags("method", "maxWithOptions").timer() + + then: + result == 10 + timer.count() == 1 + timer.totalTime(MILLISECONDS) > 0 + + cleanup: + ctx.close() + } } diff --git a/micrometer-core/src/test/java/io/micronaut/configuration/metrics/annotation/CountedTarget.java b/micrometer-core/src/test/java/io/micronaut/configuration/metrics/annotation/CountedTarget.java index d551c9ec2..ffb4a4920 100644 --- a/micrometer-core/src/test/java/io/micronaut/configuration/metrics/annotation/CountedTarget.java +++ b/micrometer-core/src/test/java/io/micronaut/configuration/metrics/annotation/CountedTarget.java @@ -1,6 +1,7 @@ package io.micronaut.configuration.metrics.annotation; import io.micrometer.core.annotation.Counted; +import io.micronaut.configuration.metrics.aggregator.MethodTaggerExample; import jakarta.inject.Singleton; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -15,6 +16,12 @@ Integer max(int a, int b) { return Math.max(a, b); } + @Counted("counted.test.maxWithOptions.blocking") + @MetricOptions(taggers = {MethodTaggerExample.class}, filterTaggers = true) + Integer maxWithOptions(int a, int b) { + return Math.max(a, b); + } + @Counted("counted.test.max.blocking") Integer error(int a, int b) { throw new NumberFormatException("cannot"); diff --git a/micrometer-core/src/test/java/io/micronaut/configuration/metrics/annotation/TimedTarget.java b/micrometer-core/src/test/java/io/micronaut/configuration/metrics/annotation/TimedTarget.java index 58b8c9271..0cfb95613 100644 --- a/micrometer-core/src/test/java/io/micronaut/configuration/metrics/annotation/TimedTarget.java +++ b/micrometer-core/src/test/java/io/micronaut/configuration/metrics/annotation/TimedTarget.java @@ -1,6 +1,7 @@ package io.micronaut.configuration.metrics.annotation; import io.micrometer.core.annotation.Timed; +import io.micronaut.configuration.metrics.aggregator.MethodTaggerExample; import jakarta.inject.Singleton; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -15,6 +16,12 @@ Integer max(int a, int b) { return Math.max(a, b); } + @Timed("timed.test.maxWithOptions.blocking") + @MetricOptions(taggers = {MethodTaggerExample.class}, filterTaggers = true) + Integer maxWithOptions(int a, int b) { + return Math.max(a, b); + } + @Timed("timed.test.repeated1") @Timed("timed.test.repeated2") Integer repeated(int a, int b) {