Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Micrometer @MeterTag #36945

Merged
merged 1 commit into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions docs/src/main/asciidoc/telemetry-micrometer.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -603,8 +603,59 @@ The `@Timed` annotation will wrap the execution of a method and will emit the fo
in addition to any tags defined on the annotation itself:
class, method, and exception (either "none" or the simple class name of a detected exception).

Using annotations is limited, as you can't dynamically assign meaningful tag values.
Also note that many methods, e.g. REST endpoint methods or Vert.x Routes, are counted and timed by the micrometer extension out of the box.
Parameters to `@Counted` and `@Timed` can be annotated with `@MeterTag` to dynamically assign meaningful tag values.

`MeterTag.resolver` can be used to extract a tag from a method parameter, by creating a bean
implementing `io.micrometer.common.annotation.ValueResolver` and referring to this class: `@MeterTag(resolver=CustomResolver.class)

`MeterTag.expression` is also supported, but you have to implement the evaluation of the expression
by creating a bean implementing `io.micrometer.common.annotation.ValueExpressionResolver` that can evaluate expressions.

[source,java]
----
enum Currency { USD, EUR }

@Singleton
class EnumOrdinalResolver implements ValueResolver {
@Override
public String resolve(Object parameter) {
if(parameter instanceof Enum) {
return String.valueOf(((Enum<?>) parameter).ordinal());
}
return null;
}
}

@Singleton
public class MyExpressionResolver implements ValueExpressionResolver {
@Override
public String resolve(String expression, Object parameter) {
return someParser.parse(expression).evaluate(parameter);
}
}

// tags = type=with_enum, currency=${currency.toString()}
@Timed(value="time_something", extraTags = {"type", "with_enum"})
public Something calculateSomething(@MeterTag Currency currency) { ... }

// tags = type=with_enum, the_currency=${currency.toString()}
@Timed(value="time_something", extraTags = {"type", "with_enum"})
public Something calculateSomething(@MeterTag(key="the_currency") Currency currency) { ... }

// tags = type=with_enum, currency=${currency.ordinal()}
@Timed(value="time_something", extraTags = {"type", "with_enum"})
public Something calculateSomething(@MeterTag(resolver=EnumOrdinalResolver.class) Currency currency) { ... }

// tags = type=with_enum, currency=${currency.ordinal()}
@Timed(value="time_something", extraTags = {"type", "with_enum"})
public Something calculateSomething(@MeterTag(expression="currency.ordinal()") Currency currency) { ... }
----

IMPORTANT: Provided tag values MUST BE of LOW-CARDINALITY.
High-cardinality values can lead to performance and storage issues in your metrics backend (a "cardinality explosion").
Tag values should not use end-user data, since those could be high-cardinality.

Many methods, like REST endpoint methods or Vert.x Routes, are counted and timed by the micrometer extension out of the box.

== Support for the MicroProfile Metrics API

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import io.quarkus.micrometer.runtime.MeterRegistryCustomizer;
import io.quarkus.micrometer.runtime.MeterRegistryCustomizerConstraint;
import io.quarkus.micrometer.runtime.MeterRegistryCustomizerConstraints;
import io.quarkus.micrometer.runtime.MeterTagsSupport;
import io.quarkus.micrometer.runtime.MicrometerCounted;
import io.quarkus.micrometer.runtime.MicrometerCountedInterceptor;
import io.quarkus.micrometer.runtime.MicrometerRecorder;
Expand All @@ -73,6 +74,7 @@ public class MicrometerProcessor {
private static final DotName COUNTED_INTERCEPTOR = DotName.createSimple(MicrometerCountedInterceptor.class.getName());
private static final DotName TIMED_ANNOTATION = DotName.createSimple(Timed.class.getName());
private static final DotName TIMED_INTERCEPTOR = DotName.createSimple(MicrometerTimedInterceptor.class.getName());
private static final DotName METER_TAG_SUPPORT = DotName.createSimple(MeterTagsSupport.class.getName());

public static class MicrometerEnabled implements BooleanSupplier {
MicrometerConfig mConfig;
Expand Down Expand Up @@ -123,6 +125,7 @@ UnremovableBeanBuildItem registerAdditionalBeans(CombinedIndexBuildItem indexBui
.addBeanClass(COUNTED_ANNOTATION.toString())
.addBeanClass(COUNTED_BINDING.toString())
.addBeanClass(COUNTED_INTERCEPTOR.toString())
.addBeanClass(METER_TAG_SUPPORT.toString())
.build());

// @Timed is registered as an additional interceptor binding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.quarkus.micrometer.test.CountedResource;
import io.quarkus.micrometer.test.GuardedResult;
import io.quarkus.micrometer.test.TestValueResolver;
import io.quarkus.micrometer.test.TimedResource;
import io.quarkus.test.QuarkusUnitTest;
import io.smallrye.mutiny.Uni;
Expand All @@ -30,6 +31,7 @@ public class MicrometerCounterInterceptorTest {
.overrideConfigKey("quarkus.micrometer.registry-enabled-default", "false")
.overrideConfigKey("quarkus.redis.devservices.enabled", "false")
.withApplicationRoot((jar) -> jar
.addClass(TestValueResolver.class)
.addClass(CountedResource.class)
.addClass(TimedResource.class)
.addClass(GuardedResult.class));
Expand Down Expand Up @@ -58,6 +60,7 @@ void testCountAllMetrics_MetricsOnSuccess() {
.tag("method", "countAllInvocations")
.tag("class", "io.quarkus.micrometer.test.CountedResource")
.tag("extra", "tag")
.tag("do_fail", "prefix_false")
.tag("exception", "none")
.tag("result", "success").counter();
Assertions.assertNotNull(counter);
Expand All @@ -71,6 +74,7 @@ void testCountAllMetrics_MetricsOnFailure() {
.tag("method", "countAllInvocations")
.tag("class", "io.quarkus.micrometer.test.CountedResource")
.tag("extra", "tag")
.tag("do_fail", "prefix_true")
.tag("exception", "NullPointerException")
.tag("result", "failure").counter();
Assertions.assertNotNull(counter);
Expand All @@ -85,6 +89,7 @@ void testCountEmptyMetricName_Success() {
.tag("method", "emptyMetricName")
.tag("class", "io.quarkus.micrometer.test.CountedResource")
.tag("exception", "none")
.tag("fail", "false")
.tag("result", "success").counter();
Assertions.assertNotNull(counter);
Assertions.assertEquals(1, counter.count());
Expand All @@ -98,6 +103,7 @@ void testCountEmptyMetricName_Failure() {
.tag("method", "emptyMetricName")
.tag("class", "io.quarkus.micrometer.test.CountedResource")
.tag("exception", "NullPointerException")
.tag("fail", "true")
.tag("result", "failure").counter();
Assertions.assertNotNull(counter);
Assertions.assertEquals(1, counter.count());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import jakarta.enterprise.context.ApplicationScoped;

import io.micrometer.core.annotation.Counted;
import io.micrometer.core.aop.MeterTag;
import io.smallrye.mutiny.Uni;

@ApplicationScoped
Expand All @@ -16,14 +17,14 @@ public void onlyCountFailures() {
}

@Counted(value = "metric.all", extraTags = { "extra", "tag" })
public void countAllInvocations(boolean fail) {
public void countAllInvocations(@MeterTag(key = "do_fail", resolver = TestValueResolver.class) boolean fail) {
if (fail) {
throw new NullPointerException("Failed on purpose");
}
}

@Counted(description = "nice description")
public void emptyMetricName(boolean fail) {
public void emptyMetricName(@MeterTag boolean fail) {
if (fail) {
throw new NullPointerException("Failed on purpose");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.quarkus.micrometer.test;

import jakarta.inject.Singleton;

import io.micrometer.common.annotation.ValueResolver;

@Singleton
public class TestValueResolver implements ValueResolver {
@Override
public String resolve(Object parameter) {
return "prefix_" + parameter;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package io.quarkus.micrometer.runtime;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jakarta.enterprise.inject.Instance;
import jakarta.inject.Singleton;

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.core.aop.MeterTag;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.quarkus.arc.All;
import io.quarkus.arc.ArcInvocationContext;
import io.quarkus.arc.ClientProxy;

@Singleton
public class MeterTagsSupport {
private final Map<Class<?>, ValueResolver> valueResolvers;
private final ValueExpressionResolver valueExpressionResolver;

public MeterTagsSupport(@All List<ValueResolver> valueResolvers,
Instance<ValueExpressionResolver> valueExpressionResolver) {
this.valueResolvers = createValueResolverMap(valueResolvers);
this.valueExpressionResolver = valueExpressionResolver.isUnsatisfied() ? null : valueExpressionResolver.get();
}

Tags getTags(ArcInvocationContext context) {
return getCommonTags(context)
.and(getMeterTags(context));
}

private Tags getMeterTags(ArcInvocationContext context) {
List<Tag> tags = new ArrayList<>();
Method method = context.getMethod();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
Parameter methodParameter = parameters[i];
MeterTag annotation = methodParameter.getAnnotation(MeterTag.class);
if (annotation != null) {
Object parameterValue = context.getParameters()[i];

tags.add(Tag.of(
resolveTagKey(annotation, methodParameter.getName()),
resolveTagValue(annotation, parameterValue)));
}
}
return Tags.of(tags);
}

private static Tags getCommonTags(ArcInvocationContext context) {
Method method = context.getMethod();
String className = method.getDeclaringClass().getName();
String methodName = method.getName();
return Tags.of("class", className, "method", methodName);
}

/*
* Precedence copied from MeterTagAnnotationHandler
*/
private String resolveTagValue(MeterTag annotation, Object parameterValue) {
if (annotation.resolver() != NoOpValueResolver.class) {
ValueResolver valueResolver = valueResolvers.get(annotation.resolver());
return valueResolver.resolve(parameterValue);
} else if (StringUtils.isNotBlank(annotation.expression())) {
if (valueExpressionResolver == null) {
throw new IllegalArgumentException("No valueExpressionResolver is defined");
}
return valueExpressionResolver.resolve(annotation.expression(), parameterValue);
} else if (parameterValue != null) {
return parameterValue.toString();
} else {
return "";
}
}

/*
* Precedence copied from MeterTagAnnotationHandler
*/
private static String resolveTagKey(MeterTag annotation, String parameterName) {
if (StringUtils.isNotBlank(annotation.value())) {
return annotation.value();
} else if (StringUtils.isNotBlank(annotation.key())) {
return annotation.key();
} else {
return parameterName;
}
}

private static Map<Class<?>, ValueResolver> createValueResolverMap(List<ValueResolver> valueResolvers) {
Map<Class<?>, ValueResolver> valueResolverMap = new HashMap<>();
for (ValueResolver valueResolver : valueResolvers) {
ValueResolver instance = ClientProxy.unwrap(valueResolver);
valueResolverMap.put(instance.getClass(), valueResolver);
}
return valueResolverMap;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ public class MicrometerCountedInterceptor {
public final String RESULT_TAG_SUCCESS_VALUE = "success";

private final MeterRegistry meterRegistry;
private final MeterTagsSupport meterTagsSupport;

public MicrometerCountedInterceptor(MeterRegistry meterRegistry) {
public MicrometerCountedInterceptor(MeterRegistry meterRegistry, MeterTagsSupport meterTagsSupport) {
this.meterRegistry = meterRegistry;
this.meterTagsSupport = meterTagsSupport;
}

/**
Expand All @@ -61,42 +63,42 @@ Object countedMethod(ArcInvocationContext context) throws Exception {
return context.proceed();
}
Method method = context.getMethod();
Tags commonTags = getCommonTags(method.getDeclaringClass().getName(), method.getName());
Tags tags = meterTagsSupport.getTags(context);

Class<?> returnType = method.getReturnType();
if (TypesUtil.isCompletionStage(returnType)) {
try {
return ((CompletionStage<?>) context.proceed()).whenComplete(new BiConsumer<Object, Throwable>() {
@Override
public void accept(Object o, Throwable throwable) {
recordCompletionResult(counted, commonTags, throwable);
recordCompletionResult(counted, tags, throwable);
}
});
} catch (Throwable e) {
record(counted, commonTags, e);
record(counted, tags, e);
}
} else if (TypesUtil.isUni(returnType)) {
try {
return ((Uni<Object>) context.proceed()).onTermination().invoke(
new Functions.TriConsumer<>() {
@Override
public void accept(Object o, Throwable throwable, Boolean cancelled) {
recordCompletionResult(counted, commonTags, throwable);
recordCompletionResult(counted, tags, throwable);
}
});
} catch (Throwable e) {
record(counted, commonTags, e);
record(counted, tags, e);
}
}

try {
Object result = context.proceed();
if (!counted.recordFailuresOnly()) {
record(counted, commonTags, null);
record(counted, tags, null);
}
return result;
} catch (Throwable e) {
record(counted, commonTags, e);
record(counted, tags, e);
throw e;
}
}
Expand All @@ -122,8 +124,4 @@ private void record(MicrometerCounted counted, Tags commonTags, Throwable throwa
builder.register(meterRegistry).increment();
}

private Tags getCommonTags(String className, String methodName) {
return Tags.of("class", className, "method", methodName);
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.quarkus.micrometer.runtime;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -33,9 +32,11 @@ public class MicrometerTimedInterceptor {
public static final String DEFAULT_METRIC_NAME = "method.timed";

private final MeterRegistry meterRegistry;
private final MeterTagsSupport meterTagsSupport;

public MicrometerTimedInterceptor(MeterRegistry meterRegistry) {
public MicrometerTimedInterceptor(MeterRegistry meterRegistry, MeterTagsSupport meterTagsSupport) {
this.meterRegistry = meterRegistry;
this.meterTagsSupport = meterTagsSupport;
}

@AroundInvoke
Expand Down Expand Up @@ -85,18 +86,17 @@ public void accept(Object o, Throwable throwable, Boolean cancelled) {
}

private List<Sample> getSamples(ArcInvocationContext context) {
Method method = context.getMethod();
Tags commonTags = getCommonTags(method.getDeclaringClass().getName(), method.getName());
List<Timed> timed = context.findIterceptorBindings(Timed.class);
if (timed.isEmpty()) {
return Collections.emptyList();
}
Tags tags = meterTagsSupport.getTags(context);
List<Sample> samples = new ArrayList<>(timed.size());
for (Timed t : timed) {
if (t.longTask()) {
samples.add(new LongTimerSample(t, commonTags));
samples.add(new LongTimerSample(t, tags));
} else {
samples.add(new TimerSample(t, commonTags));
samples.add(new TimerSample(t, tags));
}
}
return samples;
Expand Down
Loading
Loading