Skip to content

Commit

Permalink
Support Micrometer @MeterTag
Browse files Browse the repository at this point in the history
  • Loading branch information
computerlove committed Nov 27, 2023
1 parent 451260f commit 983dce2
Show file tree
Hide file tree
Showing 12 changed files with 248 additions and 45 deletions.
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);
}

}
Loading

0 comments on commit 983dce2

Please sign in to comment.