Skip to content

Commit

Permalink
feat: Build more tags using method context (#753)
Browse files Browse the repository at this point in the history
  • Loading branch information
hrothwell authored May 13, 2024
1 parent 2e56a83 commit d43366c
Show file tree
Hide file tree
Showing 11 changed files with 240 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -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<? extends AbstractMethodTagger> 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<Tag> buildTags(@NonNull MethodInvocationContext<Object, Object> context);

@NonNull public final List<Tag> getTags(@NonNull MethodInvocationContext<Object, Object> context) {
List<Tag> 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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -49,9 +53,9 @@ public class CountedInterceptor implements MethodInterceptor<Object, Object> {

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<AbstractMethodTagger> methodTaggers;

/**
* @param meterRegistry The meter registry
Expand All @@ -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<AbstractMethodTagger> methodTaggers) {
this.meterRegistry = meterRegistry;
this.conversionService = conversionService;
this.methodTaggers = Objects.requireNonNullElse(methodTaggers, Collections.emptyList());
}

@Override
Expand All @@ -90,20 +106,20 @@ public Object intercept(MethodInvocationContext<Object, Object> 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 -> {
Expand All @@ -112,7 +128,7 @@ public Object intercept(MethodInvocationContext<Object, Object> context) {
return result;
} finally {
if (metadata.isFalse(Counted.class, "recordFailuresOnly")) {
doCount(metadata, metricName, null);
doCount(metadata, metricName, null, context);
}
}
}
Expand All @@ -124,16 +140,23 @@ public Object intercept(MethodInvocationContext<Object, Object> 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<Object, Object> 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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -80,24 +82,37 @@ public class TimedInterceptor implements MethodInterceptor<Object, Object> {

private final MeterRegistry meterRegistry;
private final ConversionService conversionService;
private final List<AbstractMethodTagger> methodTaggers;

/**
* @param meterRegistry The meter registry
* @deprecated Pass conversion service in new constructor
*/
@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<AbstractMethodTagger> methodTaggers) {
this.meterRegistry = meterRegistry;
this.conversionService = conversionService;
this.methodTaggers = Objects.requireNonNullElse(methodTaggers, Collections.emptyList());
}

@Override
Expand Down Expand Up @@ -125,14 +140,14 @@ public Object intercept(MethodInvocationContext<Object, Object> 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<String> 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());
}
Expand All @@ -143,7 +158,8 @@ public Object intercept(MethodInvocationContext<Object, Object> context) {
.whenComplete((o, throwable) ->
finalizeSamples(
timedAnnotations, throwable == null ? "none" : throwable.getClass().getSimpleName(),
completionStageInvokeSamples
completionStageInvokeSamples,
context
)
);
return interceptedMethod.handleResult(completionStageResult);
Expand All @@ -160,7 +176,7 @@ public Object intercept(MethodInvocationContext<Object, Object> 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);
}
}
}
Expand All @@ -178,19 +194,21 @@ private List<Timer.Sample> initSamples(List<AnnotationValue<Timed>> timedAnnotat

private void finalizeSamples(List<AnnotationValue<Timed>> timedAnnotations,
String exceptionClass,
List<Timer.Sample> syncInvokeSamples) {
List<Timer.Sample> syncInvokeSamples,
MethodInvocationContext<Object, Object> context) {
if (CollectionUtils.isNotEmpty(syncInvokeSamples) && timedAnnotations.size() == syncInvokeSamples.size()) {
final Iterator<AnnotationValue<Timed>> i = timedAnnotations.iterator();
for (Timer.Sample syncInvokeSample : syncInvokeSamples) {
final AnnotationValue<Timed> 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<Timed> metadata) {
String exceptionClass, AnnotationValue<Timed> metadata,
MethodInvocationContext<Object, Object> context) {
try {
final String description = metadata.stringValue("description").orElse(null);
final String[] tags = metadata.stringValues("extraTags");
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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<Tag> buildTags(MethodInvocationContext<Object, Object> context) {
return Collections.singletonList(Tag.of("method", context.getMethodName()));
}
}
Loading

0 comments on commit d43366c

Please sign in to comment.