diff --git a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/aggregator/AbstractMethodTagger.java b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/aggregator/AbstractMethodTagger.java new file mode 100644 index 000000000..e561463e7 --- /dev/null +++ b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/aggregator/AbstractMethodTagger.java @@ -0,0 +1,56 @@ +/* + * 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.aggregator; + +import io.micrometer.core.instrument.Tag; +import io.micronaut.aop.MethodInvocationContext; +import io.micronaut.core.annotation.Indexed; +import io.micronaut.core.annotation.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.List; + +/** + * Appends additional {@link io.micrometer.core.instrument.Tag} to metrics annotated with {@link io.micrometer.core.annotation.Timed} and {@link io.micrometer.core.annotation.Counted}. + * + * @author Haiden Rothwell + * @since 5.5.0 + */ +@Indexed(AbstractMethodTagger.class) +public abstract class AbstractMethodTagger { + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractMethodTagger.class); + + private final Class implClass = this.getClass(); + + /** + * Build list of tags using {@link io.micronaut.aop.MethodInvocationContext} which will be included on published metric. + * @param context Context of the method annotated + * @return List of {@link io.micrometer.core.instrument.Tag} which will be included in the metric + */ + @NonNull protected abstract List buildTags(@NonNull MethodInvocationContext context); + + @NonNull public final List getTags(@NonNull MethodInvocationContext context) { + List tags = buildTags(context); + if (tags != null) { + return tags; + } else { + LOGGER.error("{} returned null list of tags and will not include additional tags on metric", implClass); + return Collections.emptyList(); + } + } +} diff --git a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/executor/ExecutorServiceMetricsBinder.java b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/executor/ExecutorServiceMetricsBinder.java index 8bac808eb..7d60cb969 100644 --- a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/executor/ExecutorServiceMetricsBinder.java +++ b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/executor/ExecutorServiceMetricsBinder.java @@ -28,7 +28,6 @@ import io.micronaut.inject.BeanIdentifier; import io.micronaut.scheduling.instrument.InstrumentedExecutorService; import io.micronaut.scheduling.instrument.InstrumentedScheduledExecutorService; -import io.netty.util.concurrent.ThreadPerTaskExecutor; import jakarta.inject.Singleton; import java.util.Collections; 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 6461ff718..bf6345126 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 @@ -22,6 +22,7 @@ import io.micronaut.aop.InterceptorBean; import io.micronaut.aop.MethodInterceptor; import io.micronaut.aop.MethodInvocationContext; +import io.micronaut.configuration.metrics.aggregator.AbstractMethodTagger; import io.micronaut.configuration.metrics.annotation.RequiresMetrics; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Nullable; @@ -33,6 +34,9 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.Collections; +import java.util.List; +import java.util.Objects; import java.util.concurrent.CompletionStage; import static io.micrometer.core.aop.TimedAspect.EXCEPTION_TAG; @@ -49,9 +53,9 @@ public class CountedInterceptor implements MethodInterceptor { public static final String DEFAULT_METRIC_NAME = "method.counted"; public static final String RESULT_TAG = "result"; - private final MeterRegistry meterRegistry; private final ConversionService conversionService; + private final List methodTaggers; /** * @param meterRegistry The meter registry @@ -65,11 +69,23 @@ public CountedInterceptor(MeterRegistry meterRegistry) { /** * @param meterRegistry The meter registry * @param conversionService The conversion service + * @deprecated Pass list of AbstractMethodTagger in new constructor */ - @Inject + @Deprecated public CountedInterceptor(MeterRegistry meterRegistry, ConversionService conversionService) { + this(meterRegistry, conversionService, Collections.emptyList()); + } + + /** + * @param meterRegistry The meter registry + * @param conversionService The conversion service + * @param methodTaggers Additional tag builders + */ + @Inject + public CountedInterceptor(MeterRegistry meterRegistry, ConversionService conversionService, List methodTaggers) { this.meterRegistry = meterRegistry; this.conversionService = conversionService; + this.methodTaggers = Objects.requireNonNullElse(methodTaggers, Collections.emptyList()); } @Override @@ -90,20 +106,20 @@ public Object intercept(MethodInvocationContext context) { if (context.getReturnType().isSingleResult()) { Mono single = Mono.from(Publishers.convertPublisher(conversionService, interceptResult, Publisher.class)); reactiveResult = single - .doOnError(throwable -> doCount(metadata, metricName, throwable)) - .doOnSuccess(o -> doCount(metadata, metricName, null)); + .doOnError(throwable -> doCount(metadata, metricName, throwable, context)) + .doOnSuccess(o -> doCount(metadata, metricName, null, context)); } else { Flux flowable = Flux.from(Publishers.convertPublisher(conversionService, interceptResult, Publisher.class)); reactiveResult = flowable - .doOnError(throwable -> doCount(metadata, metricName, throwable)) - .doOnComplete(() -> doCount(metadata, metricName, null)); + .doOnError(throwable -> doCount(metadata, metricName, throwable, context)) + .doOnComplete(() -> doCount(metadata, metricName, null, context)); } return Publishers.convertPublisher(conversionService, reactiveResult, context.getReturnType().getType()); } case COMPLETION_STAGE -> { CompletionStage completionStage = interceptedMethod.interceptResultAsCompletionStage(); CompletionStage completionStageResult = completionStage - .whenComplete((o, throwable) -> doCount(metadata, metricName, throwable)); + .whenComplete((o, throwable) -> doCount(metadata, metricName, throwable, context)); return interceptedMethod.handleResult(completionStageResult); } case SYNCHRONOUS -> { @@ -112,7 +128,7 @@ public Object intercept(MethodInvocationContext context) { return result; } finally { if (metadata.isFalse(Counted.class, "recordFailuresOnly")) { - doCount(metadata, metricName, null); + doCount(metadata, metricName, null, context); } } } @@ -124,16 +140,23 @@ public Object intercept(MethodInvocationContext context) { try { return interceptedMethod.handleException(e); } finally { - doCount(metadata, metricName, e); + doCount(metadata, metricName, e, context); } } } return context.proceed(); } - private void doCount(AnnotationMetadata metadata, String metricName, @Nullable Throwable e) { + private void doCount(AnnotationMetadata metadata, String metricName, @Nullable Throwable e, MethodInvocationContext context) { Counter.builder(metricName) .tags(metadata.stringValues(Counted.class, "extraTags")) + .tags( + methodTaggers.isEmpty() ? Collections.emptyList() : + methodTaggers + .stream() + .flatMap(b -> b.getTags(context).stream()) + .toList() + ) .description(metadata.stringValue(Counted.class, "description").orElse(null)) .tag(EXCEPTION_TAG, e != null ? e.getClass().getSimpleName() : "none") .tag(RESULT_TAG, e != null ? "failure" : "success") 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 6bf57f028..35fe90e32 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 @@ -24,6 +24,7 @@ import io.micronaut.aop.InterceptorBean; import io.micronaut.aop.MethodInterceptor; import io.micronaut.aop.MethodInvocationContext; +import io.micronaut.configuration.metrics.aggregator.AbstractMethodTagger; import io.micronaut.configuration.metrics.annotation.RequiresMetrics; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; @@ -45,6 +46,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.concurrent.CompletionStage; import java.util.concurrent.atomic.AtomicReference; @@ -80,6 +82,7 @@ public class TimedInterceptor implements MethodInterceptor { private final MeterRegistry meterRegistry; private final ConversionService conversionService; + private final List methodTaggers; /** * @param meterRegistry The meter registry @@ -87,17 +90,29 @@ public class TimedInterceptor implements MethodInterceptor { */ @Deprecated protected TimedInterceptor(MeterRegistry meterRegistry) { - this(meterRegistry, ConversionService.SHARED); + this(meterRegistry, ConversionService.SHARED, Collections.emptyList()); } /** * @param meterRegistry The meter registry * @param conversionService The conversion service + * @deprecated Pass list of AbstractMethodTaggers in new constructor */ - @Inject + @Deprecated protected TimedInterceptor(MeterRegistry meterRegistry, ConversionService conversionService) { + this(meterRegistry, conversionService, Collections.emptyList()); + } + + /** + * @param meterRegistry The meter registry + * @param conversionService The conversion service + * @param methodTaggers Additional tag builders + */ + @Inject + protected TimedInterceptor(MeterRegistry meterRegistry, ConversionService conversionService, List methodTaggers) { this.meterRegistry = meterRegistry; this.conversionService = conversionService; + this.methodTaggers = Objects.requireNonNullElse(methodTaggers, Collections.emptyList()); } @Override @@ -125,14 +140,14 @@ public Object intercept(MethodInvocationContext context) { if (context.getReturnType().isSingleResult()) { Mono single = Mono.from(Publishers.convertPublisher(conversionService, interceptResult, Publisher.class)); result = single.doOnSubscribe(d -> reactiveInvokeSample.set(initSamples(timedAnnotations))) - .doOnError(throwable -> finalizeSamples(timedAnnotations, throwable.getClass().getSimpleName(), reactiveInvokeSample.get())) - .doOnSuccess(o -> finalizeSamples(timedAnnotations, "none", reactiveInvokeSample.get())); + .doOnError(throwable -> finalizeSamples(timedAnnotations, throwable.getClass().getSimpleName(), reactiveInvokeSample.get(), context)) + .doOnSuccess(o -> finalizeSamples(timedAnnotations, "none", reactiveInvokeSample.get(), context)); } else { AtomicReference exceptionClassHolder = new AtomicReference<>("none"); Flux flowable = Flux.from(Publishers.convertPublisher(conversionService, interceptResult, Publisher.class)); result = flowable.doOnRequest(n -> reactiveInvokeSample.set(initSamples(timedAnnotations))) .doOnError(throwable -> exceptionClassHolder.set(throwable.getClass().getSimpleName())) - .doOnComplete(() -> finalizeSamples(timedAnnotations, exceptionClassHolder.get(), reactiveInvokeSample.get())); + .doOnComplete(() -> finalizeSamples(timedAnnotations, exceptionClassHolder.get(), reactiveInvokeSample.get(), context)); } return Publishers.convertPublisher(conversionService, result, context.getReturnType().getType()); } @@ -143,7 +158,8 @@ public Object intercept(MethodInvocationContext context) { .whenComplete((o, throwable) -> finalizeSamples( timedAnnotations, throwable == null ? "none" : throwable.getClass().getSimpleName(), - completionStageInvokeSamples + completionStageInvokeSamples, + context ) ); return interceptedMethod.handleResult(completionStageResult); @@ -160,7 +176,7 @@ public Object intercept(MethodInvocationContext context) { exceptionClass = e.getClass().getSimpleName(); return interceptedMethod.handleException(e); } finally { - finalizeSamples(timedAnnotations, exceptionClass, syncInvokeSamples != null ? syncInvokeSamples : Collections.emptyList()); + finalizeSamples(timedAnnotations, exceptionClass, syncInvokeSamples != null ? syncInvokeSamples : Collections.emptyList(), context); } } } @@ -178,19 +194,21 @@ private List initSamples(List> timedAnnotat private void finalizeSamples(List> timedAnnotations, String exceptionClass, - List syncInvokeSamples) { + List syncInvokeSamples, + MethodInvocationContext context) { if (CollectionUtils.isNotEmpty(syncInvokeSamples) && timedAnnotations.size() == syncInvokeSamples.size()) { final Iterator> i = timedAnnotations.iterator(); for (Timer.Sample syncInvokeSample : syncInvokeSamples) { final AnnotationValue timedAnn = i.next(); final String metricName = timedAnn.stringValue().orElse(DEFAULT_METRIC_NAME); - stopTimed(metricName, syncInvokeSample, exceptionClass, timedAnn); + stopTimed(metricName, syncInvokeSample, exceptionClass, timedAnn, context); } } } private void stopTimed(String metricName, Timer.Sample sample, - String exceptionClass, AnnotationValue metadata) { + String exceptionClass, AnnotationValue metadata, + MethodInvocationContext context) { try { final String description = metadata.stringValue("description").orElse(null); final String[] tags = metadata.stringValues("extraTags"); @@ -199,6 +217,13 @@ private void stopTimed(String metricName, Timer.Sample sample, final Timer timer = Timer.builder(metricName) .description(description) .tags(tags) + .tags( + methodTaggers.isEmpty() ? Collections.emptyList() : + methodTaggers + .stream() + .flatMap(b -> b.getTags(context).stream()) + .toList() + ) .tags(EXCEPTION_TAG, exceptionClass) .publishPercentileHistogram(histogram) .publishPercentiles(percentiles) 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 30f9f54c6..70d05d149 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 @@ -64,6 +64,25 @@ class CountedAnnotationSpec extends Specification { rxTimer.count() == 1 } + cleanup: + ctx.close() + } + + void "additional tags from taggers are added"() { + given: + ApplicationContext ctx = ApplicationContext.run() + CountedTarget tt = ctx.getBean(CountedTarget) + MeterRegistry registry = ctx.getBean(MeterRegistry) + + when: + int result = tt.max(4, 10) + def timer = registry.get("counted.test.max.blocking").tags("method", "max", "parameters", "a b").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 221b1e35a..d09094e67 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 @@ -1,6 +1,7 @@ package io.micronaut.configuration.metrics.annotation import io.micrometer.core.instrument.MeterRegistry +import io.micrometer.core.instrument.Tag import io.micronaut.context.ApplicationContext import spock.lang.Specification import spock.util.concurrent.PollingConditions @@ -79,4 +80,23 @@ class TimeAnnotationSpec extends Specification { cleanup: ctx.close() } + + void "additional tags from taggers are added"() { + given: + ApplicationContext ctx = ApplicationContext.run() + TimedTarget tt = ctx.getBean(TimedTarget) + MeterRegistry registry = ctx.getBean(MeterRegistry) + + when: + Integer result = tt.max(4, 10) + def timer = registry.get("timed.test.max.blocking").tags("method", "max", "parameters", "a b").timer() + + then: + result == 10 + timer.count() == 1 + timer.totalTime(MILLISECONDS) > 0 + + cleanup: + ctx.close() + } } diff --git a/micrometer-core/src/test/groovy/io/micronaut/docs/MethodNameTagger.java b/micrometer-core/src/test/groovy/io/micronaut/docs/MethodNameTagger.java new file mode 100644 index 000000000..816f72fc2 --- /dev/null +++ b/micrometer-core/src/test/groovy/io/micronaut/docs/MethodNameTagger.java @@ -0,0 +1,17 @@ +package io.micronaut.docs; + +import io.micrometer.core.instrument.Tag; +import io.micronaut.aop.MethodInvocationContext; +import io.micronaut.configuration.metrics.aggregator.AbstractMethodTagger; +import jakarta.inject.Singleton; + +import java.util.Collections; +import java.util.List; + +@Singleton +public class MethodNameTagger extends AbstractMethodTagger { + @Override + public List buildTags(MethodInvocationContext context) { + return Collections.singletonList(Tag.of("method", context.getMethodName())); + } +} diff --git a/micrometer-core/src/test/java/io/micronaut/configuration/metrics/aggregator/IncorrectMethodTaggerExample.java b/micrometer-core/src/test/java/io/micronaut/configuration/metrics/aggregator/IncorrectMethodTaggerExample.java new file mode 100644 index 000000000..863f9d227 --- /dev/null +++ b/micrometer-core/src/test/java/io/micronaut/configuration/metrics/aggregator/IncorrectMethodTaggerExample.java @@ -0,0 +1,19 @@ +package io.micronaut.configuration.metrics.aggregator; + +import io.micrometer.core.instrument.Tag; +import io.micronaut.aop.MethodInvocationContext; +import jakarta.inject.Singleton; + +import java.util.List; + +@Singleton +public class IncorrectMethodTaggerExample extends AbstractMethodTagger { + + /** + * Intentional improper usage for testing it does not stop publishing of metric with other valid tags + */ + @Override + public List buildTags(MethodInvocationContext context) { + return null; + } +} diff --git a/micrometer-core/src/test/java/io/micronaut/configuration/metrics/aggregator/MethodTaggerExample.java b/micrometer-core/src/test/java/io/micronaut/configuration/metrics/aggregator/MethodTaggerExample.java new file mode 100644 index 000000000..d459c8e4e --- /dev/null +++ b/micrometer-core/src/test/java/io/micronaut/configuration/metrics/aggregator/MethodTaggerExample.java @@ -0,0 +1,16 @@ +package io.micronaut.configuration.metrics.aggregator; + +import io.micrometer.core.instrument.Tag; +import io.micronaut.aop.MethodInvocationContext; +import jakarta.inject.Singleton; + +import java.util.List; + +@Singleton +public class MethodTaggerExample extends AbstractMethodTagger { + + @Override + public List buildTags(MethodInvocationContext context) { + return List.of(Tag.of("method", context.getMethodName())); + } +} diff --git a/micrometer-core/src/test/java/io/micronaut/configuration/metrics/aggregator/MethodTaggerExample2.java b/micrometer-core/src/test/java/io/micronaut/configuration/metrics/aggregator/MethodTaggerExample2.java new file mode 100644 index 000000000..04bbbd5d6 --- /dev/null +++ b/micrometer-core/src/test/java/io/micronaut/configuration/metrics/aggregator/MethodTaggerExample2.java @@ -0,0 +1,16 @@ +package io.micronaut.configuration.metrics.aggregator; + +import io.micrometer.core.instrument.Tag; +import io.micronaut.aop.MethodInvocationContext; +import jakarta.inject.Singleton; + +import java.util.List; + +@Singleton +public class MethodTaggerExample2 extends AbstractMethodTagger { + + @Override + public List buildTags(MethodInvocationContext context) { + return List.of(Tag.of("parameters", String.join(" ", context.getParameterValueMap().keySet()))); + } +} diff --git a/src/main/docs/guide/metricsAnnotations.adoc b/src/main/docs/guide/metricsAnnotations.adoc index 48746d3b1..b8b251f89 100644 --- a/src/main/docs/guide/metricsAnnotations.adoc +++ b/src/main/docs/guide/metricsAnnotations.adoc @@ -1,3 +1,12 @@ You can use the Micrometer `@Timed` and `@Counted` annotations on any bean method by adding the `micronaut-micrometer-annotation` dependency to your annotation processor classpath: dependency:micronaut-micrometer-annotation[groupId="io.micronaut.micrometer", scope="annotationProcessor"] + + +In order to support adding additional tags programmatically similar to Micrometer's `TimedAspect` / `CountedAspect` ability using a `ProceedingJoinPoint`, create beans of type `AbstractMethodTagger` + +.MetricsTagger example +[source,java] +---- +include::{testsmetricscore}/MethodNameTagger.java[] +----