From 203204290ce0aa78a364cb786ec4ce53aa86f268 Mon Sep 17 00:00:00 2001 From: Tim Quinn Date: Thu, 4 Mar 2021 13:14:13 -0600 Subject: [PATCH] Add missing metrics coverage and ensure proper coverage of future metrics in gRPC (#2818) * grpc metrics fix Signed-off-by: tim.quinn@oracle.com --- .../io/helidon/grpc/metrics/GrpcMetrics.java | 92 +++++++- .../metrics/GrpcMetricsInterceptorIT.java | 42 +++- microprofile/grpc/metrics/README.md | 104 +++++++++ microprofile/grpc/metrics/pom.xml | 5 + .../grpc/metrics/GrpcMetricsCdiExtension.java | 37 ++-- .../grpc/metrics/MetricsConfigurer.java | 200 +++++++++++++----- .../metrics/src/main/java/module-info.java | 2 +- .../grpc/metrics/CoverageTestBeanBase.java | 23 ++ .../CoverageTestBeanConcurrentGauge.java | 28 +++ .../grpc/metrics/CoverageTestBeanCounted.java | 28 +++ .../grpc/metrics/CoverageTestBeanMetered.java | 28 +++ .../metrics/CoverageTestBeanSimplyTimed.java | 28 +++ .../grpc/metrics/CoverageTestBeanTimed.java | 28 +++ .../GrpcMetricsCoverageTestCdiExtension.java | 135 ++++++++++++ .../grpc/metrics/TestMetricsCoverage.java | 117 ++++++++++ ...s.TestMetricsCoverage$GeneratedBeanCatalog | 18 ++ .../grpc/metrics/MetricsConfigurerTest.java | 112 +++++++++- 17 files changed, 952 insertions(+), 75 deletions(-) create mode 100644 microprofile/grpc/metrics/README.md create mode 100644 microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanBase.java create mode 100644 microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanConcurrentGauge.java create mode 100644 microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanCounted.java create mode 100644 microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanMetered.java create mode 100644 microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanSimplyTimed.java create mode 100644 microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanTimed.java create mode 100644 microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/GrpcMetricsCoverageTestCdiExtension.java create mode 100644 microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/TestMetricsCoverage.java create mode 100644 microprofile/grpc/metrics/src/test/resources/META-INF/services/io.helidon.microprofile.grpc.metrics.TestMetricsCoverage$GeneratedBeanCatalog diff --git a/grpc/metrics/src/main/java/io/helidon/grpc/metrics/GrpcMetrics.java b/grpc/metrics/src/main/java/io/helidon/grpc/metrics/GrpcMetrics.java index d8da3d52ef3..d3a9bb802d2 100644 --- a/grpc/metrics/src/main/java/io/helidon/grpc/metrics/GrpcMetrics.java +++ b/grpc/metrics/src/main/java/io/helidon/grpc/metrics/GrpcMetrics.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020 Oracle and/or its affiliates. + * Copyright (c) 2019, 2021 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package io.helidon.grpc.metrics; +import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -36,6 +37,7 @@ import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.Status; +import org.eclipse.microprofile.metrics.ConcurrentGauge; import org.eclipse.microprofile.metrics.Counter; import org.eclipse.microprofile.metrics.Histogram; import org.eclipse.microprofile.metrics.MetadataBuilder; @@ -43,6 +45,7 @@ import org.eclipse.microprofile.metrics.MetricRegistry; import org.eclipse.microprofile.metrics.MetricType; import org.eclipse.microprofile.metrics.MetricUnits; +import org.eclipse.microprofile.metrics.SimpleTimer; import org.eclipse.microprofile.metrics.Tag; import org.eclipse.microprofile.metrics.Timer; @@ -223,6 +226,26 @@ public static GrpcMetrics timed() { return new GrpcMetrics(new MetricsRules(MetricType.TIMER)); } + /** + * A static factory method to create a {@link GrpcMetrics} instance + * to time gRPC method calls. + * + * @return a {@link GrpcMetrics} instance to time gRPC method calls + */ + public static GrpcMetrics concurrentGauge() { + return new GrpcMetrics(new MetricsRules(MetricType.CONCURRENT_GAUGE)); + } + + /** + * A static factory method to create a {@link GrpcMetrics} instance + * to time gRPC method calls. + * + * @return a {@link GrpcMetrics} instance to time gRPC method calls + */ + public static GrpcMetrics simplyTimed() { + return new GrpcMetrics(new MetricsRules(MetricType.SIMPLE_TIMER)); + } + @Override public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, @@ -253,6 +276,14 @@ public ServerCall.Listener interceptCall(ServerCall(APP_REGISTRY.timer( rules.metadata(service, methodName), rules.toTags()), call); break; + case SIMPLE_TIMER: + serverCall = new SimplyTimedServerCall<>(APP_REGISTRY.simpleTimer( + rules.metadata(service, methodName), rules.toTags()), call); + break; + case CONCURRENT_GAUGE: + serverCall = new ConcurrentGaugeServerCall<>(APP_REGISTRY.concurrentGauge( + rules.metadata(service, methodName), rules.toTags()), call); + break; case GAUGE: case INVALID: default: @@ -332,6 +363,65 @@ public void close(Status status, Metadata responseHeaders) { } } + /** + * A {@link GrpcMetrics.MeteredServerCall} that captures call times. + * + * @param the call request type + * @param the call response type + */ + private class SimplyTimedServerCall + extends MetricServerCall { + /** + * The method start time. + */ + private final long startNanos; + + /** + * Create a {@link SimplyTimedServerCall}. + * + * @param delegate the call to time + */ + private SimplyTimedServerCall(SimpleTimer simpleTimer, ServerCall delegate) { + super(simpleTimer, delegate); + + this.startNanos = System.nanoTime(); + } + + @Override + public void close(Status status, Metadata responseHeaders) { + super.close(status, responseHeaders); + + long time = System.nanoTime() - startNanos; + getMetric().update(Duration.ofNanos(time)); + } + } + + /** + * A {@link GrpcMetrics.MeteredServerCall} that captures call times. + * + * @param the call request type + * @param the call response type + */ + private class ConcurrentGaugeServerCall + extends MetricServerCall { + + /** + * Create a {@link SimplyTimedServerCall}. + * + * @param delegate the call to time + */ + private ConcurrentGaugeServerCall(ConcurrentGauge concurrentGauge, ServerCall delegate) { + super(concurrentGauge, delegate); + } + + @Override + public void close(Status status, Metadata responseHeaders) { + super.close(status, responseHeaders); + + getMetric().inc(); + } + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/grpc/metrics/src/test/java/io/helidon/grpc/metrics/GrpcMetricsInterceptorIT.java b/grpc/metrics/src/test/java/io/helidon/grpc/metrics/GrpcMetricsInterceptorIT.java index f51bd8096e5..2dfe37a893e 100644 --- a/grpc/metrics/src/test/java/io/helidon/grpc/metrics/GrpcMetricsInterceptorIT.java +++ b/grpc/metrics/src/test/java/io/helidon/grpc/metrics/GrpcMetricsInterceptorIT.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020 Oracle and/or its affiliates. + * Copyright (c) 2019, 2021 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ import java.util.HashMap; import java.util.stream.Collectors; +import org.eclipse.microprofile.metrics.ConcurrentGauge; import org.eclipse.microprofile.metrics.Counter; import org.eclipse.microprofile.metrics.Histogram; import org.eclipse.microprofile.metrics.Meter; @@ -40,6 +41,7 @@ import org.eclipse.microprofile.metrics.MetricID; import org.eclipse.microprofile.metrics.MetricRegistry; import org.eclipse.microprofile.metrics.MetricUnits; +import org.eclipse.microprofile.metrics.SimpleTimer; import org.eclipse.microprofile.metrics.Timer; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -169,6 +171,44 @@ public void shouldUseTimerMetric() throws Exception { assertThat(appTimer.getCount(), is(1L)); } + @Test + public void shouldUseSimpleTimerMetric() throws Exception { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .unary("barSimplyTimed", this::dummyUnary) + .build(); + + MethodDescriptor methodDescriptor = descriptor.method("barSimplyTimed"); + GrpcMetrics metrics = GrpcMetrics.simplyTimed(); + + ServerCall call = call(metrics, methodDescriptor); + + call.close(Status.OK, new Metadata()); + + SimpleTimer appSimpleTimer = appRegistry.simpleTimer("Foo.barSimplyTimed"); + + assertVendorMetrics(); + assertThat(appSimpleTimer.getCount(), is(1L)); + } + + @Test + public void shouldUseConcurrentGaugeMetric() throws Exception { + ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) + .unary("barConcurrentGauge", this::dummyUnary) + .build(); + + MethodDescriptor methodDescriptor = descriptor.method("barConcurrentGauge"); + GrpcMetrics metrics = GrpcMetrics.concurrentGauge(); + + ServerCall call = call(metrics, methodDescriptor); + + call.close(Status.OK, new Metadata()); + + ConcurrentGauge appConcurrentGauge = appRegistry.concurrentGauge("Foo.barConcurrentGauge"); + + assertVendorMetrics(); + assertThat(appConcurrentGauge.getCount(), is(1L)); + } + @Test public void shouldApplyTags() throws Exception { ServiceDescriptor descriptor = ServiceDescriptor.builder(createMockService()) diff --git a/microprofile/grpc/metrics/README.md b/microprofile/grpc/metrics/README.md new file mode 100644 index 00000000000..0c9fb16fd95 --- /dev/null +++ b/microprofile/grpc/metrics/README.md @@ -0,0 +1,104 @@ +# gRPC Metrics + +## Adding support for a new metric type +From time to time, the metrics spec evolves and new metrics appear. +To add support for a new metric to gRPC, follow these steps. + +### Update `GrpcMetrics` +#### Add `xxx` metric factory method +The class contains a very simple method for each metric type like this: +```java +public static GrpcMetrics concurrentGauge() {...} +``` +Just add a new method following the same pattern for the new metric type. + +#### Add `xxxServerCall` method +The class also contains a method per metrics like this: +```java +private class ConcurrentGaugeServerCall + extends MetricServerCall {...} +``` +Add a new method following the same pattern for the new metric type. The new method will need to +update its metric in a way that varies among the different metrics types. + +#### Update the `interceptCall` method +Add a new `case` for the new metric to the `switch` statement. + +### Update `GrpcMetricsInterceptorIT` +Add a test for the new metric: +```java +public void shouldUseConcurrentGaugeMetric() throws Exception {...} +``` +Follow the pattern set by the methods for other metrics, changing the names and using a +metric-specific check to make sure the metric was correctly updated. +### Update `MetricsConfigurer` +Update the map near the top of the file to add the new metric annotation. +The rest of the logic uses the map rather than hard-coded methods. + +### Update `GrpcMetricsCdiExtension` + +Add the new metric and its corresponding annotation to the `METRICS_ANNOTATIONS` `EnumMap` +initialization. + +On the `registerMetrics` method, add the new metrics annotation(s) to the `@WithAnnotation` list of +metrics +annotations. + +### Update `MetricsConfigurerTest` +#### Update `ServiceOne` inner class +Add an empty method for the new metric type. +```java +@Unary +@ConcurrentGauge +public void concurrentGauge(String request, StreamObserver response) {} +``` + +#### Update `ServiceTwo` interface +Add a method for the new metric type: +```java +@Unary +@ConcurrentGauge +void concurrentGauge(String request, StreamObserver response); +``` + +#### Update `ServiceTwoImpl` class +Add a method for the new metric type: +```java +@Override +public void concurrentGauge(String request, StreamObserver response) { +} +``` + +Add an invocation of the new method to the `update` method: +```java + ... + .unary("concurrentGauge", this::concurrentGauge); +``` +#### Add tests +##### Add a test to make sure the metric is updated + +Follow the pattern: +```java +@Test +public void shouldAddConcurrentGaugeMetricFromClassAnnotation() {...} +``` +##### Add a test to make sure the new metric annotation on the interface is ignored + +Follow the pattern: +```java +@Test +public void shouldIgnoreConcurrentGaugeMetricFromInterfaceAnnotation {...} +``` +### Add `CoverageTestBeanXXX` test class +where `XXX` is the simple name of the new metrics annotation (e.g., `Counted`). +Use one of the existing classes as a pattern, removing the existing metrics annotation from the +method and adding the new metrics annotation in its place. + +## Testing +To help make sure all known metrics annotations (from the `microprofile/metrics` artifact) are +handled, a test-time CDI extension finds all the coverage test beans and adds them to CDI. +It also examines those classes during the normal CDI extension life cycle to make sure the +actual gRPC extension processes them as expected. + +Several tests make sure that all the metrics annotations are represented and processed, listing any annotations that were not processed as expected. + diff --git a/microprofile/grpc/metrics/pom.xml b/microprofile/grpc/metrics/pom.xml index 1b442b01de0..58178dcaa9a 100644 --- a/microprofile/grpc/metrics/pom.xml +++ b/microprofile/grpc/metrics/pom.xml @@ -90,6 +90,11 @@ jersey-client test + + io.helidon.microprofile.tests + helidon-microprofile-tests-junit5 + test + diff --git a/microprofile/grpc/metrics/src/main/java/io/helidon/microprofile/grpc/metrics/GrpcMetricsCdiExtension.java b/microprofile/grpc/metrics/src/main/java/io/helidon/microprofile/grpc/metrics/GrpcMetricsCdiExtension.java index 9f89604ff5c..0f6e81085c0 100644 --- a/microprofile/grpc/metrics/src/main/java/io/helidon/microprofile/grpc/metrics/GrpcMetricsCdiExtension.java +++ b/microprofile/grpc/metrics/src/main/java/io/helidon/microprofile/grpc/metrics/GrpcMetricsCdiExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020 Oracle and/or its affiliates. + * Copyright (c) 2019, 2021 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,8 @@ package io.helidon.microprofile.grpc.metrics; import java.lang.annotation.Annotation; -import java.util.Arrays; -import java.util.List; +import java.util.EnumMap; +import java.util.Map; import javax.annotation.Priority; import javax.enterprise.event.Observes; @@ -32,8 +32,11 @@ import io.helidon.microprofile.grpc.core.Grpc; import io.helidon.microprofile.grpc.core.GrpcMethod; +import org.eclipse.microprofile.metrics.MetricType; +import org.eclipse.microprofile.metrics.annotation.ConcurrentGauge; import org.eclipse.microprofile.metrics.annotation.Counted; import org.eclipse.microprofile.metrics.annotation.Metered; +import org.eclipse.microprofile.metrics.annotation.SimplyTimed; import org.eclipse.microprofile.metrics.annotation.Timed; /** @@ -50,11 +53,21 @@ public class GrpcMetricsCdiExtension implements Extension { - /** - * The supported metrics annotations. - */ - private static final List> METRIC_ANNOTATIONS - = Arrays.asList(Counted.class, Timed.class, Metered.class); + static final int OBSERVER_PRIORITY = Interceptor.Priority.APPLICATION; + + static final EnumMap> METRICS_ANNOTATIONS; + + static { + Map> map = Map.of( + MetricType.CONCURRENT_GAUGE, ConcurrentGauge.class, + MetricType.COUNTER, Counted.class, + MetricType.METERED, Metered.class, + MetricType.SIMPLE_TIMER, SimplyTimed.class, + MetricType.TIMER, Timed.class + ); + METRICS_ANNOTATIONS = new EnumMap<>(map); + } + /** * Observer {@link ProcessAnnotatedType} events and process any method @@ -63,11 +76,11 @@ public class GrpcMetricsCdiExtension * @param pat the {@link ProcessAnnotatedType} to observer */ private void registerMetrics(@Observes - @WithAnnotations({Counted.class, Timed.class, Metered.class, Grpc.class}) - @Priority(Interceptor.Priority.APPLICATION) + @WithAnnotations({Counted.class, Timed.class, Metered.class, ConcurrentGauge.class, + SimplyTimed.class, Grpc.class}) + @Priority(OBSERVER_PRIORITY) ProcessAnnotatedType pat) { - - METRIC_ANNOTATIONS.forEach(type -> + METRICS_ANNOTATIONS.values().forEach(type -> pat.configureAnnotatedType() .methods() .stream() diff --git a/microprofile/grpc/metrics/src/main/java/io/helidon/microprofile/grpc/metrics/MetricsConfigurer.java b/microprofile/grpc/metrics/src/main/java/io/helidon/microprofile/grpc/metrics/MetricsConfigurer.java index 9ad98360541..d3ccc82b0d4 100644 --- a/microprofile/grpc/metrics/src/main/java/io/helidon/microprofile/grpc/metrics/MetricsConfigurer.java +++ b/microprofile/grpc/metrics/src/main/java/io/helidon/microprofile/grpc/metrics/MetricsConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020 Oracle and/or its affiliates. + * Copyright (c) 2019, 2021 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,11 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; @@ -32,9 +37,10 @@ import io.helidon.microprofile.metrics.MetricsCdiExtension; import org.eclipse.microprofile.metrics.MetricType; +import org.eclipse.microprofile.metrics.annotation.ConcurrentGauge; import org.eclipse.microprofile.metrics.annotation.Counted; -import org.eclipse.microprofile.metrics.annotation.Gauge; import org.eclipse.microprofile.metrics.annotation.Metered; +import org.eclipse.microprofile.metrics.annotation.SimplyTimed; import org.eclipse.microprofile.metrics.annotation.Timed; import static io.helidon.microprofile.metrics.MetricUtil.getMetricName; @@ -53,45 +59,146 @@ public class MetricsConfigurer private static final Logger LOGGER = Logger.getLogger(MetricsConfigurer.class.getName()); - @Override - public void accept(Class serviceClass, Class annotatedClass, ServiceDescriptor.Builder builder) { + /** + * Captures information and logic for dealing with the various metrics annotations. This allows the metrics handling to be + * largely data-driven rather than requiring separate code for each type of metric. + * + * @param the type of the specific metrics annotation + */ + private static class MetricAnnotationInfo { + private final Class annotationClass; + private final Supplier gRpcMetricsSupplier; + private final Function annotationNameFunction; + private final Function annotationAbsoluteFunction; + private final Function annotationDescriptorFunction; + private final Function annotationDisplayNameFunction; + private final Function annotationReusableFunction; + private final Function annotationUnitsFunction; - AnnotatedMethodList methodList = AnnotatedMethodList.create(serviceClass); - methodList.withAnnotation(Timed.class) - .stream() - .filter(am -> isServiceAnnotated(serviceClass, am, Timed.class)) - .forEach(annotatedMethod -> addTimer(builder, annotatedMethod)); + MetricAnnotationInfo( + Class annotationClass, + Supplier gRpcMetricsSupplier, + Function annotationNameFunction, + Function annotationAbsoluteFunction, + Function annotationDescriptorFunction, + Function annotationDisplayNameFunction, + Function annotationReusableFunction, + Function annotationUnitsFunction) { + this.annotationClass = annotationClass; + this.gRpcMetricsSupplier = gRpcMetricsSupplier; + this.annotationNameFunction = annotationNameFunction; + this.annotationAbsoluteFunction = annotationAbsoluteFunction; + this.annotationDescriptorFunction = annotationDescriptorFunction; + this.annotationDisplayNameFunction = annotationDisplayNameFunction; + this.annotationReusableFunction = annotationReusableFunction; + this.annotationUnitsFunction = annotationUnitsFunction; + } - methodList.withAnnotation(Counted.class) - .stream() - .filter(am -> isServiceAnnotated(serviceClass, am, Counted.class)) - .forEach(annotatedMethod -> addCounter(builder, annotatedMethod)); + A annotationOnMethod(AnnotatedMethod am) { + return am.getAnnotation(annotationClass); + } - methodList.withAnnotation(Metered.class) - .stream() - .filter(am -> isServiceAnnotated(serviceClass, am, Metered.class)) - .forEach(annotatedMethod -> addMeter(builder, annotatedMethod)); - } + String name(AnnotatedMethod am) { + return annotationNameFunction.apply(am.getAnnotation(annotationClass)); + } - private boolean isServiceAnnotated(Class cls, AnnotatedMethod annotatedMethod, Class annotation) { - Method method = annotatedMethod.declaredMethod(); - return method.getDeclaringClass().equals(cls) && method.isAnnotationPresent(annotation); + boolean absolute(AnnotatedMethod am) { + return annotationAbsoluteFunction.apply(am.getAnnotation(annotationClass)); + } + + String displayName(AnnotatedMethod am) { + return annotationDisplayNameFunction.apply(am.getAnnotation(annotationClass)); + } + + String description(AnnotatedMethod am) { + return annotationDescriptorFunction.apply(am.getAnnotation(annotationClass)); + } + + boolean reusable(AnnotatedMethod am) { + return annotationReusableFunction.apply(am.getAnnotation(annotationClass)); + } + + String units(AnnotatedMethod am) { + return annotationUnitsFunction.apply(am.getAnnotation(annotationClass)); + } } - private void addTimer(ServiceDescriptor.Builder builder, AnnotatedMethod annotatedMethod) { - Timed timed = annotatedMethod.getAnnotation(Timed.class); - addMetric(builder, annotatedMethod, GrpcMetrics.timed(), timed, timed.name(), timed.absolute()); + private static final Map, MetricAnnotationInfo> METRIC_ANNOTATION_INFO = Map.of( + Counted.class, new MetricAnnotationInfo( + Counted.class, + GrpcMetrics::counted, + Counted::name, + Counted::absolute, + Counted::description, + Counted::displayName, + Counted::reusable, + Counted::unit), + Metered.class, new MetricAnnotationInfo( + Metered.class, + GrpcMetrics::metered, + Metered::name, + Metered::absolute, + Metered::description, + Metered::displayName, + Metered::reusable, + Metered::unit), + Timed.class, new MetricAnnotationInfo( + Timed.class, + GrpcMetrics::timed, + Timed::name, + Timed::absolute, + Timed::description, + Timed::displayName, + Timed::reusable, + Timed::unit), + ConcurrentGauge.class, new MetricAnnotationInfo( + ConcurrentGauge.class, + GrpcMetrics::concurrentGauge, + ConcurrentGauge::name, + ConcurrentGauge::absolute, + ConcurrentGauge::description, + ConcurrentGauge::displayName, + ConcurrentGauge::reusable, + ConcurrentGauge::unit), + SimplyTimed.class, new MetricAnnotationInfo( + SimplyTimed.class, + GrpcMetrics::simplyTimed, + SimplyTimed::name, + SimplyTimed::absolute, + SimplyTimed::description, + SimplyTimed::displayName, + SimplyTimed::reusable, + SimplyTimed::unit) + ); + + // for testing + static Set> metricsAnnotationsSupported() { + return Collections.unmodifiableSet(METRIC_ANNOTATION_INFO.keySet()); } - private void addCounter(ServiceDescriptor.Builder builder, AnnotatedMethod annotatedMethod) { - Counted counted = annotatedMethod.getAnnotation(Counted.class); - addMetric(builder, annotatedMethod, GrpcMetrics.counted(), counted, counted.name(), counted.absolute()); + @Override + public void accept(Class serviceClass, Class annotatedClass, ServiceDescriptor.Builder builder) { + + AnnotatedMethodList methodList = AnnotatedMethodList.create(serviceClass); + + METRIC_ANNOTATION_INFO.forEach((annotationClass, info) -> methodList.withAnnotation(annotationClass) + .stream() + .filter(am -> isServiceAnnotated(serviceClass, am, annotationClass)) + .forEach(annotatedMethod -> { + Annotation anno = info.annotationOnMethod(annotatedMethod); + addMetric(builder, + annotatedMethod, + info.gRpcMetricsSupplier.get(), + anno, + info.name(annotatedMethod), + info.absolute(annotatedMethod)); + })); } - private void addMeter(ServiceDescriptor.Builder builder, AnnotatedMethod annotatedMethod) { - Metered metered = annotatedMethod.getAnnotation(Metered.class); - addMetric(builder, annotatedMethod, GrpcMetrics.metered(), metered, metered.name(), metered.absolute()); + private boolean isServiceAnnotated(Class cls, AnnotatedMethod annotatedMethod, Class annotation) { + Method method = annotatedMethod.declaredMethod(); + return method.getDeclaringClass().equals(cls) && method.isAnnotationPresent(annotation); } private void addMetric(ServiceDescriptor.Builder builder, @@ -120,33 +227,12 @@ private void addMetric(ServiceDescriptor.Builder builder, MetricUtil.LookupResult lookupResult = MetricUtil.lookupAnnotation(method, annotation.annotationType(), annotatedClass); - if (annotation instanceof Metered) { - Metered metered = (Metered) annotation; - String displayName = metered.displayName().trim(); - interceptor = interceptor.description(metered.description()); - interceptor = interceptor.displayName(displayName.isEmpty() ? metricName : displayName); - interceptor = interceptor.reusable(metered.reusable()); - interceptor = interceptor.units(metered.unit()); - } else if (annotation instanceof Gauge) { - Gauge gauge = (Gauge) annotation; - String displayName = gauge.displayName().trim(); - interceptor = interceptor.description(gauge.description()); - interceptor = interceptor.displayName(displayName.isEmpty() ? metricName : displayName); - interceptor = interceptor.units(gauge.unit()); - } else if (annotation instanceof Timed) { - Timed timed = (Timed) annotation; - String displayName = timed.displayName().trim(); - interceptor = interceptor.description(timed.description()); - interceptor = interceptor.displayName(displayName.isEmpty() ? metricName : displayName); - interceptor = interceptor.reusable(timed.reusable()); - interceptor = interceptor.units(timed.unit()); - } else if (annotation instanceof Counted) { - Counted counted = (Counted) annotation; - String displayName = counted.displayName().trim(); - interceptor = interceptor.description(counted.description()); - interceptor = interceptor.displayName(displayName.isEmpty() ? metricName : displayName); - interceptor = interceptor.reusable(counted.reusable()); - interceptor = interceptor.units(counted.unit()); + MetricAnnotationInfo mInfo = METRIC_ANNOTATION_INFO.get(annotation.annotationType()); + if (mInfo != null && mInfo.annotationClass.isInstance(annotation)) { + interceptor = interceptor.description(mInfo.description(annotatedMethod)) + .displayName(mInfo.displayName(annotatedMethod)) + .reusable(mInfo.reusable(annotatedMethod)) + .units(mInfo.units(annotatedMethod)); } MetricsCdiExtension.registerMetric(method, annotatedClass, lookupResult); diff --git a/microprofile/grpc/metrics/src/main/java/module-info.java b/microprofile/grpc/metrics/src/main/java/module-info.java index fc9cb82ba44..8ed152abd6a 100644 --- a/microprofile/grpc/metrics/src/main/java/module-info.java +++ b/microprofile/grpc/metrics/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020 Oracle and/or its affiliates. + * Copyright (c) 2019, 2021 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanBase.java b/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanBase.java new file mode 100644 index 00000000000..cd7ed98ee07 --- /dev/null +++ b/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanBase.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.microprofile.grpc.metrics; + +import javax.enterprise.context.Dependent; + +@Dependent +public class CoverageTestBeanBase { +} diff --git a/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanConcurrentGauge.java b/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanConcurrentGauge.java new file mode 100644 index 00000000000..8e4593dc6fd --- /dev/null +++ b/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanConcurrentGauge.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.microprofile.grpc.metrics; + +import io.helidon.microprofile.grpc.core.GrpcMethod; +import org.eclipse.microprofile.metrics.annotation.ConcurrentGauge; + +public class CoverageTestBeanConcurrentGauge extends CoverageTestBeanBase { + + @ConcurrentGauge + @GrpcMethod(type = io.grpc.MethodDescriptor.MethodType.UNARY) + public void measuredMethod() { + } +} diff --git a/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanCounted.java b/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanCounted.java new file mode 100644 index 00000000000..f9af475ba12 --- /dev/null +++ b/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanCounted.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.microprofile.grpc.metrics; + +import io.helidon.microprofile.grpc.core.GrpcMethod; +import org.eclipse.microprofile.metrics.annotation.Counted; + +public class CoverageTestBeanCounted extends CoverageTestBeanBase { + + @Counted + @GrpcMethod(type = io.grpc.MethodDescriptor.MethodType.UNARY) + public void measuredMethod() { + } +} diff --git a/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanMetered.java b/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanMetered.java new file mode 100644 index 00000000000..3b85cb096be --- /dev/null +++ b/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanMetered.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.microprofile.grpc.metrics; + +import io.helidon.microprofile.grpc.core.GrpcMethod; +import org.eclipse.microprofile.metrics.annotation.Metered; + +public class CoverageTestBeanMetered extends CoverageTestBeanBase { + + @Metered + @GrpcMethod(type = io.grpc.MethodDescriptor.MethodType.UNARY) + public void measuredMethod() { + } +} diff --git a/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanSimplyTimed.java b/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanSimplyTimed.java new file mode 100644 index 00000000000..f9c94eaea1b --- /dev/null +++ b/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanSimplyTimed.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.microprofile.grpc.metrics; + +import io.helidon.microprofile.grpc.core.GrpcMethod; +import org.eclipse.microprofile.metrics.annotation.SimplyTimed; + +public class CoverageTestBeanSimplyTimed extends CoverageTestBeanBase { + + @SimplyTimed + @GrpcMethod(type = io.grpc.MethodDescriptor.MethodType.UNARY) + public void measuredMethod() { + } +} diff --git a/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanTimed.java b/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanTimed.java new file mode 100644 index 00000000000..1a61e507d68 --- /dev/null +++ b/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/CoverageTestBeanTimed.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.microprofile.grpc.metrics; + +import io.helidon.microprofile.grpc.core.GrpcMethod; +import org.eclipse.microprofile.metrics.annotation.Timed; + +public class CoverageTestBeanTimed extends CoverageTestBeanBase { + + @Timed + @GrpcMethod(type = io.grpc.MethodDescriptor.MethodType.UNARY) + public void measuredMethod() { + } +} diff --git a/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/GrpcMetricsCoverageTestCdiExtension.java b/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/GrpcMetricsCoverageTestCdiExtension.java new file mode 100644 index 00000000000..04f8622a3f5 --- /dev/null +++ b/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/GrpcMetricsCoverageTestCdiExtension.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.microprofile.grpc.metrics; + +import io.helidon.microprofile.grpc.core.GrpcMethod; + +import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import javax.annotation.Priority; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.spi.AnnotatedMethod; +import javax.enterprise.inject.spi.BeforeBeanDiscovery; +import javax.enterprise.inject.spi.Extension; +import javax.enterprise.inject.spi.ProcessAnnotatedType; +import javax.enterprise.inject.spi.ProcessManagedBean; +import javax.enterprise.inject.spi.WithAnnotations; + +public class GrpcMetricsCoverageTestCdiExtension implements Extension { + + private static final Logger LOGGER = Logger.getLogger(GrpcMetricsCoverageTestCdiExtension.class.getName()); + + private final Set> metricsAnnotationsUsed = new HashSet<>(); + + private final Map, Set>> remainingTestBeanMethodAnnotations = + new HashMap<>(); + + private final Set> annotationClassesNotCoveredByTestBeans = new HashSet<>(); + + void before(@Observes BeforeBeanDiscovery bbd) { + + metricsAnnotationsUsed.clear(); + remainingTestBeanMethodAnnotations.clear(); + annotationClassesNotCoveredByTestBeans.clear(); + + String testBeanClassBaseName = CoverageTestBeanBase.class.getName(); + String testBeanClassNamePrefix = testBeanClassBaseName.substring(0, testBeanClassBaseName.indexOf("Base")); + + GrpcMetricsCdiExtension.METRICS_ANNOTATIONS.values().forEach(annotationClass -> { + String testBeanClassName = testBeanClassNamePrefix + annotationClass.getSimpleName(); + try { + Class testBeanClass = Class.forName(testBeanClassName); + bbd.addAnnotatedType(testBeanClass, testBeanClassName); + } catch (ClassNotFoundException e) { + annotationClassesNotCoveredByTestBeans.add(annotationClass); + } + }); + } + + /** + * Collects all the metrics annotations that are used on methods in the {@code CoverageTestBeanXXX} classes. + *

+ * A test retrieves this collection and makes sure that all the metric annotations known to the main Helidon metrics + * module are accounted for on methods on the test beans. That's because another test makes sure that the gRPC + * extension, which removes metrics annotations from methods that also have {@code @GrpcMethod}, correctly removes all + * of those metrics annotations. We want to make sure that all known annotations are covered by the test beans so we + * can check that the gRPC extension is properly handling all of them. + *

+ *

+ * And, because the main gRPC extension removes metrics annotations that way, this observer needs to run before that + * one so we get an accurate collection of metrics annotations actually used in the test bean class. + *

+ * @param pat + */ + void recordMetricsAnnotationsOnTestBean(@Observes @Priority(GrpcMetricsCdiExtension.OBSERVER_PRIORITY - 10) + @WithAnnotations(GrpcMethod.class) ProcessAnnotatedType pat) { + + pat.getAnnotatedType() + .getMethods() + .forEach(m -> { + Set> metricsAnnotationClassesForThisMethod = + m.getAnnotations().stream() + .map(Annotation::annotationType) + .collect(Collectors.toSet()); + + metricsAnnotationClassesForThisMethod.retainAll(GrpcMetricsCdiExtension.METRICS_ANNOTATIONS.values()); + LOGGER.log(Level.FINE, () -> String.format("Recording annotation(s) %s on %s", + metricsAnnotationClassesForThisMethod, m.getJavaMember().toString())); + metricsAnnotationsUsed.addAll(metricsAnnotationClassesForThisMethod); + }); + } + + /** + * Tracks which methods on the test bean still have any metrics annotations, given that the gRPC extension should have + * removed them. + * + * @param pmb the ProcessManagedBean event + */ + void checkForMetricsAnnotations(@Observes ProcessManagedBean pmb) { + + pmb.getAnnotatedBeanClass().getMethods().forEach(m -> { + Set> remainingMetricsAnnotationsForThisMethod = + m.getAnnotations().stream() + .map(Annotation::annotationType) + .collect(Collectors.toSet()); + + remainingMetricsAnnotationsForThisMethod.retainAll(GrpcMetricsCdiExtension.METRICS_ANNOTATIONS.values()); + if (!remainingMetricsAnnotationsForThisMethod.isEmpty()) { + remainingTestBeanMethodAnnotations.put(m, remainingMetricsAnnotationsForThisMethod); + } + }); + } + + Map, Set>> remainingTestBeanMethodAnnotations() { + return remainingTestBeanMethodAnnotations; + } + + Set> metricsAnnotationsUsed() { + return metricsAnnotationsUsed; + } + + Set> annotationClassesNotCoveredByTestBeans() { + return annotationClassesNotCoveredByTestBeans; + } +} diff --git a/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/TestMetricsCoverage.java b/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/TestMetricsCoverage.java new file mode 100644 index 00000000000..5a4f73981d5 --- /dev/null +++ b/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/TestMetricsCoverage.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.microprofile.grpc.metrics; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.enterprise.inject.spi.AnnotatedMethod; +import javax.inject.Inject; + +import io.helidon.microprofile.tests.junit5.AddBean; +import io.helidon.microprofile.tests.junit5.AddExtension; +import io.helidon.microprofile.tests.junit5.HelidonTest; + +import org.eclipse.microprofile.metrics.MetricType; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; + +@HelidonTest +@AddExtension(GrpcMetricsCoverageTestCdiExtension.class) +@AddBean(CoverageTestBeanCounted.class) +public class TestMetricsCoverage { + + @Inject + GrpcMetricsCoverageTestCdiExtension extension; + + @Test + public void checkThatAllMetricsAnnotationsAreCoveredByTestBeans() { + assertThat("Some metrics annotations are not covered by test beans", + extension.annotationClassesNotCoveredByTestBeans(), is(empty())); + } + + @Test + public void checkThatAllMetricsWereRemovedFromGrpcMethods() { + + Map, Set>> leftoverAnnotations = + extension.remainingTestBeanMethodAnnotations(); + + assertThat("Metrics annotations unexpectedly remain on method", leftoverAnnotations.keySet(), is(empty())); + } + + @Test + public void checkThatAllMetricsAnnotationsWereEncountered() { + + Set> metricsAnnotationsUnused = + new HashSet<>(GrpcMetricsCdiExtension.METRICS_ANNOTATIONS.values()); + metricsAnnotationsUnused.removeAll(extension.metricsAnnotationsUsed()); + + assertThat("The CoverageTestBeanBase subclasses seem not to cover all known annotations", metricsAnnotationsUnused, + is(empty())); + } + + /** + * Checks that {@code MetricsConfigurer} deals with all metrics annotations that are known to the main Helidon metrics CDI + * extension. + *

+ * The {@code MetricsConfigurer} class has been changed to be data-driven. The + * {@code metricInfo} map contains all the metric-specific code (mostly as method references) so adding support for a new + * metrics (if more are added) happens on that one table. + *

+ *

+ * This test makes sure that the map created there contains an entry for every type of metric annotation known to + * Helidon + * Microprofile metrics. + *

+ */ + @Test + public void checkForAllMetricsInMetricInfo() { + Set> metricsAnnotations = + new HashSet<>(GrpcMetricsCdiExtension.METRICS_ANNOTATIONS.values()); + + metricsAnnotations.removeAll(MetricsConfigurer.metricsAnnotationsSupported()); + assertThat("One or more metrics annotations seem not supported in MetricsConfigurer but should be", + metricsAnnotations, is(empty())); + } + + /** + * Makes sure that all metrics types that have corresponding annotations we process are present in the type-to-annotation + * EnumMap. + */ + @Test + public void checkForAllMetricTypesMappedToAnnotationType() { + Set ignoredMetricTypes = Set.of(MetricType.INVALID, // ignore this + MetricType.GAUGE, // we don't process gauge annotations + MetricType.HISTOGRAM // there is no histogram annotation for the histogram metric type + ); + + Set incorrectlySkippedMetricTypes = new HashSet<>(Arrays.asList(MetricType.values())); + incorrectlySkippedMetricTypes.removeAll(ignoredMetricTypes); + + incorrectlySkippedMetricTypes.removeAll(GrpcMetricsCdiExtension.METRICS_ANNOTATIONS.keySet()); + assertThat("At least one MicroProfile metric with an annotation exists that is not present in " + + "GrpcMetricsCdiExtension.METRICS_ANNOTATIONS", + incorrectlySkippedMetricTypes, is(empty())); + } +} diff --git a/microprofile/grpc/metrics/src/test/resources/META-INF/services/io.helidon.microprofile.grpc.metrics.TestMetricsCoverage$GeneratedBeanCatalog b/microprofile/grpc/metrics/src/test/resources/META-INF/services/io.helidon.microprofile.grpc.metrics.TestMetricsCoverage$GeneratedBeanCatalog new file mode 100644 index 00000000000..48946a78e5b --- /dev/null +++ b/microprofile/grpc/metrics/src/test/resources/META-INF/services/io.helidon.microprofile.grpc.metrics.TestMetricsCoverage$GeneratedBeanCatalog @@ -0,0 +1,18 @@ +# +# Copyright (c) 2021 Oracle and/or its affiliates. +# +# 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 +# +# http://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. +# + +# The service class is generated. +io.helidon.microprofile.grpc.metrics.CoverageTestBeanCatalog \ No newline at end of file diff --git a/tests/integration/mp-grpc/src/test/java/io/helidon/microprofile/grpc/metrics/MetricsConfigurerTest.java b/tests/integration/mp-grpc/src/test/java/io/helidon/microprofile/grpc/metrics/MetricsConfigurerTest.java index 047d3454760..b4af57e068a 100644 --- a/tests/integration/mp-grpc/src/test/java/io/helidon/microprofile/grpc/metrics/MetricsConfigurerTest.java +++ b/tests/integration/mp-grpc/src/test/java/io/helidon/microprofile/grpc/metrics/MetricsConfigurerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020 Oracle and/or its affiliates. + * Copyright (c) 2019, 2021 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,8 +32,10 @@ import org.eclipse.microprofile.metrics.MetricID; import org.eclipse.microprofile.metrics.MetricRegistry; import org.eclipse.microprofile.metrics.MetricType; +import org.eclipse.microprofile.metrics.annotation.ConcurrentGauge; import org.eclipse.microprofile.metrics.annotation.Counted; import org.eclipse.microprofile.metrics.annotation.Metered; +import org.eclipse.microprofile.metrics.annotation.SimplyTimed; import org.eclipse.microprofile.metrics.annotation.Timed; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.BeforeAll; @@ -115,6 +117,47 @@ public void shouldAddTimerMetricFromClassAnnotation() { assertThat(registry.getTimers().get(new MetricID(ServiceOne.class.getName() + ".timed")), is(notNullValue())); } + @Test + public void shouldAddSimpleTimerMetricFromClassAnnotation() { + Class serviceClass = ServiceOne.class; + Class annotatedClass = ServiceOne.class; + ServiceDescriptor.Builder builder = ServiceDescriptor.builder(new ServiceOne()); + + MetricsConfigurer configurer = new MetricsConfigurer(); + configurer.accept(serviceClass, annotatedClass, builder); + + ServiceDescriptor descriptor = builder.build(); + List serviceInterceptors = descriptor.interceptors().stream().collect(Collectors.toList()); + MethodDescriptor methodDescriptor = descriptor.method("simplyTimed"); + List methodInterceptors = methodDescriptor.interceptors().stream().collect(Collectors.toList()); + assertThat(serviceInterceptors, is(emptyIterable())); + assertThat(methodInterceptors.size(), is(1)); + assertThat(methodInterceptors.get(0), is(instanceOf(GrpcMetrics.class))); + assertThat(((GrpcMetrics) methodInterceptors.get(0)).metricType(), is(MetricType.SIMPLE_TIMER)); + assertThat(registry.getSimpleTimers().get(new MetricID(ServiceOne.class.getName() + ".simplyTimed")), is(notNullValue())); + } + + @Test + public void shouldAddConcurrentGaugeMetricFromClassAnnotation() { + Class serviceClass = ServiceOne.class; + Class annotatedClass = ServiceOne.class; + ServiceDescriptor.Builder builder = ServiceDescriptor.builder(new ServiceOne()); + + MetricsConfigurer configurer = new MetricsConfigurer(); + configurer.accept(serviceClass, annotatedClass, builder); + + ServiceDescriptor descriptor = builder.build(); + List serviceInterceptors = descriptor.interceptors().stream().collect(Collectors.toList()); + MethodDescriptor methodDescriptor = descriptor.method("concurrentGauge"); + List methodInterceptors = methodDescriptor.interceptors().stream().collect(Collectors.toList()); + assertThat(serviceInterceptors, is(emptyIterable())); + assertThat(methodInterceptors.size(), is(1)); + assertThat(methodInterceptors.get(0), is(instanceOf(GrpcMetrics.class))); + assertThat(((GrpcMetrics) methodInterceptors.get(0)).metricType(), is(MetricType.CONCURRENT_GAUGE)); + assertThat(registry.getConcurrentGauges().get(new MetricID(ServiceOne.class.getName() + ".concurrentGauge")), + is(notNullValue())); + } + @Test public void shouldAddOverriddenCounterMetricFromSuperClassAnnotation() { Class serviceClass = ServiceThree.class; @@ -221,6 +264,39 @@ public void shouldIgnoreTimerMetricFromInterfaceAnnotation() { assertThat(methodInterceptors, is(emptyIterable())); } + @Test + public void shouldIgnoreSimpleTimerMetricFromInterfaceAnnotation() { + Class serviceClass = ServiceTwoImpl.class; + Class annotatedClass = ServiceTwo.class; + ServiceDescriptor.Builder builder = ServiceDescriptor.builder(new ServiceTwoImpl()); + + MetricsConfigurer configurer = new MetricsConfigurer(); + configurer.accept(serviceClass, annotatedClass, builder); + + ServiceDescriptor descriptor = builder.build(); + List serviceInterceptors = descriptor.interceptors().stream().collect(Collectors.toList()); + MethodDescriptor methodDescriptor = descriptor.method("simplyTimed"); + List methodInterceptors = methodDescriptor.interceptors().stream().collect(Collectors.toList()); + assertThat(serviceInterceptors, is(emptyIterable())); + assertThat(methodInterceptors, is(emptyIterable())); + } + @Test + public void shouldIgnoreConcurrentGaugeMetricFromInterfaceAnnotation() { + Class serviceClass = ServiceTwoImpl.class; + Class annotatedClass = ServiceTwo.class; + ServiceDescriptor.Builder builder = ServiceDescriptor.builder(new ServiceTwoImpl()); + + MetricsConfigurer configurer = new MetricsConfigurer(); + configurer.accept(serviceClass, annotatedClass, builder); + + ServiceDescriptor descriptor = builder.build(); + List serviceInterceptors = descriptor.interceptors().stream().collect(Collectors.toList()); + MethodDescriptor methodDescriptor = descriptor.method("concurrentGauge"); + List methodInterceptors = methodDescriptor.interceptors().stream().collect(Collectors.toList()); + assertThat(serviceInterceptors, is(emptyIterable())); + assertThat(methodInterceptors, is(emptyIterable())); + } + @Grpc public static class ServiceOne @@ -229,7 +305,9 @@ public static class ServiceOne public void update(ServiceDescriptor.Rules rules) { rules.unary("counted", this::counted) .unary("timed", this::timed) - .unary("metered", this::metered); + .unary("metered", this::metered) + .unary("simplyTimed", this::simplyTimed) + .unary("concurrentGauge", this::concurrentGauge); } @Unary @@ -246,6 +324,16 @@ public void timed(String request, StreamObserver response) { @Metered public void metered(String request, StreamObserver response) { } + + @Unary + @SimplyTimed + public void simplyTimed(String request, StreamObserver response) { + } + + @Unary + @ConcurrentGauge + public void concurrentGauge(String request, StreamObserver response) { + } } @Grpc @@ -263,6 +351,14 @@ public interface ServiceTwo { @Unary @Metered void metered(String request, StreamObserver response); + + @Unary + @SimplyTimed + void simplyTimed(String request, StreamObserver response); + + @Unary + @ConcurrentGauge + void concurrentGauge(String request, StreamObserver response); } public static class ServiceTwoImpl @@ -271,7 +367,9 @@ public static class ServiceTwoImpl public void update(ServiceDescriptor.Rules rules) { rules.unary("counted", this::counted) .unary("timed", this::timed) - .unary("metered", this::metered); + .unary("metered", this::metered) + .unary("simplyTimed", this::simplyTimed) + .unary("concurrentGauge", this::concurrentGauge); } @Override @@ -285,6 +383,14 @@ public void timed(String request, StreamObserver response) { @Override public void metered(String request, StreamObserver response) { } + + @Override + public void simplyTimed(String request, StreamObserver response) { + } + + @Override + public void concurrentGauge(String request, StreamObserver response) { + } } public static class ServiceThree