diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java index 1eb7a1939c498..78a7ae4bb1881 100644 --- a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java +++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java @@ -3,11 +3,14 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.BooleanSupplier; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationTarget.Kind; +import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.FieldInfo; @@ -23,9 +26,12 @@ import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; import io.quarkus.arc.deployment.BeanContainerBuildItem; +import io.quarkus.arc.deployment.InterceptorBindingRegistrarBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; +import io.quarkus.arc.processor.Annotations; import io.quarkus.arc.processor.AnnotationsTransformer; import io.quarkus.arc.processor.DotNames; +import io.quarkus.arc.processor.InterceptorBindingRegistrar; import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -45,7 +51,6 @@ import io.quarkus.micrometer.runtime.MicrometerCounted; import io.quarkus.micrometer.runtime.MicrometerCountedInterceptor; import io.quarkus.micrometer.runtime.MicrometerRecorder; -import io.quarkus.micrometer.runtime.MicrometerTimed; import io.quarkus.micrometer.runtime.MicrometerTimedInterceptor; import io.quarkus.micrometer.runtime.config.MicrometerConfig; import io.quarkus.runtime.RuntimeValue; @@ -62,7 +67,6 @@ public class MicrometerProcessor { private static final DotName COUNTED_BINDING = DotName.createSimple(MicrometerCounted.class.getName()); 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_BINDING = DotName.createSimple(MicrometerTimed.class.getName()); private static final DotName TIMED_INTERCEPTOR = DotName.createSimple(MicrometerTimedInterceptor.class.getName()); public static class MicrometerEnabled implements BooleanSupplier { @@ -97,7 +101,8 @@ MetricsCapabilityBuildItem metricsCapabilityPrometheusBuildItem( UnremovableBeanBuildItem registerAdditionalBeans(CombinedIndexBuildItem indexBuildItem, BuildProducer providerClasses, BuildProducer reflectiveClasses, - BuildProducer additionalBeans) { + BuildProducer additionalBeans, + BuildProducer interceptorBindings) { // Create and keep some basic Providers additionalBeans.produce(AdditionalBeanBuildItem.builder() @@ -111,13 +116,22 @@ UnremovableBeanBuildItem registerAdditionalBeans(CombinedIndexBuildItem indexBui .addBeanClass(MeterFilterConstraint.class) .addBeanClass(MeterFilterConstraints.class) .addBeanClass(TIMED_ANNOTATION.toString()) - .addBeanClass(TIMED_BINDING.toString()) .addBeanClass(TIMED_INTERCEPTOR.toString()) .addBeanClass(COUNTED_ANNOTATION.toString()) .addBeanClass(COUNTED_BINDING.toString()) .addBeanClass(COUNTED_INTERCEPTOR.toString()) .build()); + // @Timed is registered as an additional interceptor binding + interceptorBindings.produce(new InterceptorBindingRegistrarBuildItem(new InterceptorBindingRegistrar() { + // TODO replace with the new API from https://github.com/quarkusio/quarkus/pull/19064 + @Override + public Map> registerAdditionalBindings() { + return Map.of(DotName.createSimple(Timed.class.getName()), + Set.of("value", "extraTags", "longTask", "percentiles", "histogram", "description")); + } + })); + IndexView index = indexBuildItem.getIndex(); // Find classes that define MeterRegistries, MeterBinders, and MeterFilters @@ -182,9 +196,27 @@ void collectNames(Collection classes, Collection names) { } @BuildStep(onlyIf = MicrometerEnabled.class) - void processAnnotatedMetrics(BuildProducer annotationsTransformers) { - annotationsTransformers.produce(createAnnotationTransformer(COUNTED_ANNOTATION, COUNTED_BINDING)); - annotationsTransformers.produce(createAnnotationTransformer(TIMED_ANNOTATION, TIMED_BINDING)); + AnnotationsTransformerBuildItem processAnnotatedMetrics( + BuildProducer annotationsTransformers) { + return new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { + + @Override + public boolean appliesTo(Kind kind) { + // @Counted is only applicable to a method + return kind == Kind.METHOD; + } + + @Override + public void transform(TransformationContext ctx) { + final Collection annotations = ctx.getAnnotations(); + AnnotationInstance counted = Annotations.find(annotations, COUNTED_ANNOTATION); + if (counted == null) { + return; + } + // Copy all the values so that the interceptor can use the binding annotation instead of java.lang.reflect.Method + ctx.transform().add(COUNTED_BINDING, counted.values().toArray(new AnnotationValue[] {})).done(); + } + }); } @BuildStep(onlyIf = MicrometerEnabled.class) @@ -238,15 +270,6 @@ void configureRegistry(MicrometerRecorder recorder, } } - public static AnnotationInstance findAnnotation(Collection annotations, DotName annotationClass) { - for (AnnotationInstance a : annotations) { - if (annotationClass.equals(a.name())) { - return a; - } - } - return null; - } - ReflectiveClassBuildItem createReflectiveBuildItem(DotName sourceAnnotation, IndexView index) { Set classes = new HashSet<>(); @@ -268,17 +291,4 @@ ReflectiveClassBuildItem createReflectiveBuildItem(DotName sourceAnnotation, Ind return ReflectiveClassBuildItem.builder(classes.toArray(new String[0])).build(); } - AnnotationsTransformerBuildItem createAnnotationTransformer(DotName sourceAnnotation, DotName bindingAnnotation) { - return new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { - @Override - public void transform(TransformationContext ctx) { - final Collection annotations = ctx.getAnnotations(); - AnnotationInstance annotation = MicrometerProcessor.findAnnotation(annotations, sourceAnnotation); - if (annotation == null) { - return; - } - ctx.transform().add(bindingAnnotation).done(); - } - }); - } } diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/mpmetrics/AnnotationHandler.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/mpmetrics/AnnotationHandler.java index bd4490c904b2d..ff3ab91c0d757 100644 --- a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/mpmetrics/AnnotationHandler.java +++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/mpmetrics/AnnotationHandler.java @@ -6,9 +6,9 @@ import org.jboss.logging.Logger; import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; +import io.quarkus.arc.processor.Annotations; import io.quarkus.arc.processor.AnnotationsTransformer; import io.quarkus.arc.processor.DotNames; -import io.quarkus.micrometer.deployment.MicrometerProcessor; /** * Create beans to handle MP Metrics API annotations. @@ -28,7 +28,7 @@ static AnnotationsTransformerBuildItem transformAnnotations(final IndexView inde @Override public void transform(TransformationContext ctx) { final Collection annotations = ctx.getAnnotations(); - AnnotationInstance annotation = MicrometerProcessor.findAnnotation(annotations, sourceAnnotation); + AnnotationInstance annotation = Annotations.find(annotations, sourceAnnotation); if (annotation == null) { return; } @@ -78,9 +78,8 @@ static boolean removeCountedWhenTimed(DotName sourceAnnotation, AnnotationTarget MethodInfo methodInfo) { if (MetricDotNames.COUNTED_ANNOTATION.equals(sourceAnnotation)) { if (methodInfo == null) { - if (MicrometerProcessor.findAnnotation(classInfo.classAnnotations(), MetricDotNames.TIMED_ANNOTATION) == null && - MicrometerProcessor.findAnnotation(classInfo.classAnnotations(), - MetricDotNames.SIMPLY_TIMED_ANNOTATION) == null) { + if (Annotations.contains(classInfo.classAnnotations(), MetricDotNames.TIMED_ANNOTATION) && + Annotations.contains(classInfo.classAnnotations(), MetricDotNames.SIMPLY_TIMED_ANNOTATION)) { return false; } log.warnf("Bean %s is both counted and timed. The @Counted annotation " + diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/runtime/MicrometerTimedInterceptorTest.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/runtime/MicrometerTimedInterceptorTest.java index 02d4d84684815..090c2060be30b 100644 --- a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/runtime/MicrometerTimedInterceptorTest.java +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/runtime/MicrometerTimedInterceptorTest.java @@ -154,4 +154,23 @@ void testTimeMethod_LongTaskTimer_AsyncFailed() { Assertions.assertEquals(0, timer.activeTasks()); } + @Test + void testTimeMethod_repeatable() { + timed.repeatableCall(false); + Timer alphaTimer = registry.get("alpha") + .tag("method", "repeatableCall") + .tag("class", "io.quarkus.micrometer.test.TimedResource") + .tag("exception", "none") + .tag("extra", "tag").timer(); + Assertions.assertNotNull(alphaTimer); + Assertions.assertEquals(1, alphaTimer.count()); + Timer bravoTimer = registry.get("bravo") + .tag("method", "repeatableCall") + .tag("class", "io.quarkus.micrometer.test.TimedResource") + .tag("exception", "none") + .tag("extra", "tag").timer(); + Assertions.assertNotNull(bravoTimer); + Assertions.assertEquals(1, bravoTimer.count()); + } + } diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/TimedResource.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/TimedResource.java index d4856e8bc600b..f40895f1e8b73 100644 --- a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/TimedResource.java +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/TimedResource.java @@ -42,4 +42,12 @@ public CompletableFuture longAsyncCall(GuardedResult guardedResult) { } return supplyAsync(guardedResult::get); } + + @Timed(value = "alpha", extraTags = { "extra", "tag" }) + @Timed(value = "bravo", extraTags = { "extra", "tag" }) + public void repeatableCall(boolean fail) { + if (fail) { + throw new NullPointerException("Failed on purpose"); + } + } } diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerCounted.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerCounted.java index 7cad4a9914812..985410fc6d3fc 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerCounted.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerCounted.java @@ -6,11 +6,38 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import javax.enterprise.util.Nonbinding; import javax.interceptor.InterceptorBinding; +import io.micrometer.core.annotation.Counted; + @Inherited @InterceptorBinding @Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface MicrometerCounted { + + /** + * @see Counted#value() + */ + @Nonbinding + String value() default "method.counted"; + + /** + * @see Counted#recordFailuresOnly() + */ + @Nonbinding + boolean recordFailuresOnly() default false; + + /** + * @see Counted#extraTags() + */ + @Nonbinding + String[] extraTags() default {}; + + /** + * @see Counted#description() + */ + @Nonbinding + String description() default ""; } diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerCountedInterceptor.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerCountedInterceptor.java index 82968c20efe16..a0d081acc7838 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerCountedInterceptor.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerCountedInterceptor.java @@ -1,6 +1,8 @@ package io.quarkus.micrometer.runtime; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.Set; import java.util.concurrent.CompletionStage; import javax.annotation.Priority; @@ -12,6 +14,7 @@ import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tags; +import io.quarkus.arc.ArcInvocationContext; /** * Quarkus declared interceptor responsible for intercepting all methods @@ -52,12 +55,11 @@ public MicrometerCountedInterceptor(MeterRegistry meterRegistry) { */ @AroundInvoke Object countedMethod(InvocationContext context) throws Exception { - Method method = context.getMethod(); - Counted counted = method.getAnnotation(Counted.class); + MicrometerCounted counted = getCounted(context); if (counted == null) { return context.proceed(); } - + Method method = context.getMethod(); Tags commonTags = getCommonTags(method.getDeclaringClass().getName(), method.getName()); // If we're working with a CompletionStage @@ -84,7 +86,7 @@ Object countedMethod(InvocationContext context) throws Exception { } } - private void recordCompletionResult(Counted counted, Tags commonTags, Throwable throwable) { + private void recordCompletionResult(MicrometerCounted counted, Tags commonTags, Throwable throwable) { if (throwable != null) { record(counted, commonTags, throwable); } else if (!counted.recordFailuresOnly()) { @@ -92,7 +94,7 @@ private void recordCompletionResult(Counted counted, Tags commonTags, Throwable } } - private void record(Counted counted, Tags commonTags, Throwable throwable) { + private void record(MicrometerCounted counted, Tags commonTags, Throwable throwable) { Counter.Builder builder = Counter.builder(counted.value()) .tags(commonTags) .tags(counted.extraTags()) @@ -108,4 +110,16 @@ private void record(Counted counted, Tags commonTags, Throwable throwable) { private Tags getCommonTags(String className, String methodName) { return Tags.of("class", className, "method", methodName); } + + private MicrometerCounted getCounted(InvocationContext context) { + @SuppressWarnings("unchecked") + Set bindings = (Set) context.getContextData() + .get(ArcInvocationContext.KEY_INTERCEPTOR_BINDINGS); + for (Annotation annotation : bindings) { + if (annotation.annotationType().equals(MicrometerCounted.class)) { + return (MicrometerCounted) annotation; + } + } + return null; + } } diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerTimed.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerTimed.java deleted file mode 100644 index 2e90821cea451..0000000000000 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerTimed.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.quarkus.micrometer.runtime; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import javax.interceptor.InterceptorBinding; - -@Inherited -@InterceptorBinding -@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.METHOD }) -@Retention(RetentionPolicy.RUNTIME) -public @interface MicrometerTimed { -} diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerTimedInterceptor.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerTimedInterceptor.java index 7f843fc6aa804..b7af4a0f77ed2 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerTimedInterceptor.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerTimedInterceptor.java @@ -1,6 +1,11 @@ package io.quarkus.micrometer.runtime; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; import java.util.concurrent.CompletionStage; import javax.annotation.Priority; @@ -15,16 +20,16 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.Timer; +import io.quarkus.arc.ArcInvocationContext; /** * Quarkus defined interceptor for types or methods annotated with {@link Timed @Timed}. - * - * @see Timed */ @Interceptor -@MicrometerTimed +@Timed @Priority(Interceptor.Priority.LIBRARY_BEFORE + 10) public class MicrometerTimedInterceptor { + private static final Logger log = Logger.getLogger(MicrometerTimedInterceptor.class); public static final String DEFAULT_METRIC_NAME = "method.timed"; @@ -36,41 +41,25 @@ public MicrometerTimedInterceptor(MeterRegistry meterRegistry) { @AroundInvoke Object timedMethod(InvocationContext context) throws Exception { - Method method = context.getMethod(); - Timed timed = method.getAnnotation(Timed.class); - if (timed == null) { - return context.proceed(); - } + final boolean stopWhenCompleted = CompletionStage.class.isAssignableFrom(context.getMethod().getReturnType()); + final List samples = getSamples(context); - Tags commonTags = getCommonTags(method.getDeclaringClass().getName(), method.getName()); - final boolean stopWhenCompleted = CompletionStage.class.isAssignableFrom(method.getReturnType()); - - return time(context, timed, commonTags, stopWhenCompleted); - } - - Object time(InvocationContext context, Timed timed, Tags commonTags, boolean stopWhenCompleted) throws Exception { - final String metricName = timed.value().isEmpty() ? DEFAULT_METRIC_NAME : timed.value(); - - if (timed.longTask()) { - return processWithLongTaskTimer(context, timed, commonTags, metricName, stopWhenCompleted); - } else { - return processWithTimer(context, timed, commonTags, metricName, stopWhenCompleted); + if (samples.isEmpty()) { + // This should never happen - at least one @Timed binding must be present + return context.proceed(); } - } - - private Object processWithTimer(InvocationContext context, Timed timed, Tags commonTags, String metricName, - boolean stopWhenCompleted) throws Exception { - - Timer.Sample sample = Timer.start(meterRegistry); - Tags timerTags = Tags.concat(commonTags, timed.extraTags()); if (stopWhenCompleted) { try { return ((CompletionStage) context.proceed()).whenComplete((result, throwable) -> { - record(timed, metricName, sample, MicrometerRecorder.getExceptionTag(throwable), timerTags); + for (Sample sample : samples) { + sample.stop(MicrometerRecorder.getExceptionTag(throwable)); + } }); } catch (Exception ex) { - record(timed, metricName, sample, MicrometerRecorder.getExceptionTag(ex), timerTags); + for (Sample sample : samples) { + sample.stop(MicrometerRecorder.getExceptionTag(ex)); + } throw ex; } } @@ -82,11 +71,40 @@ private Object processWithTimer(InvocationContext context, Timed timed, Tags com exceptionClass = MicrometerRecorder.getExceptionTag(ex); throw ex; } finally { - record(timed, metricName, sample, exceptionClass, timerTags); + for (Sample sample : samples) { + sample.stop(exceptionClass); + } } } - private void record(Timed timed, String metricName, Timer.Sample sample, String exceptionClass, Tags timerTags) { + private List getSamples(InvocationContext context) { + Method method = context.getMethod(); + Tags commonTags = getCommonTags(method.getDeclaringClass().getName(), method.getName()); + List timed = new ArrayList<>(); + @SuppressWarnings("unchecked") + Set bindings = (Set) context.getContextData() + .get(ArcInvocationContext.KEY_INTERCEPTOR_BINDINGS); + for (Annotation annotation : bindings) { + if (annotation.annotationType().equals(Timed.class)) { + timed.add((Timed) annotation); + } + } + if (timed.isEmpty()) { + return Collections.emptyList(); + } + List samples = new ArrayList<>(timed.size()); + for (Timed t : timed) { + if (t.longTask()) { + samples.add(new LongTimerSample(t, commonTags)); + } else { + samples.add(new TimerSample(t, commonTags)); + } + } + return samples; + } + + private void record(Timed timed, Timer.Sample sample, String exceptionClass, Tags timerTags) { + final String metricName = timed.value().isEmpty() ? DEFAULT_METRIC_NAME : timed.value(); try { Timer.Builder builder = Timer.builder(metricName) .description(timed.description().isEmpty() ? null : timed.description()) @@ -103,30 +121,6 @@ private void record(Timed timed, String metricName, Timer.Sample sample, String } } - private Object processWithLongTaskTimer(InvocationContext context, Timed timed, Tags commonTags, String metricName, - boolean stopWhenCompleted) throws Exception { - LongTaskTimer.Sample sample = startLongTaskTimer(timed, commonTags, metricName); - if (sample == null) { - return context.proceed(); - } - - if (stopWhenCompleted) { - try { - return ((CompletionStage) context.proceed()) - .whenComplete((result, throwable) -> stopLongTaskTimer(metricName, sample)); - } catch (Exception ex) { - stopLongTaskTimer(metricName, sample); - throw ex; - } - } - - try { - return context.proceed(); - } finally { - stopLongTaskTimer(metricName, sample); - } - } - LongTaskTimer.Sample startLongTaskTimer(Timed timed, Tags commonTags, String metricName) { try { // This will throw if the annotation is incorrect. @@ -156,4 +150,53 @@ private void stopLongTaskTimer(String metricName, LongTaskTimer.Sample sample) { private Tags getCommonTags(String className, String methodName) { return Tags.of("class", className, "method", methodName); } + + abstract class Sample { + + protected final Timed timed; + protected final Tags commonTags; + + public Sample(Timed timed, Tags commonTags) { + this.timed = timed; + this.commonTags = commonTags; + } + + String metricName() { + return timed.value().isEmpty() ? DEFAULT_METRIC_NAME : timed.value(); + } + + abstract void stop(String exceptionClass); + } + + final class TimerSample extends Sample { + + private final Timer.Sample sample; + + public TimerSample(Timed timed, Tags commonTags) { + super(timed, commonTags); + this.sample = Timer.start(meterRegistry); + } + + @Override + void stop(String exceptionClass) { + record(timed, sample, exceptionClass, Tags.concat(commonTags, timed.extraTags())); + } + + } + + final class LongTimerSample extends Sample { + + private final LongTaskTimer.Sample sample; + + public LongTimerSample(Timed timed, Tags commonTags) { + super(timed, commonTags); + this.sample = startLongTaskTimer(timed, commonTags, metricName()); + } + + @Override + void stop(String exceptionClass) { + stopLongTaskTimer(metricName(), sample); + } + + } }