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 16, 2023
1 parent 2c45aa6 commit a978c18
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 38 deletions.
54 changes: 53 additions & 1 deletion docs/src/main/asciidoc/telemetry-micrometer.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +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.
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 will have to implement the evaluation of the expression your self
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) { ... }
----


`MeterTag.expression` is currently not supported.

IMPORTANT: Provided tag values MUST BE of LOW-CARDINALITY. If you fail to provide low-cardinality values, that can lead to performance issues of your metrics backend. Values should not come from the end-user since those could be high-cardinality.

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.

== Support for the MicroProfile Metrics API
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,7 @@
import io.quarkus.devui.spi.page.Page;
import io.quarkus.micrometer.deployment.export.PrometheusRegistryProcessor;
import io.quarkus.micrometer.deployment.export.RegistryBuildItem;
import io.quarkus.micrometer.runtime.ClockProvider;
import io.quarkus.micrometer.runtime.CompositeRegistryCreator;
import io.quarkus.micrometer.runtime.MeterFilterConstraint;
import io.quarkus.micrometer.runtime.MeterFilterConstraints;
import io.quarkus.micrometer.runtime.MeterRegistryCustomizer;
import io.quarkus.micrometer.runtime.MeterRegistryCustomizerConstraint;
import io.quarkus.micrometer.runtime.MeterRegistryCustomizerConstraints;
import io.quarkus.micrometer.runtime.MicrometerCounted;
import io.quarkus.micrometer.runtime.MicrometerCountedInterceptor;
import io.quarkus.micrometer.runtime.MicrometerRecorder;
import io.quarkus.micrometer.runtime.MicrometerTimedInterceptor;
import io.quarkus.micrometer.runtime.*;
import io.quarkus.micrometer.runtime.config.MicrometerConfig;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.metrics.MetricsFactory;
Expand All @@ -73,6 +63,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 +114,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 a978c18

Please sign in to comment.