diff --git a/microprofile/grpc/metrics/pom.xml b/microprofile/grpc/metrics/pom.xml index a3d3878525f..dec7b83636d 100644 --- a/microprofile/grpc/metrics/pom.xml +++ b/microprofile/grpc/metrics/pom.xml @@ -42,6 +42,10 @@ io.helidon.microprofile.grpc helidon-microprofile-grpc-server + + io.helidon.common + helidon-common-service-loader + io.helidon.microprofile.metrics helidon-microprofile-metrics diff --git a/microprofile/grpc/metrics/src/main/java/io/helidon/microprofile/grpc/metrics/GrpcMetricAnnotationDiscoveryObserver.java b/microprofile/grpc/metrics/src/main/java/io/helidon/microprofile/grpc/metrics/GrpcMetricAnnotationDiscoveryObserver.java new file mode 100644 index 00000000000..10da1c76d57 --- /dev/null +++ b/microprofile/grpc/metrics/src/main/java/io/helidon/microprofile/grpc/metrics/GrpcMetricAnnotationDiscoveryObserver.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2022 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.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import io.helidon.common.LazyValue; +import io.helidon.microprofile.grpc.core.AnnotatedMethod; +import io.helidon.microprofile.grpc.core.GrpcMethod; +import io.helidon.microprofile.metrics.MetricAnnotationDiscovery; +import io.helidon.microprofile.metrics.spi.MetricAnnotationDiscoveryObserver; + +import jakarta.enterprise.inject.spi.configurator.AnnotatedMethodConfigurator; + +/** + * The gRPC implementation of {@link io.helidon.microprofile.metrics.spi.MetricAnnotationDiscoveryObserver}. + */ +class GrpcMetricAnnotationDiscoveryObserver implements MetricAnnotationDiscoveryObserver { + + private static final LazyValue INSTANCE = + LazyValue.create(GrpcMetricAnnotationDiscoveryObserver::new); + + private final Map, MetricAnnotationDiscovery.OfMethod>> discoveriesByMethod = + new HashMap<>(); + + + private GrpcMetricAnnotationDiscoveryObserver() { + } + + + static GrpcMetricAnnotationDiscoveryObserver instance() { + return INSTANCE.get(); + } + + @Override + public void onDiscovery(MetricAnnotationDiscovery discovery) { + if (discovery instanceof MetricAnnotationDiscovery.OfMethod methodDiscovery + && isRpcMethod(methodDiscovery.configurator(), discovery.annotation().annotationType())) { + + if (!MetricsConfigurer.isServiceAnnotated(methodDiscovery.annotatedTypeConfigurator().getAnnotated().getJavaClass(), + methodDiscovery.configurator().getAnnotated().getJavaMember(), + discovery.annotation().annotationType())) { + // This endpoint should not give rise to a gRPC-inspired metric. + discovery.deactivate(); + } else { + // This endpoint SHOULD give rise to a gRPC-inspired metric. + discovery.disableDefaultInterceptor(); + discoveriesByMethod.computeIfAbsent(methodDiscovery.configurator().getAnnotated().getJavaMember(), + key -> new HashMap<>()) + .putIfAbsent(discovery.annotation().annotationType(), methodDiscovery); + } + } + } + + static Map, MetricAnnotationDiscovery.OfMethod> discoveries(Method method) { + return INSTANCE.get().discoveriesByMethod.get(method); + } + + static boolean isDiscovered(Method method) { + return INSTANCE.get().discoveriesByMethod.containsKey(method); + } + + /** + * Determine whether a method is annotated with both a metrics annotation + * and an annotation of type {@link io.helidon.microprofile.grpc.core.GrpcMethod}. + * + * @param configurator the {@link jakarta.enterprise.inject.spi.configurator.AnnotatedMethodConfigurator} representing + * the annotated method + * + * @return {@code true} if the method is a timed gRPC method + */ + private static boolean isRpcMethod(AnnotatedMethodConfigurator configurator, Class type) { + AnnotatedMethod method = AnnotatedMethod.create(configurator.getAnnotated().getJavaMember()); + GrpcMethod rpcMethod = method.firstAnnotationOrMetaAnnotation(GrpcMethod.class); + if (rpcMethod != null) { + Annotation annotation = method.firstAnnotationOrMetaAnnotation(type); + return annotation != null; + } + return false; + } + +} diff --git a/microprofile/grpc/metrics/src/main/java/io/helidon/microprofile/grpc/metrics/GrpcMetricRegistrationObserver.java b/microprofile/grpc/metrics/src/main/java/io/helidon/microprofile/grpc/metrics/GrpcMetricRegistrationObserver.java new file mode 100644 index 00000000000..d803a8872ae --- /dev/null +++ b/microprofile/grpc/metrics/src/main/java/io/helidon/microprofile/grpc/metrics/GrpcMetricRegistrationObserver.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2022 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.util.HashMap; +import java.util.Map; + +import io.helidon.common.LazyValue; +import io.helidon.microprofile.metrics.MetricAnnotationDiscovery; +import io.helidon.microprofile.metrics.spi.MetricRegistrationObserver; + +import org.eclipse.microprofile.metrics.Metadata; +import org.eclipse.microprofile.metrics.Metric; +import org.eclipse.microprofile.metrics.MetricID; + +/** + * The gRPC implementation of {@link io.helidon.microprofile.metrics.spi.MetricRegistrationObserver} with a static factory method. + */ +public class GrpcMetricRegistrationObserver implements MetricRegistrationObserver { + + private static final LazyValue INSTANCE + = LazyValue.create(GrpcMetricRegistrationObserver::new); + + private final Map metadataByDiscovery = new HashMap<>(); + + private GrpcMetricRegistrationObserver() { + } + + static GrpcMetricRegistrationObserver instance() { + return INSTANCE.get(); + } + + @Override + public void onRegistration(MetricAnnotationDiscovery discovery, + Metadata metadata, + MetricID metricId, + Metric metric) { + metadataByDiscovery.put(discovery, metadata); + } + + static Metadata metadata(MetricAnnotationDiscovery discovery) { + return INSTANCE.get().metadataByDiscovery.get(discovery); + } +} 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 86e5bc706a7..e70a7d078ae 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, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2022 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,93 +16,24 @@ package io.helidon.microprofile.grpc.metrics; -import java.lang.annotation.Annotation; -import java.util.EnumMap; -import java.util.Map; +import io.helidon.microprofile.metrics.MetricsCdiExtension; -import io.helidon.microprofile.grpc.core.AnnotatedMethod; -import io.helidon.microprofile.grpc.core.Grpc; -import io.helidon.microprofile.grpc.core.GrpcMethod; - -import jakarta.annotation.Priority; import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.BeforeBeanDiscovery; import jakarta.enterprise.inject.spi.Extension; -import jakarta.enterprise.inject.spi.ProcessAnnotatedType; -import jakarta.enterprise.inject.spi.WithAnnotations; -import jakarta.enterprise.inject.spi.configurator.AnnotatedMethodConfigurator; -import jakarta.interceptor.Interceptor; -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; /** * A CDI extension for gRPC metrics. *

- * This extension will process annotated types that are gRPC methods and - * ensure that those methods re properly intercepted with a gRPC metrics - * {@link io.grpc.ServerInterceptor}. - *

- * If a method is discovered that is annotated with both a metrics annotation and a gRPC - * method type annotation they metrics annotation will be effectively removed from the CDI - * bean so that normal Helidon metrics interceptors do not also intercept that method. + * This extension instantiates and enrolls with the metrics CDI extension a metrics annotation discovery observer and a metrics + * registration observer. It records them for later use by the {@code MetricsConfigurer}. */ -public class GrpcMetricsCdiExtension - implements Extension { - - 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 - * annotated with a gRPC method annotation and a metric annotation. - * - * @param pat the {@link ProcessAnnotatedType} to observer - */ - private void registerMetrics(@Observes - @WithAnnotations({Counted.class, Timed.class, Metered.class, ConcurrentGauge.class, - SimplyTimed.class, Grpc.class}) - @Priority(OBSERVER_PRIORITY) - ProcessAnnotatedType pat) { - METRICS_ANNOTATIONS.values().forEach(type -> - pat.configureAnnotatedType() - .methods() - .stream() - .filter(method -> isRpcMethod(method, type)) - .forEach(method -> method.remove(ann -> type.isAssignableFrom(ann.getClass())))); - } +public class GrpcMetricsCdiExtension implements Extension { - /** - * Determine whether a method is annotated with both a metrics annotation - * and an annotation of type {@link io.helidon.microprofile.grpc.core.GrpcMethod}. - * - * @param configurator the {@link AnnotatedMethodConfigurator} representing - * the annotated method - * - * @return {@code true} if the method is a timed gRPC method - */ - private boolean isRpcMethod(AnnotatedMethodConfigurator configurator, Class type) { - AnnotatedMethod method = AnnotatedMethod.create(configurator.getAnnotated().getJavaMember()); - GrpcMethod rpcMethod = method.firstAnnotationOrMetaAnnotation(GrpcMethod.class); - if (rpcMethod != null) { - Annotation annotation = method.firstAnnotationOrMetaAnnotation(type); - return annotation != null; - } - return false; + private void before(@Observes BeforeBeanDiscovery bbd, BeanManager beanManager) { + MetricsCdiExtension metricsCdiExtension = beanManager.getExtension(MetricsCdiExtension.class); + metricsCdiExtension.enroll(GrpcMetricAnnotationDiscoveryObserver.instance()); + metricsCdiExtension.enroll(GrpcMetricRegistrationObserver.instance()); } } 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 cb6dfa9410a..043e03cfcf3 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, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2022 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,10 +18,7 @@ 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; @@ -33,18 +30,20 @@ import io.helidon.microprofile.grpc.core.GrpcMethod; import io.helidon.microprofile.grpc.server.AnnotatedServiceConfigurer; import io.helidon.microprofile.grpc.server.GrpcServiceBuilder; -import io.helidon.microprofile.metrics.MetricUtil; -import io.helidon.microprofile.metrics.MetricsCdiExtension; +import org.eclipse.microprofile.metrics.Counter; +import org.eclipse.microprofile.metrics.Metadata; +import org.eclipse.microprofile.metrics.Meter; +import org.eclipse.microprofile.metrics.Metric; import org.eclipse.microprofile.metrics.MetricType; +import org.eclipse.microprofile.metrics.SimpleTimer; +import org.eclipse.microprofile.metrics.Timer; 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 static io.helidon.microprofile.metrics.MetricUtil.getMetricName; - /** * A {@link AnnotatedServiceConfigurer} that adds a * {@link io.helidon.grpc.metrics.GrpcMetrics gRPC metrics interceptor} @@ -59,187 +58,95 @@ public class MetricsConfigurer private static final Logger LOGGER = Logger.getLogger(MetricsConfigurer.class.getName()); - /** - * 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 annotationUnitsFunction; - - - MetricAnnotationInfo( - Class annotationClass, - Supplier gRpcMetricsSupplier, - Function annotationNameFunction, - Function annotationAbsoluteFunction, - Function annotationDescriptorFunction, - Function annotationDisplayNameFunction, - Function annotationUnitsFunction) { - this.annotationClass = annotationClass; - this.gRpcMetricsSupplier = gRpcMetricsSupplier; - this.annotationNameFunction = annotationNameFunction; - this.annotationAbsoluteFunction = annotationAbsoluteFunction; - this.annotationDescriptorFunction = annotationDescriptorFunction; - this.annotationDisplayNameFunction = annotationDisplayNameFunction; - this.annotationUnitsFunction = annotationUnitsFunction; - } - - A annotationOnMethod(AnnotatedMethod am) { - return am.getAnnotation(annotationClass); - } - - String name(AnnotatedMethod am) { - return annotationNameFunction.apply(am.getAnnotation(annotationClass)); - } - - 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)); - } - - String units(AnnotatedMethod am) { - return annotationUnitsFunction.apply(am.getAnnotation(annotationClass)); - } - } - - 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::unit), - Metered.class, new MetricAnnotationInfo( - Metered.class, - GrpcMetrics::metered, - Metered::name, - Metered::absolute, - Metered::description, - Metered::displayName, - Metered::unit), - Timed.class, new MetricAnnotationInfo( - Timed.class, - GrpcMetrics::timed, - Timed::name, - Timed::absolute, - Timed::description, - Timed::displayName, - Timed::unit), - ConcurrentGauge.class, new MetricAnnotationInfo( - ConcurrentGauge.class, - GrpcMetrics::concurrentGauge, - ConcurrentGauge::name, - ConcurrentGauge::absolute, - ConcurrentGauge::description, - ConcurrentGauge::displayName, - ConcurrentGauge::unit), - SimplyTimed.class, new MetricAnnotationInfo( - SimplyTimed.class, - GrpcMetrics::simplyTimed, - SimplyTimed::name, - SimplyTimed::absolute, - SimplyTimed::description, - SimplyTimed::displayName, - SimplyTimed::unit) + static final Map, MetricInfo> METRIC_ANNOTATION_INFO = Map.of( + Counted.class, MetricInfo.create(GrpcMetrics::counted, Counter.class), + Metered.class, MetricInfo.create(GrpcMetrics::metered, Meter.class), + Timed.class, MetricInfo.create(GrpcMetrics::timed, Timer.class), + ConcurrentGauge.class, MetricInfo.create(GrpcMetrics::concurrentGauge, + org.eclipse.microprofile.metrics.ConcurrentGauge.class), + SimplyTimed.class, MetricInfo.create(GrpcMetrics::simplyTimed, SimpleTimer.class) ); - // for testing - static Set> metricsAnnotationsSupported() { - return Collections.unmodifiableSet(METRIC_ANNOTATION_INFO.keySet()); - } - @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)); - })); + // For each annotated method: + // Retrieve all discoveries of metric annotations on that method, and for each: + // Get the metric registration for it (which has the metric metadata). + // Add the interceptor for the annotation associated with the annotation site that was discovered. + methodList.stream() + .filter(am -> GrpcMetricAnnotationDiscoveryObserver.isDiscovered(am.method())) + .forEach(annotatedMethod -> + GrpcMetricAnnotationDiscoveryObserver.discoveries(annotatedMethod.method()) + .forEach((annotationClass, discovery) -> { + if (isServiceAnnotated(serviceClass, + annotatedMethod.declaredMethod(), + annotationClass)) { + processMetricAnnotationSite( + builder, + annotatedMethod, + METRIC_ANNOTATION_INFO + .get(annotationClass) + .grpcMetricsSupplier + .get(), + discovery.annotation(), + GrpcMetricRegistrationObserver.metadata(discovery) + ); + } + }) + ); } - private boolean isServiceAnnotated(Class cls, AnnotatedMethod annotatedMethod, Class annotation) { - Method method = annotatedMethod.declaredMethod(); + static boolean isServiceAnnotated(Class cls, + Method method, + Class annotation) { return method.getDeclaringClass().equals(cls) && method.isAnnotationPresent(annotation); } - private void addMetric(ServiceDescriptor.Builder builder, - AnnotatedMethod annotatedMethod, - GrpcMetrics interceptor, - Annotation annotation, - String name, - boolean absolute) { + record MetricInfo(Supplier grpcMetricsSupplier, + Class metricClass) { + private static MetricInfo create(Supplier grpcMetricsSupplier, Class metricClass) { + return new MetricInfo(grpcMetricsSupplier, metricClass); + } + } + + + + // Package-private for testing. + + private void processMetricAnnotationSite(ServiceDescriptor.Builder builder, + AnnotatedMethod annotatedMethod, + GrpcMetrics interceptor, + Annotation annotation, + Metadata metadata) { GrpcMethod rpcMethod = annotatedMethod.firstAnnotationOrMetaAnnotation(GrpcMethod.class); if (rpcMethod != null) { - Method method = findAnnotatedMethod(annotatedMethod, annotation.annotationType()); - Class annotatedClass = method.getDeclaringClass(); String grpcMethodName = GrpcServiceBuilder.determineMethodName(annotatedMethod, rpcMethod); - String metricName = getMetricName(method, - annotatedClass, - MetricUtil.MatchingType.METHOD, - name, - absolute); LOGGER.log(Level.FINE, () -> String.format("Adding gRPC '%s' metric interceptor to service '%s' method '%s'", annotation.annotationType().getSimpleName(), builder.name(), grpcMethodName)); - MetricUtil.LookupResult lookupResult - = MetricUtil.lookupAnnotation(method, annotation.annotationType(), annotatedClass); - - MetricAnnotationInfo mInfo = METRIC_ANNOTATION_INFO.get(annotation.annotationType()); - if (mInfo != null && mInfo.annotationClass.isInstance(annotation)) { - String candidateDescription = mInfo.description(annotatedMethod); + if (METRIC_ANNOTATION_INFO.containsKey(annotation.annotationType()) + && annotation.annotationType().isInstance(annotation)) { + String candidateDescription = metadata.getDescription(); if (candidateDescription != null && !candidateDescription.trim().isEmpty()) { interceptor = interceptor.description(candidateDescription.trim()); } - String candidateDisplayName = mInfo.displayName(annotatedMethod); + String candidateDisplayName = metadata.getDisplayName(); if (candidateDisplayName != null && !candidateDisplayName.trim().isEmpty()) { interceptor = interceptor.displayName(candidateDisplayName.trim()); } interceptor = interceptor - .units(mInfo.units(annotatedMethod)); + .units(metadata.getUnit()); } - MetricsCdiExtension.registerMetric(method, annotatedClass, lookupResult); - builder.intercept(grpcMethodName, interceptor.nameFunction(new ConstantNamingFunction(metricName))); - } - } - - private Method findAnnotatedMethod(AnnotatedMethod annotatedMethod, Class type) { - Method method = annotatedMethod.declaredMethod(); - if (!method.isAnnotationPresent(type)) { - method = annotatedMethod.method(); + builder.intercept(grpcMethodName, interceptor.nameFunction(new ConstantNamingFunction(metadata.getName()))); } - return method; } /** diff --git a/microprofile/grpc/metrics/src/main/java/module-info.java b/microprofile/grpc/metrics/src/main/java/module-info.java index 4de0abf97f4..5ff244bd6c9 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, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2022 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/GrpcMetricsCoverageTestCdiExtension.java b/microprofile/grpc/metrics/src/test/java/io/helidon/microprofile/grpc/metrics/GrpcMetricsCoverageTestCdiExtension.java index 973801b3c77..91b68ef9224 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2022 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. @@ -34,9 +34,12 @@ import jakarta.enterprise.inject.spi.ProcessAnnotatedType; import jakarta.enterprise.inject.spi.ProcessManagedBean; import jakarta.enterprise.inject.spi.WithAnnotations; +import jakarta.interceptor.Interceptor; public class GrpcMetricsCoverageTestCdiExtension implements Extension { + private static final int OBSERVER_PRIORITY = Interceptor.Priority.APPLICATION; + private static final Logger LOGGER = Logger.getLogger(GrpcMetricsCoverageTestCdiExtension.class.getName()); private final Set> metricsAnnotationsUsed = new HashSet<>(); @@ -55,7 +58,7 @@ void before(@Observes BeforeBeanDiscovery bbd) { String testBeanClassBaseName = CoverageTestBeanBase.class.getName(); String testBeanClassNamePrefix = testBeanClassBaseName.substring(0, testBeanClassBaseName.indexOf("Base")); - GrpcMetricsCdiExtension.METRICS_ANNOTATIONS.values().forEach(annotationClass -> { + MetricsConfigurer.METRIC_ANNOTATION_INFO.forEach((annotationClass, metricInfo) -> { String testBeanClassName = testBeanClassNamePrefix + annotationClass.getSimpleName(); try { Class testBeanClass = Class.forName(testBeanClassName); @@ -81,7 +84,7 @@ void before(@Observes BeforeBeanDiscovery bbd) { *

* @param pat */ - void recordMetricsAnnotationsOnTestBean(@Observes @Priority(GrpcMetricsCdiExtension.OBSERVER_PRIORITY - 10) + void recordMetricsAnnotationsOnTestBean(@Observes @Priority(OBSERVER_PRIORITY - 10) @WithAnnotations(GrpcMethod.class) ProcessAnnotatedType pat) { pat.getAnnotatedType() @@ -92,7 +95,7 @@ void recordMetricsAnnotationsOnTestBean(@Observes @Priority(GrpcMetricsCdiExtens .map(Annotation::annotationType) .collect(Collectors.toSet()); - metricsAnnotationClassesForThisMethod.retainAll(GrpcMetricsCdiExtension.METRICS_ANNOTATIONS.values()); + metricsAnnotationClassesForThisMethod.retainAll(MetricsConfigurer.METRIC_ANNOTATION_INFO.keySet()); LOGGER.log(Level.FINE, () -> String.format("Recording annotation(s) %s on %s", metricsAnnotationClassesForThisMethod, m.getJavaMember().toString())); metricsAnnotationsUsed.addAll(metricsAnnotationClassesForThisMethod); @@ -113,7 +116,7 @@ void checkForMetricsAnnotations(@Observes ProcessManagedBean, 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()); + new HashSet<>(MetricsConfigurer.METRIC_ANNOTATION_INFO.keySet()); metricsAnnotationsUnused.removeAll(extension.metricsAnnotationsUsed()); - assertThat("The CoverageTestBeanBase subclasses seem not to cover all known annotations", metricsAnnotationsUnused, + assertThat("Known annotations not covered by CoverageTestBeanBase subclasses", metricsAnnotationsUnused, is(empty())); } @@ -84,12 +77,18 @@ public void checkThatAllMetricsAnnotationsWereEncountered() { */ @Test public void checkForAllMetricsInMetricInfo() { - Set> metricsAnnotations = - new HashSet<>(GrpcMetricsCdiExtension.METRICS_ANNOTATIONS.values()); + var metricTypesSupportedByGrpc = new HashSet<>(List.of(MetricType.values())); + metricTypesSupportedByGrpc.removeAll(Set.of(MetricType.GAUGE, MetricType.HISTOGRAM, MetricType.INVALID)); + + var metricTypesAbsentFromMetricsConfigurer = new HashSet<>(metricTypesSupportedByGrpc); + + // Remove metrics types represented in the MetricsConfigurer's annotation info. + MetricsConfigurer.METRIC_ANNOTATION_INFO.forEach((annotationClass, metricInfo) -> { + metricTypesAbsentFromMetricsConfigurer.remove(MetricType.from(metricInfo.metricClass())); + }); - metricsAnnotations.removeAll(MetricsConfigurer.metricsAnnotationsSupported()); - assertThat("One or more metrics annotations seem not supported in MetricsConfigurer but should be", - metricsAnnotations, is(empty())); + assertThat("Metrics types not supported in MetricsConfigurer but should be", + metricTypesAbsentFromMetricsConfigurer, is(empty())); } /** @@ -106,9 +105,14 @@ public void checkForAllMetricTypesMappedToAnnotationType() { Set incorrectlySkippedMetricTypes = new HashSet<>(Arrays.asList(MetricType.values())); incorrectlySkippedMetricTypes.removeAll(ignoredMetricTypes); - incorrectlySkippedMetricTypes.removeAll(GrpcMetricsCdiExtension.METRICS_ANNOTATIONS.keySet()); + MetricsConfigurer.METRIC_ANNOTATION_INFO.values() + .stream() + .map(MetricsConfigurer.MetricInfo::metricClass) + .map(MetricType::from) + .toList() + .forEach(incorrectlySkippedMetricTypes::remove); assertThat("At least one MicroProfile metric with an annotation exists that is not present in " - + "GrpcMetricsCdiExtension.METRICS_ANNOTATIONS", + + "MetricsConfigurer.METRIC_ANNOTATION_INFO", incorrectlySkippedMetricTypes, is(empty())); } } diff --git a/microprofile/metrics/pom.xml b/microprofile/metrics/pom.xml index e21e8225ba8..797922584ff 100644 --- a/microprofile/metrics/pom.xml +++ b/microprofile/metrics/pom.xml @@ -69,6 +69,10 @@ io.helidon.metrics helidon-metrics-service-api
+ + io.helidon.common + helidon-common-service-loader + io.helidon.metrics helidon-metrics diff --git a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorConcurrentGauge.java b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorConcurrentGauge.java index d905e826dea..4489fe4ad04 100644 --- a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorConcurrentGauge.java +++ b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorConcurrentGauge.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2022 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,23 +16,33 @@ package io.helidon.microprofile.metrics; +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 jakarta.annotation.Priority; +import jakarta.enterprise.util.AnnotationLiteral; import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InterceptorBinding; import org.eclipse.microprofile.metrics.ConcurrentGauge; /** * Interceptor for {@link ConcurrentGauge} annotation. */ -@org.eclipse.microprofile.metrics.annotation.ConcurrentGauge +@InterceptorConcurrentGauge.Binding @Interceptor @Priority(Interceptor.Priority.PLATFORM_BEFORE + 11) final class InterceptorConcurrentGauge extends MetricsInterceptorBase.WithPostCompletion { - - InterceptorConcurrentGauge() { super(org.eclipse.microprofile.metrics.annotation.ConcurrentGauge.class, ConcurrentGauge.class); } + static Binding.Literal binding() { + return Binding.Literal.instance(); + } + @Override void preInvoke(ConcurrentGauge metric) { metric.inc(); @@ -42,4 +52,24 @@ void preInvoke(ConcurrentGauge metric) { void postComplete(ConcurrentGauge metric) { metric.dec(); } + @Inherited + @InterceptorBinding + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @org.eclipse.microprofile.metrics.annotation.ConcurrentGauge + @interface Binding { + class Literal extends AnnotationLiteral implements Binding { + + private static final long serialVersionUID = 1L; + + private static final Literal INSTANCE = new Literal(); + + static Literal instance() { + return INSTANCE; + } + + private Literal() { + } + } + } } diff --git a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorCounted.java b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorCounted.java index 19beed87f84..e000d8c4e50 100644 --- a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorCounted.java +++ b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorCounted.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2022 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,15 +16,23 @@ package io.helidon.microprofile.metrics; +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 jakarta.annotation.Priority; +import jakarta.enterprise.util.AnnotationLiteral; import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InterceptorBinding; import org.eclipse.microprofile.metrics.Counter; import org.eclipse.microprofile.metrics.annotation.Counted; /** * Interceptor for {@link Counted} annotation. */ -@Counted +@InterceptorCounted.Binding @Interceptor @Priority(Interceptor.Priority.PLATFORM_BEFORE + 8) final class InterceptorCounted extends MetricsInterceptorBase { @@ -33,8 +41,32 @@ final class InterceptorCounted extends MetricsInterceptorBase { super(Counted.class, Counter.class); } + static Binding.Literal binding() { + return Binding.Literal.instance(); + } + @Override void preInvoke(Counter metric) { metric.inc(); } + @Inherited + @InterceptorBinding + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Counted + @interface Binding { + class Literal extends AnnotationLiteral implements Binding { + + private static final long serialVersionUID = 1L; + + private static final Literal INSTANCE = new Literal(); + + static Literal instance() { + return INSTANCE; + } + + private Literal() { + } + } + } } diff --git a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorMetered.java b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorMetered.java index f564ea847ee..e60c7b4e485 100644 --- a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorMetered.java +++ b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorMetered.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2022 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,15 +16,23 @@ package io.helidon.microprofile.metrics; +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 jakarta.annotation.Priority; +import jakarta.enterprise.util.AnnotationLiteral; import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InterceptorBinding; import org.eclipse.microprofile.metrics.Meter; import org.eclipse.microprofile.metrics.annotation.Metered; /** * Interceptor for {@link Metered} annotation. */ -@Metered +@InterceptorMetered.Binding @Interceptor @Priority(Interceptor.Priority.PLATFORM_BEFORE + 9) final class InterceptorMetered extends MetricsInterceptorBase { @@ -33,9 +41,32 @@ final class InterceptorMetered extends MetricsInterceptorBase { super(Metered.class, Meter.class); } + static Binding.Literal binding() { + return Binding.Literal.instance(); + } @Override void preInvoke(Meter metric) { metric.mark(); } + @Inherited + @InterceptorBinding + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Metered + @interface Binding { + class Literal extends AnnotationLiteral implements Binding { + + private static final long serialVersionUID = 1L; + + private static final Literal INSTANCE = new Literal(); + + static Literal instance() { + return INSTANCE; + } + + private Literal() { + } + } + } } diff --git a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorSimplyTimed.java b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorSimplyTimed.java index 23744da4b3f..628fd2ddf93 100644 --- a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorSimplyTimed.java +++ b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorSimplyTimed.java @@ -16,9 +16,18 @@ package io.helidon.microprofile.metrics; +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 jakarta.annotation.Priority; +import jakarta.enterprise.util.AnnotationLiteral; import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InterceptorBinding; import org.eclipse.microprofile.metrics.SimpleTimer; +import org.eclipse.microprofile.metrics.annotation.Metered; import org.eclipse.microprofile.metrics.annotation.SimplyTimed; /** @@ -29,7 +38,7 @@ * method, and the separate {@link InterceptorSyntheticRestRequest} interceptor deals with those. *

*/ -@SimplyTimed +@InterceptorSimplyTimed.Binding @Interceptor @Priority(Interceptor.Priority.PLATFORM_BEFORE + 10) final class InterceptorSimplyTimed extends InterceptorTimedBase { @@ -38,9 +47,33 @@ final class InterceptorSimplyTimed extends InterceptorTimedBase { super(SimplyTimed.class, SimpleTimer.class); } + static Binding.Literal binding() { + return Binding.Literal.instance(); + } + @Override void postComplete(SimpleTimer metric) { metric.update(duration()); } + @Inherited + @InterceptorBinding + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Metered + @interface Binding { + class Literal extends AnnotationLiteral implements Binding { + + private static final long serialVersionUID = 1L; + + private static final Literal INSTANCE = new Literal(); + + static Literal instance() { + return INSTANCE; + } + + private Literal() { + } + } + } } diff --git a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorTimed.java b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorTimed.java index 9906e12a883..9fc14667c9b 100644 --- a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorTimed.java +++ b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorTimed.java @@ -16,17 +16,24 @@ package io.helidon.microprofile.metrics; +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 java.time.Duration; import jakarta.annotation.Priority; +import jakarta.enterprise.util.AnnotationLiteral; import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InterceptorBinding; import org.eclipse.microprofile.metrics.Timer; import org.eclipse.microprofile.metrics.annotation.Timed; /** * Interceptor for {@link Timed} annotation. */ -@Timed +@InterceptorTimed.Binding @Interceptor @Priority(Interceptor.Priority.PLATFORM_BEFORE + 10) final class InterceptorTimed extends InterceptorTimedBase { @@ -35,8 +42,32 @@ final class InterceptorTimed extends InterceptorTimedBase { super(Timed.class, Timer.class); } + static Binding.Literal binding() { + return Binding.Literal.instance(); + } + @Override void postComplete(Timer metric) { metric.update(Duration.ofNanos(durationNanoseconds())); } + @Inherited + @InterceptorBinding + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Timed + @interface Binding { + class Literal extends AnnotationLiteral implements Binding { + + private static final long serialVersionUID = 1L; + + private static final Literal INSTANCE = new Literal(); + + static Literal instance() { + return INSTANCE; + } + + private Literal() { + } + } + } } diff --git a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricAnnotationDiscovery.java b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricAnnotationDiscovery.java new file mode 100644 index 00000000000..918f130d193 --- /dev/null +++ b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricAnnotationDiscovery.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022 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.metrics; + +import java.lang.annotation.Annotation; + +import jakarta.enterprise.inject.spi.configurator.AnnotatedConstructorConfigurator; +import jakarta.enterprise.inject.spi.configurator.AnnotatedMethodConfigurator; +import jakarta.enterprise.inject.spi.configurator.AnnotatedTypeConfigurator; + +/** + * Conveys information about the discovery of a metric annotation as it applies to an executable. + *

+ * The discovery event describes the executable to which the metric annotation applies. + * This is not necessarily where the annotation appears, because a metric annotation which appears on the + * type applies to all methods and constructors on that type. + * In that case, the discovery event describes the discovery of the metric as applied + * to the method or constructor, not to the type itself. + * Further, a metric annotation declared at the type level triggers a separate discovery event for each constructor + * and method on the type. + *

+ */ +public interface MetricAnnotationDiscovery { + + /** + * Returns the configurator for the annotated type containing the site to which the metric annotation applies. + * + * @return the configurator for the annotated type + */ + AnnotatedTypeConfigurator annotatedTypeConfigurator(); + + /** + * Returns the {@link java.lang.annotation.Annotation} object for the metric annotation discovered. + * + * @return the annotation object for the metrics annotation + */ + Annotation annotation(); + + /** + * Requests that the discovery be deactivated, thereby preventing it from triggering a metric registration. + */ + void deactivate(); + + /** + * Requests that the default metrics interceptor not be used for the metric corresponding to the indicated annotation + * which appears on this method. + */ + void disableDefaultInterceptor(); + + /** + * + * @return if the discovery is active (i.e., has not been deactivated) + */ + boolean isActive(); + + /** + * Discovery of an annotation of interest on a constructor. + */ + interface OfConstructor extends MetricAnnotationDiscovery { + + /** + * @return the configurator for the constructor on which an annotation of interest appears + */ + AnnotatedConstructorConfigurator configurator(); + } + + /** + * Discovery of an annotation of interest on a method. + */ + interface OfMethod extends MetricAnnotationDiscovery { + + /** + * @return the configurotor for the method on which an annotation of interest appears + */ + AnnotatedMethodConfigurator configurator(); + } +} diff --git a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricAnnotationDiscoveryBase.java b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricAnnotationDiscoveryBase.java new file mode 100644 index 00000000000..3c1c6ba0855 --- /dev/null +++ b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricAnnotationDiscoveryBase.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2022 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.metrics; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Member; +import java.util.StringJoiner; + +import jakarta.enterprise.inject.spi.configurator.AnnotatedConstructorConfigurator; +import jakarta.enterprise.inject.spi.configurator.AnnotatedMethodConfigurator; +import jakarta.enterprise.inject.spi.configurator.AnnotatedTypeConfigurator; + +/** + * Implementation of metrics annotation discovery event. + *

+ * The {@link jakarta.enterprise.inject.spi.configurator.AnnotatedConstructorConfigurator} and + * {@link jakarta.enterprise.inject.spi.configurator.AnnotatedMethodConfigurator} interfaces share no common ancestor, so + * we have two subtypes of discovery, one for each: + * {@link MetricAnnotationDiscoveryBase.OfConstructor OfConstructor} and + * {@link MetricAnnotationDiscovery.OfMethod ofMethod}. + *

+ * + */ +abstract class MetricAnnotationDiscoveryBase implements MetricAnnotationDiscovery { + + private final AnnotatedTypeConfigurator annotatedTypeConfigurator; + private final Annotation annotation; + + private boolean isActive = true; + private boolean useDefaultInterceptor = true; + + private MetricAnnotationDiscoveryBase(AnnotatedTypeConfigurator annotatedTypeConfigurator, + Annotation annotation) { + this.annotatedTypeConfigurator = annotatedTypeConfigurator; + this.annotation = annotation; + } + + static MetricAnnotationDiscoveryBase create( + AnnotatedTypeConfigurator annotatedTypeConfigurator, + C annotatedCallableConfigurator, + Annotation annotation) { + if (annotatedCallableConfigurator instanceof AnnotatedConstructorConfigurator) { + return new OfConstructor(annotatedTypeConfigurator, + (AnnotatedConstructorConfigurator) annotatedCallableConfigurator, + annotation); + } else if (annotatedCallableConfigurator instanceof AnnotatedMethodConfigurator) { + return new OfMethod(annotatedTypeConfigurator, + (AnnotatedMethodConfigurator) annotatedCallableConfigurator, + annotation); + } else { + throw new IllegalArgumentException(String.format("annotatedCallableConfigurator must be of type %s or %s", + AnnotatedConstructorConfigurator.class.getName(), + AnnotatedMethodConfigurator.class.getName())); + } + } + + @Override + public AnnotatedTypeConfigurator annotatedTypeConfigurator() { + return annotatedTypeConfigurator; + } + + @Override + public Annotation annotation() { + return annotation; + } + + @Override + public void deactivate() { + isActive = false; + disableDefaultInterceptor(); + } + + @Override + public void disableDefaultInterceptor() { + useDefaultInterceptor = false; + } + + @Override + public String toString() { + return new StringJoiner(", ", getClass().getSimpleName() + "[", "]") + .add("annotatedConfigurator=" + annotatedMember()) + .add("annotatedTypeConfigurator=" + annotatedTypeConfigurator.getAnnotated().getJavaClass().getName()) + .add("annotation=" + annotation) + .add("isActive=" + isActive) + .add("useDefaultInterceptor=" + useDefaultInterceptor) + .toString(); + } + + @Override + public boolean isActive() { + return isActive; + } + + boolean shouldUseDefaultInterceptor() { + return useDefaultInterceptor; + } + + protected abstract Member annotatedMember(); + + private static class OfConstructor extends MetricAnnotationDiscoveryBase + implements MetricAnnotationDiscovery.OfConstructor { + + private final AnnotatedConstructorConfigurator configurator; + + private OfConstructor( + AnnotatedTypeConfigurator annotatedTypeConfigurator, + AnnotatedConstructorConfigurator annotatedConstructorConfigurator, + Annotation annotation) { + super(annotatedTypeConfigurator, annotation); + configurator = annotatedConstructorConfigurator; + } + + @Override + public AnnotatedConstructorConfigurator configurator() { + return configurator; + } + + @Override + protected Member annotatedMember() { + return configurator.getAnnotated().getJavaMember(); + } + } + + private static class OfMethod extends MetricAnnotationDiscoveryBase + implements MetricAnnotationDiscovery.OfMethod { + + private final AnnotatedMethodConfigurator configurator; + + private OfMethod( + AnnotatedTypeConfigurator annotatedTypeConfigurator, + AnnotatedMethodConfigurator annotatedMethodConfigurator, + Annotation annotation) { + super(annotatedTypeConfigurator, annotation); + configurator = annotatedMethodConfigurator; + } + + @Override + public AnnotatedMethodConfigurator configurator() { + return configurator; + } + + @Override + protected Member annotatedMember() { + return configurator.getAnnotated().getJavaMember(); + } + } +} diff --git a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricAnnotationInfo.java b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricAnnotationInfo.java index 70b5ae433a5..c6edce347e4 100644 --- a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricAnnotationInfo.java +++ b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricAnnotationInfo.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2022 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. @@ -123,6 +123,10 @@ Class annotationType() { return annotationType; } + Metadata metadata() { + return metadata; + } + Metric register(MetricRegistry registry) { return registration.register(registry, metadata, tags); } diff --git a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricUtil.java b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricUtil.java index 126faeb3137..9b46662ace3 100644 --- a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricUtil.java +++ b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2022 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,77 +32,19 @@ import jakarta.enterprise.inject.spi.Annotated; import jakarta.enterprise.inject.spi.AnnotatedMember; -import jakarta.enterprise.inject.spi.AnnotatedMethod; import jakarta.enterprise.inject.spi.AnnotatedType; -import org.eclipse.microprofile.metrics.Metadata; import org.eclipse.microprofile.metrics.MetricRegistry; -import org.eclipse.microprofile.metrics.MetricType; import org.eclipse.microprofile.metrics.Tag; -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; /** * Class MetricUtil. */ -public final class MetricUtil { +final class MetricUtil { private static final Logger LOGGER = Logger.getLogger(MetricUtil.class.getName()); private MetricUtil() { } - /** - * DO NOT USE THIS METHOD please, it will be removed. - *

- * Instead, see {@link MatchingType}. - *

- * - * @param element element - * @param annotClass annotation class - * @param clazz class - * @param element type - * @param
annotation type - * @return lookup result - * @deprecated This method is made public to migrate from metrics1 to metrics2 for gRPC, this should be refactored. - * This method will be removed outside of major version of Helidon. - */ - @SuppressWarnings("unchecked") - @Deprecated - public static - LookupResult lookupAnnotation(E element, Class annotClass, Class clazz) { - // First check annotation on element - A annotation = (A) element.getAnnotation(annotClass); - if (annotation != null) { - return new LookupResult<>(MatchingType.METHOD, annotation); - } - // Finally check annotations on class - annotation = (A) element.getDeclaringClass().getAnnotation(annotClass); - if (annotation == null) { - annotation = (A) clazz.getAnnotation(annotClass); - } - return annotation == null ? null : new LookupResult<>(MatchingType.CLASS, annotation); - } - - @Deprecated - static LookupResult lookupAnnotation( - AnnotatedType annotatedType, - AnnotatedMethod annotatedMethod, - Class annotClass) { - A annotation = annotatedMethod.getAnnotation(annotClass); - if (annotation != null) { - return new LookupResult<>(matchingType(annotatedMethod), annotation); - } - - annotation = annotatedType.getAnnotation(annotClass); - if (annotation == null) { - annotation = annotatedType.getJavaClass().getAnnotation(annotClass); - } - return annotation == null ? null : new LookupResult<>(MatchingType.CLASS, annotation); - } - - @Deprecated static List> lookupAnnotations( AnnotatedType annotatedType, AnnotatedMember annotatedMember, @@ -165,8 +107,7 @@ static Stream metricsAnnotationsOnElement(Annotated an * * @return name of the metric */ - @Deprecated - public static + static String getMetricName(Member element, Class clazz, MatchingType matchingType, String explicitName, boolean absolute) { String result; if (matchingType == MatchingType.METHOD) { @@ -204,182 +145,6 @@ String getMetricName(Member element, Class clazz, MatchingType matchingType, return result; } - /** - * Register a metric. - * - * @param registry the metric registry in which to register the metric - * @param element the annotated element - * @param clazz the annotated class - * @param annotation the annotation to register - * @param type the {@link MatchingType} indicating the type of annotated element - * @param the annotated element type - */ - @Deprecated - public static - void registerMetric(MetricRegistry registry, E element, Class clazz, Annotation annotation, MatchingType type) { - - if (annotation instanceof Counted) { - Counted counted = (Counted) annotation; - String metricName = getMetricName(element, clazz, type, counted.name().trim(), counted.absolute()); - String displayName = counted.displayName().trim(); - Metadata meta = Metadata.builder() - .withName(metricName) - .withDisplayName(displayName.isEmpty() ? metricName : displayName) - .withDescription(counted.description().trim()) - .withType(MetricType.COUNTER) - .withUnit(counted.unit().trim()) - .build(); - registry.counter(meta, tags(counted.tags())); - LOGGER.fine(() -> "### Registered counter " + metricName); - } else if (annotation instanceof Metered) { - Metered metered = (Metered) annotation; - String metricName = getMetricName(element, clazz, type, metered.name().trim(), metered.absolute()); - String displayName = metered.displayName().trim(); - Metadata meta = Metadata.builder() - .withName(metricName) - .withDisplayName(displayName.isEmpty() ? metricName : displayName) - .withDescription(metered.description().trim()) - .withType(MetricType.METERED) - .withUnit(metered.unit().trim()) - .build(); - registry.meter(meta, tags(metered.tags())); - LOGGER.fine(() -> "### Registered meter " + metricName); - } else if (annotation instanceof ConcurrentGauge) { - ConcurrentGauge concurrentGauge = (ConcurrentGauge) annotation; - String metricName = getMetricName(element, clazz, type, concurrentGauge.name().trim(), - concurrentGauge.absolute()); - String displayName = concurrentGauge.displayName().trim(); - Metadata meta = Metadata.builder() - .withName(metricName) - .withDisplayName(displayName.isEmpty() ? metricName : displayName) - .withDescription(concurrentGauge.description().trim()) - .withType(MetricType.METERED) - .withUnit(concurrentGauge.unit().trim()).build(); - registry.concurrentGauge(meta, tags(concurrentGauge.tags())); - LOGGER.fine(() -> "### Registered ConcurrentGauge " + metricName); - } else if (annotation instanceof Timed) { - Timed timed = (Timed) annotation; - String metricName = getMetricName(element, clazz, type, timed.name().trim(), timed.absolute()); - String displayName = timed.displayName().trim(); - Metadata meta = Metadata.builder() - .withName(metricName) - .withDisplayName(displayName.isEmpty() ? metricName : displayName) - .withDescription(timed.description().trim()) - .withType(MetricType.TIMER) - .withUnit(timed.unit().trim()) - .build(); - registry.timer(meta, tags(timed.tags())); - LOGGER.fine(() -> "### Registered timer " + metricName); - } else if (annotation instanceof SimplyTimed) { - SimplyTimed simplyTimed = (SimplyTimed) annotation; - String metricName = getMetricName(element, clazz, type, simplyTimed.name().trim(), simplyTimed.absolute()); - String displayName = simplyTimed.displayName().trim(); - Metadata meta = Metadata.builder() - .withName(metricName) - .withDisplayName(displayName.isEmpty() ? metricName : displayName) - .withDescription(simplyTimed.description().trim()) - .withType(MetricType.SIMPLE_TIMER) - .withUnit(simplyTimed.unit().trim()) - .build(); - registry.simpleTimer(meta, tags(simplyTimed.tags())); - LOGGER.fine(() -> "### Registered simple timer " + metricName); - } - } - - /** - * Register a metric. - * - * @param element the annotated element - * @param clazz the annotated class - * @param lookupResult the annotation lookup result - * @param the annotated element type - */ - public static - void registerMetric(E element, Class clazz, LookupResult lookupResult) { - registerMetric(element, clazz, lookupResult.getAnnotation(), lookupResult.getType()); - } - - /** - * Register a metric. - * - * @param element the annotated element - * @param clazz the annotated class - * @param annotation the annotation to register - * @param type the {@link MatchingType} indicating the type of annotated element - * @param the annotated element type - */ - public static - void registerMetric(E element, Class clazz, Annotation annotation, MatchingType type) { - MetricRegistry registry = getMetricRegistry(); - - if (annotation instanceof Counted) { - Counted counted = (Counted) annotation; - String metricName = getMetricName(element, clazz, type, counted.name().trim(), counted.absolute()); - String displayName = counted.displayName().trim(); - Metadata meta = Metadata.builder() - .withName(metricName) - .withDisplayName(displayName.isEmpty() ? metricName : displayName) - .withDescription(counted.description().trim()) - .withType(MetricType.COUNTER) - .withUnit(counted.unit().trim()) - .build(); - registry.counter(meta); - LOGGER.fine(() -> "### Registered counter " + metricName); - } else if (annotation instanceof Metered) { - Metered metered = (Metered) annotation; - String metricName = getMetricName(element, clazz, type, metered.name().trim(), metered.absolute()); - String displayName = metered.displayName().trim(); - Metadata meta = Metadata.builder() - .withName(metricName) - .withDisplayName(displayName.isEmpty() ? metricName : displayName) - .withDescription(metered.description().trim()) - .withType(MetricType.METERED) - .withUnit(metered.unit().trim()) - .build(); - registry.meter(meta); - LOGGER.fine(() -> "### Registered meter " + metricName); - } else if (annotation instanceof ConcurrentGauge) { - ConcurrentGauge concurrentGauge = (ConcurrentGauge) annotation; - String metricName = getMetricName(element, clazz, type, concurrentGauge.name().trim(), - concurrentGauge.absolute()); - String displayName = concurrentGauge.displayName().trim(); - Metadata meta = Metadata.builder() - .withName(metricName) - .withDisplayName(displayName.isEmpty() ? metricName : displayName) - .withDescription(concurrentGauge.description().trim()) - .withType(MetricType.METERED) - .withUnit(concurrentGauge.unit().trim()).build(); - registry.concurrentGauge(meta); - LOGGER.fine(() -> "### Registered ConcurrentGauge " + metricName); - } else if (annotation instanceof Timed) { - Timed timed = (Timed) annotation; - String metricName = getMetricName(element, clazz, type, timed.name().trim(), timed.absolute()); - String displayName = timed.displayName().trim(); - Metadata meta = Metadata.builder() - .withName(metricName) - .withDisplayName(displayName.isEmpty() ? metricName : displayName) - .withDescription(timed.description().trim()) - .withType(MetricType.TIMER) - .withUnit(timed.unit().trim()) - .build(); - registry.timer(meta); - LOGGER.fine(() -> "### Registered timer " + metricName); - } else if (annotation instanceof SimplyTimed) { - SimplyTimed simplyTimed = (SimplyTimed) annotation; - String metricName = getMetricName(element, clazz, type, simplyTimed.name().trim(), simplyTimed.absolute()); - String displayName = simplyTimed.displayName().trim(); - Metadata meta = Metadata.builder() - .withName(metricName) - .withDisplayName(displayName.isEmpty() ? metricName : displayName) - .withDescription(simplyTimed.description().trim()) - .withType(MetricType.SIMPLE_TIMER) - .withUnit(simplyTimed.unit().trim()) - .build(); - registry.timer(meta); - LOGGER.fine(() -> "### Registered simple timer " + metricName); - } - } - private static MetricRegistry getMetricRegistry() { return RegistryProducer.getDefaultRegistry(); } @@ -401,14 +166,7 @@ static Tag[] tags(String[] tagStrings) { return result.toArray(new Tag[result.size()]); } - /** - * DO NOT USE THIS CLASS please. - * - * Types of possible matching. - * @deprecated This class is made public to migrate from metrics1 to metrics2 for gRPC, this should be refactored - */ - @Deprecated - public enum MatchingType { + enum MatchingType { /** * Method. */ @@ -426,13 +184,7 @@ private static MatchingType matchingType(Annotated annotated) { : MatchingType.CLASS; } - /** - * DO NOT USE THIS CLASS please. - * @param type of annotation - * @deprecated This class is made public to migrate from metrics1 to metrics2 for gRPC, this should be refactored - */ - @Deprecated - public static class LookupResult { + static class LookupResult { private final MatchingType type; @@ -454,7 +206,7 @@ public static class LookupResult { * * @return matching type */ - public MatchingType getType() { + MatchingType getType() { return type; } @@ -463,7 +215,7 @@ public MatchingType getType() { * * @return the annotation */ - public A getAnnotation() { + A getAnnotation() { return annotation; } } diff --git a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricsCdiExtension.java b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricsCdiExtension.java index e13b5573042..4ced691e43e 100644 --- a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricsCdiExtension.java +++ b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricsCdiExtension.java @@ -18,7 +18,6 @@ import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Member; import java.lang.reflect.Method; @@ -31,9 +30,9 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; +import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -50,6 +49,8 @@ import io.helidon.metrics.serviceapi.MetricsSupport; import io.helidon.microprofile.metrics.MetricAnnotationInfo.RegistrationPrep; import io.helidon.microprofile.metrics.MetricUtil.LookupResult; +import io.helidon.microprofile.metrics.spi.MetricAnnotationDiscoveryObserver; +import io.helidon.microprofile.metrics.spi.MetricRegistrationObserver; import io.helidon.microprofile.server.ServerCdiExtension; import io.helidon.servicecommon.restcdi.HelidonRestCdiExtension; import io.helidon.webserver.Routing; @@ -63,7 +64,6 @@ import jakarta.enterprise.inject.spi.AfterDeploymentValidation; import jakarta.enterprise.inject.spi.Annotated; import jakarta.enterprise.inject.spi.AnnotatedCallable; -import jakarta.enterprise.inject.spi.AnnotatedMember; import jakarta.enterprise.inject.spi.AnnotatedMethod; import jakarta.enterprise.inject.spi.AnnotatedType; import jakarta.enterprise.inject.spi.Bean; @@ -73,7 +73,10 @@ import jakarta.enterprise.inject.spi.ProcessAnnotatedType; import jakarta.enterprise.inject.spi.ProcessManagedBean; import jakarta.enterprise.inject.spi.WithAnnotations; +import jakarta.enterprise.inject.spi.configurator.AnnotatedConstructorConfigurator; +import jakarta.enterprise.inject.spi.configurator.AnnotatedMethodConfigurator; import jakarta.enterprise.inject.spi.configurator.AnnotatedTypeConfigurator; +import jakarta.enterprise.util.AnnotationLiteral; import jakarta.inject.Singleton; import jakarta.interceptor.Interceptor; import jakarta.ws.rs.DELETE; @@ -106,8 +109,9 @@ * *

* Earlier versions of this class detected app-provided producer fields and methods and triggered creation and registration - * of the corresponding metrics upon such detection. As explained in - * https://github.com/eclipse/microprofile-metrics/issues/456 and https://github.com/eclipse/microprofile-metrics/pull/594 + * of the corresponding metrics upon such detection. As explained in this + * MP metrics issue + * and this MP metrics PR, * this probably was never correct and does not work because {@code @Metric} no longer applies to producers per the * MP metrics 3.0 spec. The issue and PR discussion explain how developers who provide their own producers should use * CDI qualifiers on the producers (and, therefore, injection points) to avoid ambiguity between their own producers and @@ -122,12 +126,16 @@ public class MetricsCdiExtension extends HelidonRestCdiExtension private static final Logger LOGGER = Logger.getLogger(MetricsCdiExtension.class.getName()); - static final Class[] ALL_METRIC_ANNOTATIONS_ARRAY = - (Class[]) new Class[] - {Counted.class, Metered.class, Timed.class, ConcurrentGauge.class, SimplyTimed.class, Gauge.class}; + static final Set> ALL_METRIC_ANNOTATIONS = Set.of( + Counted.class, Metered.class, Timed.class, ConcurrentGauge.class, SimplyTimed.class, Gauge.class); - static final Set> ALL_METRIC_ANNOTATIONS = - new HashSet<>(Arrays.asList(ALL_METRIC_ANNOTATIONS_ARRAY)); + private static final Map, AnnotationLiteral> INTERCEPTED_METRIC_ANNOTATIONS = + Map.of( + Counted.class, InterceptorCounted.binding(), + Metered.class, InterceptorMetered.binding(), + Timed.class, InterceptorTimed.binding(), + ConcurrentGauge.class, InterceptorConcurrentGauge.binding(), + SimplyTimed.class, InterceptorSimplyTimed.binding()); private static final List> JAX_RS_ANNOTATIONS = Arrays.asList(GET.class, PUT.class, POST.class, HEAD.class, OPTIONS.class, DELETE.class, PATCH.class); @@ -170,10 +178,6 @@ public class MetricsCdiExtension extends HelidonRestCdiExtension .withUnit(MetricUnits.NONE) .build(); - // only for compatibility with gRPC usage of registerMetric - @Deprecated - private static final List LEGACY_ANNOTATED_SITES = new ArrayList<>(); - private boolean restEndpointsMetricsEnabled = REST_ENDPOINTS_METRIC_ENABLED_DEFAULT_VALUE; private final Map> annotatedGaugeSites = new HashMap<>(); @@ -185,11 +189,15 @@ public class MetricsCdiExtension extends HelidonRestCdiExtension private final Set> restRequestMetricsClassesProcessed = new HashSet<>(); private final Set restRequestMetricsToRegister = new HashSet<>(); - private final AtomicReference config = new AtomicReference<>(); - private final AtomicReference metricsConfig = new AtomicReference<>(); - private final WorkItemsManager workItemsManager = WorkItemsManager.create(); + private final List metricAnnotationDiscoveryObservers = new ArrayList<>(); + private final List metricRegistrationObservers = new ArrayList<>(); + + private final Map> metricAnnotationDiscoveriesByExecutable = new HashMap<>(); + + @SuppressWarnings("unchecked") + // records stereotype annotations which have metrics annotations inside them private final Map, StereotypeMetricsInfo> stereotypeMetricsInfo = new HashMap<>(); @@ -206,41 +214,21 @@ public MetricsCdiExtension() { } /** - * DO NOT USE THIS METHOD please. + * Records an observer of metric annotation discoveries. * - * @param element element - * @param clazz class - * @param lookupResult lookup result - * @param type of element - * @deprecated This method is made public to migrate from metrics1 to metrics2 for gRPC, this should be refactored + * @param metricAnnotationDiscoveryObserver the observer to enroll */ - @SuppressWarnings("rawtypes") - @Deprecated - public static - void registerMetric(E element, Class clazz, LookupResult lookupResult) { - Executable executable; - if (element instanceof AnnotatedCallable) { - executable = (Executable) ((AnnotatedCallable) element).getJavaMember(); - } else if (element instanceof Constructor) { - // code checking instanceof Executable and casting to it would not compile on Java17 - executable = (Constructor) element; - } else if (element instanceof Method) { - executable = (Method) element; - } else { - throw new IllegalArgumentException("Element must be an AnnotatedCallable or Executable but was " - + element.getClass().getName()); - } - - registerMetricInternal(LEGACY_ANNOTATED_SITES, element, clazz, lookupResult, executable); + public void enroll(MetricAnnotationDiscoveryObserver metricAnnotationDiscoveryObserver) { + metricAnnotationDiscoveryObservers.add(metricAnnotationDiscoveryObserver); } - static - void registerMetricInternal(List sites, - E element, - Class clazz, - LookupResult lookupResult, - Executable executable) { - recordAnnotatedSite(sites, element, clazz, lookupResult, executable); + /** + * Records an observer of metric registrations. + * + * @param metricRegistrationObserver the observer to enroll + */ + public void enroll(MetricRegistrationObserver metricRegistrationObserver) { + metricRegistrationObservers.add(metricRegistrationObserver); } private static void recordAnnotatedSite( @@ -258,32 +246,23 @@ private static void recordAnnotatedSite( private void registerMetricsForAnnotatedSites() { MetricRegistry registry = getMetricRegistry(); - List.of(annotatedSites, LEGACY_ANNOTATED_SITES) - .forEach(sites -> { - for (RegistrationPrep registrationPrep : sites) { - org.eclipse.microprofile.metrics.Metric metric = registrationPrep.register(registry); - workItemsManager.put(registrationPrep.executable(), registrationPrep.annotationType(), - BasicMetricWorkItem - .create(new MetricID(registrationPrep.metricName(), - registrationPrep.tags()), - metric)); - } - sites.clear(); - }); - } - - /** - * For test use only. - * - * This method is used from gRPC integration tests and should not be used elsewhere. - */ - @Deprecated - protected static void registerMetricsForAnnotatedSitesFromGrpcTest() { - MetricRegistry registry = getMetricRegistry(); - for (RegistrationPrep registrationPrep : LEGACY_ANNOTATED_SITES) { - registrationPrep.register(registry); + for (RegistrationPrep registrationPrep : annotatedSites) { + metricAnnotationDiscoveriesByExecutable.get(registrationPrep.executable()) + .forEach(discovery -> { + if (discovery.isActive()) { // All annotation discovery observers agreed to preserve the discovery. + org.eclipse.microprofile.metrics.Metric metric = registrationPrep.register(registry); + MetricID metricID = new MetricID(registrationPrep.metricName(), registrationPrep.tags()); + metricRegistrationObservers.forEach( + o -> o.onRegistration(discovery, registrationPrep.metadata(), metricID, metric)); + workItemsManager.put(registrationPrep.executable(), registrationPrep.annotationType(), + BasicMetricWorkItem + .create(new MetricID(registrationPrep.metricName(), + registrationPrep.tags()), + metric)); + } + }); } - LEGACY_ANNOTATED_SITES.clear(); + annotatedSites.clear(); } @Override @@ -406,27 +385,98 @@ public void clearAnnotationInfo(@Observes AfterDeploymentValidation adv) { * observe managed beans (which CDI invokes for all managed beans) where we also have to examine each method and * constructor, we can quickly eliminate from consideration any classes we have not recorded here. * + * This observer runs after other {@code ProcessAnnotatedType} observers to give other extensions a chance to provide their + * own interceptors for selected constructors and methods by adding {@link MetricAnnotationDiscoveryObserver} + * to the configured type, constructor, or method. + * * @param pat ProcessAnnotatedType event */ - private void recordMetricAnnotatedClass(@Observes - @WithAnnotations({Counted.class, Metered.class, Timed.class, ConcurrentGauge.class, SimplyTimed.class, Gauge.class}) - ProcessAnnotatedType pat) { + private void recordMetricAnnotatedClass(@Observes @Priority(Interceptor.Priority.APPLICATION + 500 + 10) + @WithAnnotations({Counted.class, + Metered.class, + Timed.class, + ConcurrentGauge.class, + SimplyTimed.class}) ProcessAnnotatedType pat) { if (isConcreteNonInterceptor(pat)) { recordAnnotatedType(pat); + recordStereotypes(pat); + + /* + Wherever metrics annotations appear--at the type, at each constructor, and at each method--record those sites + and, if the annotation discovery observers concur, set up the normal metrics interceptors as needed. + + We need to visit all the possible sites now so we can add the private interceptor-bound annotations as + appropriate. + + */ + bindInterceptors(pat); + } + } + + private void bindInterceptors(ProcessAnnotatedType pat) { + + // Sadly, the various AnnotatedXXXConfigurator types do not share a common supertype, so we deal with each individually + // using plenty of method references. + + AnnotatedTypeConfigurator annotatedTypeConfigurator = pat.configureAnnotatedType(); + + bindInterceptorsAndRecordDiscoveries(annotatedTypeConfigurator, + pat.configureAnnotatedType().constructors(), + AnnotatedConstructorConfigurator::getAnnotated, + AnnotatedConstructorConfigurator::add); + + bindInterceptorsAndRecordDiscoveries(annotatedTypeConfigurator, + pat.configureAnnotatedType().methods(), + AnnotatedMethodConfigurator::getAnnotated, + (BiFunction, Annotation, + AnnotatedMethodConfigurator>) AnnotatedMethodConfigurator::add); + } + + private > void bindInterceptorsAndRecordDiscoveries( + AnnotatedTypeConfigurator annotatedTypeConfigurator, + Iterable executableConfigurators, + Function configuratorAnnotatedGetter, + BiFunction annotationAdder) { + executableConfigurators.forEach(executableConfigurator -> { + // Process all metric annotations which apply to this executable, either from the type-level or from this + // executable itself. + A annotatedCallable = configuratorAnnotatedGetter.apply(executableConfigurator); + Executable exec = (annotatedCallable.getJavaMember() instanceof Executable) + ? (Executable) annotatedCallable.getJavaMember() + : null; + metricsLookupResults(annotatedTypeConfigurator.getAnnotated(), + annotatedCallable) + .forEach(lookupResult -> { + MetricAnnotationDiscoveryBase discoveryEvent = MetricAnnotationDiscoveryBase.create( + annotatedTypeConfigurator, + executableConfigurator, + lookupResult.getAnnotation()); + if (exec != null) { + metricAnnotationDiscoveriesByExecutable.computeIfAbsent(exec, o -> new ArrayList<>()) + .add(discoveryEvent); + metricAnnotationDiscoveryObservers.forEach(observer -> observer.onDiscovery(discoveryEvent)); + } + if (discoveryEvent.shouldUseDefaultInterceptor()) { + Class metricAnnotationClass = lookupResult.getAnnotation().annotationType(); + annotationAdder.apply(executableConfigurator, + INTERCEPTED_METRIC_ANNOTATIONS.get(metricAnnotationClass)); + } + }); + }); + } - // Find and record stereotypes applied to the type or its members which themselves carry metrics annotations. - AnnotatedType annotatedType = pat.getAnnotatedType(); - - Stream.concat(Stream.of(annotatedType), - Stream.concat(pat.getAnnotatedType().getMethods().stream(), - Stream.concat(pat.getAnnotatedType().getConstructors().stream(), - pat.getAnnotatedType().getFields().stream()))) - .map(Annotated::getAnnotations) - .flatMap(Set::stream) - .distinct() - .filter(MetricsCdiExtension::isStereotype) - .forEach(this::recordIfMetricsRelatedStereotype); - } + private void recordStereotypes(ProcessAnnotatedType pat) { + AnnotatedType annotatedType = pat.getAnnotatedType(); + // Find and record stereotypes applied to the type or its members which themselves carry metrics annotations. + Stream.concat(Stream.of(annotatedType), + Stream.concat(pat.getAnnotatedType().getMethods().stream(), + Stream.concat(pat.getAnnotatedType().getConstructors().stream(), + pat.getAnnotatedType().getFields().stream()))) + .map(Annotated::getAnnotations) + .flatMap(Set::stream) + .distinct() + .filter(MetricsCdiExtension::isStereotype) + .forEach(this::recordIfMetricsRelatedStereotype); } private static boolean isStereotype(Annotation annotation) { @@ -444,6 +494,34 @@ private void recordIfMetricsRelatedStereotype(Annotation stereotypeAnnotation) { } } + /** + * Collects all {@code LookupResult} objects for metrics annotations on a given annotated executable. + * + * @param annotatedType the annotated type containing the constructor or method + * @param annotatedMember the constructor or method + * @return {@code LookupResult} instances that apply to the executable + */ + private Iterable> metricsLookupResults(AnnotatedType annotatedType, + AnnotatedCallable annotatedMember) { + List> result = new ArrayList<>(); + INTERCEPTED_METRIC_ANNOTATIONS.keySet().forEach(metricAnnotationClass -> { + result.addAll(MetricUtil.lookupAnnotations(annotatedType, + annotatedMember, + metricAnnotationClass, + stereotypeMetricsInfo)); + }); + return result; + } + +// private Set> metricsAnnotationClasses(Annotated annotated) { +// return annotated +// .getAnnotations() +// .stream() +// .map(Annotation::annotationType) +// .filter(METRIC_ANNOTATIONS::containsKey) +// .collect(Collectors.toSet()); +// } + /** * Checks to make sure the annotated type is not abstract and is not an interceptor. * @@ -729,23 +807,6 @@ private static boolean chooseRestEndpointsSetting(Config metricsConfig) { return result; } - private static MetricType getMetricType(T metric) { - // Find subtype of Metric, needed for user-defined metrics - Class clazz = metric.getClass(); - do { - Optional> optionalClass = Arrays.stream(clazz.getInterfaces()) - .filter(org.eclipse.microprofile.metrics.Metric.class::isAssignableFrom) - .findFirst(); - if (optionalClass.isPresent()) { - clazz = optionalClass.get(); - break; - } - clazz = clazz.getSuperclass(); - } while (clazz != null); - - return MetricType.from(clazz == null ? metric.getClass() : clazz); - } - private void recordAnnotatedGaugeSite(@Observes ProcessManagedBean pmb) { AnnotatedType type = pmb.getAnnotatedBeanClass(); Class clazz = type.getJavaClass(); @@ -904,61 +965,10 @@ private static Class typeToNumber(Class clazz) { return narrowedReturnType; } - static class AnnotatedElementWrapper implements AnnotatedElement, Member { - - private final AnnotatedMember annotatedMember; - - AnnotatedElementWrapper(AnnotatedMember annotatedMember) { - this.annotatedMember = annotatedMember; - } - - @Override - public boolean isAnnotationPresent(Class annotationClass) { - return annotatedMember.isAnnotationPresent(annotationClass); - } - - @Override - public T getAnnotation(Class annotationClass) { - return annotatedMember.getAnnotation(annotationClass); - } - - @Override - public Annotation[] getAnnotations() { - return annotatedMember.getAnnotations().toArray(new Annotation[] {}); - } - - @Override - public Annotation[] getDeclaredAnnotations() { - return getAnnotations(); - } - - @Override - public Class getDeclaringClass() { - return annotatedMember.getDeclaringType().getJavaClass(); - } - - @Override - public String getName() { - return annotatedMember.getJavaMember().getName(); - } - - @Override - public int getModifiers() { - return annotatedMember.getJavaMember().getModifiers(); - } - - @Override - public boolean isSynthetic() { - return annotatedMember.getJavaMember().isSynthetic(); - } - } - - static record StereotypeMetricsInfo(Set metricsAnnotations) { + record StereotypeMetricsInfo(Set metricsAnnotations) { static StereotypeMetricsInfo create(Set metricsAnnotations) { return new StereotypeMetricsInfo(metricsAnnotations); } } - - } diff --git a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/spi/MetricAnnotationDiscoveryObserver.java b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/spi/MetricAnnotationDiscoveryObserver.java new file mode 100644 index 00000000000..19d6e2431e3 --- /dev/null +++ b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/spi/MetricAnnotationDiscoveryObserver.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 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.metrics.spi; + +import io.helidon.microprofile.metrics.MetricAnnotationDiscovery; + +/** + * Observer of the discovery of metric annotations which are applied to constructors and methods. + *

+ * Implementations make themselves known via the Java service loader mechanism. + *

+ *

+ * Observers are notified during {@code ProcessAnnotatedType}, for each metric annotation that is + * discovered to apply to an executable, via a + * {@link io.helidon.microprofile.metrics.MetricAnnotationDiscovery} event. + *

+ */ +public interface MetricAnnotationDiscoveryObserver { + + /** + * Notifies the observer that a metric annotation has been discovered to apply to a constructor or method. + * + * @param metricAnnotationDiscovery the discovery event + */ + void onDiscovery(MetricAnnotationDiscovery metricAnnotationDiscovery); + +} diff --git a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/spi/MetricRegistrationObserver.java b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/spi/MetricRegistrationObserver.java new file mode 100644 index 00000000000..0a223276522 --- /dev/null +++ b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/spi/MetricRegistrationObserver.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022 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.metrics.spi; + +import io.helidon.microprofile.metrics.MetricAnnotationDiscovery; + +import org.eclipse.microprofile.metrics.Metadata; +import org.eclipse.microprofile.metrics.Metric; +import org.eclipse.microprofile.metrics.MetricID; + +/** + * Observer of the registration of metrics due to metric annotations applied to executables--constructors and methods. + *

+ * Implementations make themselves known via the Java service loader mechanism. + *

+ *

+ * Once registered, the observer is notified each time a metric required by a metric annotation is registered. + *

+ */ +public interface MetricRegistrationObserver { + + /** + * Notifies the observer that a metric has been registered due to a metric annotation that applies to an executable. + * + * @param discovery the {@link io.helidon.microprofile.metrics.MetricAnnotationDiscovery} + * triggering this metric registration + * @param metadata the metrics {@link org.eclipse.microprofile.metrics.Metadata} for the indicated metric + * @param metricId the {@link org.eclipse.microprofile.metrics.MetricID} for the indicated metric + * @param metric the metric associated with the discovery index + */ + void onRegistration(MetricAnnotationDiscovery discovery, Metadata metadata, MetricID metricId, Metric metric); +} diff --git a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/spi/package-info.java b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/spi/package-info.java new file mode 100644 index 00000000000..402d15257d8 --- /dev/null +++ b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/spi/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 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. + */ + +/** + * Interfaces optionally implemented by other components interested in key metrics processing events. + */ +package io.helidon.microprofile.metrics.spi; diff --git a/microprofile/metrics/src/main/java/module-info.java b/microprofile/metrics/src/main/java/module-info.java index 36e1f7ca2e4..a5d4de4a3fa 100644 --- a/microprofile/metrics/src/main/java/module-info.java +++ b/microprofile/metrics/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2022 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. @@ -31,6 +31,7 @@ requires io.helidon.servicecommon.restcdi; requires io.helidon.microprofile.server; requires io.helidon.microprofile.config; + requires io.helidon.common.serviceloader; requires transitive io.helidon.metrics.api; requires transitive io.helidon.metrics.serviceapi; @@ -39,9 +40,11 @@ requires io.helidon.config.mp; exports io.helidon.microprofile.metrics; + exports io.helidon.microprofile.metrics.spi; // this is needed for CDI extensions that use non-public observer methods opens io.helidon.microprofile.metrics to weld.core.impl, io.helidon.microprofile.cdi; + opens io.helidon.microprofile.metrics.spi to io.helidon.microprofile.cdi, weld.core.impl; provides jakarta.enterprise.inject.spi.Extension with io.helidon.microprofile.metrics.MetricsCdiExtension; } diff --git a/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestDisabledMetrics.java b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestDisabledMetrics.java index 06a4cd25cba..c49cb564030 100644 --- a/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestDisabledMetrics.java +++ b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestDisabledMetrics.java @@ -33,7 +33,7 @@ class TestDisabledMetrics { @Test void ensureRegistryFactoryIsMinimal() { - // Invoking getInstance() should retrieve the factory previously initialized as disabled. + // Invoking instance() should retrieve the factory previously initialized as disabled. RegistryFactory rf = RegistryFactory.getInstance(); assertThat("RegistryFactory type", rf, not(instanceOf(io.helidon.metrics.RegistryFactory.class))); } diff --git a/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestDiscoveryObserverImpl.java b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestDiscoveryObserverImpl.java new file mode 100644 index 00000000000..97959fb06bc --- /dev/null +++ b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestDiscoveryObserverImpl.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 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.metrics; + +import java.util.ArrayList; +import java.util.List; + +import io.helidon.common.LazyValue; +import io.helidon.microprofile.metrics.spi.MetricAnnotationDiscoveryObserver; + +public class TestDiscoveryObserverImpl implements MetricAnnotationDiscoveryObserver { + + private static final LazyValue INSTANCE = LazyValue.create(TestDiscoveryObserverImpl::new); + + private final List discoveries = new ArrayList<>(); + + static TestDiscoveryObserverImpl instance() { + return INSTANCE.get(); + } + + @Override + public void onDiscovery(MetricAnnotationDiscovery metricAnnotationDiscovery) { + discoveries.add(metricAnnotationDiscovery); + } + + static List discoveries() { + return instance().discoveries; + } +} diff --git a/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestObservers.java b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestObservers.java new file mode 100644 index 00000000000..1e0d886c939 --- /dev/null +++ b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestObservers.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2022 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.metrics; + +import io.helidon.microprofile.tests.junit5.AddExtension; +import io.helidon.microprofile.tests.junit5.HelidonTest; + +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.BeforeBeanDiscovery; +import jakarta.enterprise.inject.spi.Extension; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +@HelidonTest +@AddExtension(TestObservers.TestExtension.class) +public class TestObservers { + + @Test + void testDiscoveryObserver() { + assertThat("Observer's discoveries", TestDiscoveryObserverImpl.discoveries().size(), is(not(0))); + } + + @Test + void testRegistrationObserver() { + assertThat("Observer's registrations", TestRegistrationObserverImpl.registrations(), is(not(0))); + } + + public static class TestExtension implements Extension { + + void enroll(@Observes BeforeBeanDiscovery bbd, BeanManager beanManager) { + MetricsCdiExtension metricsCdiExtension = beanManager.getExtension(MetricsCdiExtension.class); + metricsCdiExtension.enroll(TestDiscoveryObserverImpl.instance()); + metricsCdiExtension.enroll(TestRegistrationObserverImpl.instance()); + } + } +} diff --git a/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestRegistrationObserverImpl.java b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestRegistrationObserverImpl.java new file mode 100644 index 00000000000..fdf1d73056d --- /dev/null +++ b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestRegistrationObserverImpl.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022 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.metrics; + +import io.helidon.common.LazyValue; +import io.helidon.microprofile.metrics.spi.MetricRegistrationObserver; + +import org.eclipse.microprofile.metrics.Metadata; +import org.eclipse.microprofile.metrics.Metric; +import org.eclipse.microprofile.metrics.MetricID; + +public class TestRegistrationObserverImpl implements MetricRegistrationObserver { + + private static final LazyValue INSTANCE = LazyValue.create(TestRegistrationObserverImpl::new); + + static TestRegistrationObserverImpl instance() { + return INSTANCE.get(); + } + + private int registrations; + + @Override + public void onRegistration(MetricAnnotationDiscovery discovery, + Metadata metadata, + MetricID metricId, + Metric metric) { + registrations++; + } + + static int registrations() { + return INSTANCE.get().registrations; + } +} diff --git a/tests/integration/mp-grpc/pom.xml b/tests/integration/mp-grpc/pom.xml index 606d03a0be2..f7f00467cf6 100644 --- a/tests/integration/mp-grpc/pom.xml +++ b/tests/integration/mp-grpc/pom.xml @@ -44,11 +44,20 @@ helidon-microprofile-server test + + io.helidon.metrics + helidon-metrics-api + io.helidon.microprofile.metrics helidon-microprofile-metrics provided + + io.helidon.microprofile.tests + helidon-microprofile-tests-junit5 + test + org.junit.jupiter junit-jupiter-api @@ -91,13 +100,8 @@ test - org.jboss.weld - weld-junit5 - test - - - org.glassfish.jersey.core - jersey-client + io.helidon.microprofile.weld + weld-se-core test 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 258f497b856..854237dd75e 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 @@ -23,16 +23,31 @@ import io.helidon.grpc.server.GrpcService; import io.helidon.grpc.server.MethodDescriptor; import io.helidon.grpc.server.ServiceDescriptor; -import io.helidon.metrics.RegistryFactory; +import io.helidon.metrics.api.RegistryFactory; import io.helidon.microprofile.grpc.core.Grpc; import io.helidon.microprofile.grpc.core.Unary; import io.grpc.ServerInterceptor; -import io.grpc.stub.StreamObserver; + import io.helidon.microprofile.grpc.server.JavaMarshaller; +import io.helidon.microprofile.grpc.server.test.Services; +import io.helidon.microprofile.metrics.MetricsCdiExtension; +import io.helidon.microprofile.server.JaxRsCdiExtension; +import io.helidon.microprofile.server.ServerCdiExtension; +import io.helidon.microprofile.tests.junit5.AddBean; +import io.helidon.microprofile.tests.junit5.AddExtension; +import io.helidon.microprofile.tests.junit5.DisableDiscovery; +import io.helidon.microprofile.tests.junit5.HelidonTest; + +import io.grpc.stub.StreamObserver; +import jakarta.enterprise.inject.Typed; +import jakarta.inject.Inject; +import jakarta.ws.rs.Produces; +import org.eclipse.microprofile.metrics.Counter; import org.eclipse.microprofile.metrics.MetricID; import org.eclipse.microprofile.metrics.MetricRegistry; import org.eclipse.microprofile.metrics.MetricType; +import org.eclipse.microprofile.metrics.SimpleTimer; import org.eclipse.microprofile.metrics.annotation.ConcurrentGauge; import org.eclipse.microprofile.metrics.annotation.Counted; import org.eclipse.microprofile.metrics.annotation.Metered; @@ -47,14 +62,35 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsEmptyIterable.emptyIterable; - +/** + * Exercises the metrics configurer. + * + * Because the metrics CDI extension is (as of 3.x) responsible for registering metrics, even gRPC-inspired ones, we need to + * start the container. We cannot currently allow the gRPC CDI extension to start because it tries to register both ServiceOne and + * ServiceThree under the same name, ServiceOne, because ServiceThree extends ServiceOne. The attempt to register two services + * with the same name fails. So we turn off discovery and explicitly add in the beans and extensions we need. + */ +@HelidonTest +@DisableDiscovery +@AddBean(MetricsConfigurerTest.ServiceOne.class) +@AddBean(MetricsConfigurerTest.ServiceThree.class) +@AddBean(MetricsConfigurerTest.ServiceTwoProducer.class) +@AddBean(NonGrpcMetricAnnotatedBean.class) +@AddExtension(MetricsCdiExtension.class) +@AddExtension(ServerCdiExtension.class) // needed for MetricsCdiExtension +@AddExtension(JaxRsCdiExtension.class) +@AddExtension(GrpcMetricsCdiExtension.class) public class MetricsConfigurerTest { private static MetricRegistry registry; + @Inject + private NonGrpcMetricAnnotatedBean nonGrpcMetricAnnotatedBean; + @BeforeAll public static void setup() { registry = RegistryFactory.getInstance().getRegistry(MetricRegistry.Type.APPLICATION); + } @Test @@ -74,7 +110,7 @@ public void shouldAddCounterMetricFromClassAnnotation() { assertThat(methodInterceptors.size(), is(1)); assertThat(methodInterceptors.get(0), is(instanceOf(GrpcMetrics.class))); assertThat(((GrpcMetrics) methodInterceptors.get(0)).metricType(), is(MetricType.COUNTER)); - assertThat(updReg().getCounters().get(new MetricID(ServiceOne.class.getName() + ".counted")), is(notNullValue())); + assertThat(registry.getCounters().get(new MetricID(ServiceOne.class.getName() + ".counted")), is(notNullValue())); } @Test @@ -94,7 +130,7 @@ public void shouldAddMeterMetricFromClassAnnotation() { assertThat(methodInterceptors.size(), is(1)); assertThat(methodInterceptors.get(0), is(instanceOf(GrpcMetrics.class))); assertThat(((GrpcMetrics) methodInterceptors.get(0)).metricType(), is(MetricType.METERED)); - assertThat(updReg().getMeters().get(new MetricID(ServiceOne.class.getName() + ".metered")), is(notNullValue())); + assertThat(registry.getMeters().get(new MetricID(ServiceOne.class.getName() + ".metered")), is(notNullValue())); } @Test @@ -114,7 +150,7 @@ public void shouldAddTimerMetricFromClassAnnotation() { assertThat(methodInterceptors.size(), is(1)); assertThat(methodInterceptors.get(0), is(instanceOf(GrpcMetrics.class))); assertThat(((GrpcMetrics) methodInterceptors.get(0)).metricType(), is(MetricType.TIMER)); - assertThat(updReg().getTimers().get(new MetricID(ServiceOne.class.getName() + ".timed")), is(notNullValue())); + assertThat(registry.getTimers().get(new MetricID(ServiceOne.class.getName() + ".timed")), is(notNullValue())); } @Test @@ -134,7 +170,7 @@ public void shouldAddSimpleTimerMetricFromClassAnnotation() { assertThat(methodInterceptors.size(), is(1)); assertThat(methodInterceptors.get(0), is(instanceOf(GrpcMetrics.class))); assertThat(((GrpcMetrics) methodInterceptors.get(0)).metricType(), is(MetricType.SIMPLE_TIMER)); - assertThat(updReg().getSimpleTimers().get(new MetricID(ServiceOne.class.getName() + ".simplyTimed")), is(notNullValue())); + assertThat(registry.getSimpleTimers().get(new MetricID(ServiceOne.class.getName() + ".simplyTimed")), is(notNullValue())); } @Test @@ -154,7 +190,7 @@ public void shouldAddConcurrentGaugeMetricFromClassAnnotation() { assertThat(methodInterceptors.size(), is(1)); assertThat(methodInterceptors.get(0), is(instanceOf(GrpcMetrics.class))); assertThat(((GrpcMetrics) methodInterceptors.get(0)).metricType(), is(MetricType.CONCURRENT_GAUGE)); - assertThat(updReg().getConcurrentGauges().get(new MetricID(ServiceOne.class.getName() + ".concurrentGauge")), + assertThat(registry.getConcurrentGauges().get(new MetricID(ServiceOne.class.getName() + ".concurrentGauge")), is(notNullValue())); } @@ -175,7 +211,7 @@ public void shouldAddOverriddenCounterMetricFromSuperClassAnnotation() { assertThat(methodInterceptors.size(), is(1)); assertThat(methodInterceptors.get(0), is(instanceOf(GrpcMetrics.class))); assertThat(((GrpcMetrics) methodInterceptors.get(0)).metricType(), is(MetricType.COUNTER)); - assertThat(updReg().getCounters().get(new MetricID(ServiceThree.class.getName() + ".foo")), is(notNullValue())); + assertThat(registry.getCounters().get(new MetricID(ServiceThree.class.getName() + ".foo")), is(notNullValue())); } @Test @@ -297,6 +333,38 @@ public void shouldIgnoreConcurrentGaugeMetricFromInterfaceAnnotation() { assertThat(methodInterceptors, is(emptyIterable())); } + @Test + void checkNonGrpcResourceForMetrics() { + + // Make sure the gRPC metrics observer did not accidentally discard discoveries of legitimate, + // non-gRPC metric annotations. + Counter helloWorldClassLevelMessage = registry.getCounter( + new MetricID(NonGrpcMetricAnnotatedBean.class.getName() + ".message")); + assertThat("message counter declared at class level", helloWorldClassLevelMessage, is(notNullValue())); + + SimpleTimer messageWithArgSimpleTimer = registry.getSimpleTimer( + new MetricID(NonGrpcMetricAnnotatedBean.MESSAGE_SIMPLE_TIMER)); + assertThat("messageWithArg simple timer at method level", messageWithArgSimpleTimer, is(notNullValue())); + + Counter helloWorldClassLevelMessageWithArg = registry.getCounter( + new MetricID(NonGrpcMetricAnnotatedBean.class.getName() + ".messageWithArg")); + assertThat("messageWithArg counter declared at class level", + helloWorldClassLevelMessageWithArg, + is(notNullValue())); + + // Now make sure the gRPC observer left the default interceptors in place. + long before = helloWorldClassLevelMessage.getCount(); + nonGrpcMetricAnnotatedBean.message(); + assertThat("Change in counter on method with inherited metric annotation", + helloWorldClassLevelMessage.getCount() - before, + is(1L)); + + before = helloWorldClassLevelMessageWithArg.getCount(); + nonGrpcMetricAnnotatedBean.messageWithArg("Joe"); + assertThat("Change in simple timer on method with explicit metric annotation", + helloWorldClassLevelMessageWithArg.getCount() - before, + is(1L)); + } @Grpc public static class ServiceOne @@ -313,57 +381,57 @@ public void update(ServiceDescriptor.Rules rules) { @Unary @Counted - public void counted(String request, StreamObserver response) { + public void counted(Services.TestRequest request, StreamObserver response) { } @Unary @Timed - public void timed(String request, StreamObserver response) { + public void timed(Services.TestRequest request, StreamObserver response) { } @Unary @Metered - public void metered(String request, StreamObserver response) { + public void metered(Services.TestRequest request, StreamObserver response) { } @Unary @SimplyTimed - public void simplyTimed(String request, StreamObserver response) { + public void simplyTimed(Services.TestRequest request, StreamObserver response) { } @Unary @ConcurrentGauge - public void concurrentGauge(String request, StreamObserver response) { + public void concurrentGauge(Services.TestRequest request, StreamObserver response) { } } @Grpc @SuppressWarnings("CdiManagedBeanInconsistencyInspection") - public interface ServiceTwo { + public interface ServiceTwo extends GrpcService { @Unary @Counted - void counted(String request, StreamObserver response); + void counted(Services.TestRequest request, StreamObserver response); @Unary @Timed - void timed(String request, StreamObserver response); + void timed(Services.TestRequest request, StreamObserver response); @Unary @Metered - void metered(String request, StreamObserver response); + void metered(Services.TestRequest request, StreamObserver response); @Unary @SimplyTimed - void simplyTimed(String request, StreamObserver response); + void simplyTimed(Services.TestRequest request, StreamObserver response); @Unary @ConcurrentGauge - void concurrentGauge(String request, StreamObserver response); + void concurrentGauge(Services.TestRequest request, StreamObserver response); } public static class ServiceTwoImpl - implements ServiceTwo, GrpcService { + implements ServiceTwo { @Override public void update(ServiceDescriptor.Rules rules) { rules.marshallerSupplier(new JavaMarshaller.Supplier()) @@ -375,53 +443,46 @@ public void update(ServiceDescriptor.Rules rules) { } @Override - public void counted(String request, StreamObserver response) { + public void counted(Services.TestRequest request, StreamObserver response) { } @Override - public void timed(String request, StreamObserver response) { + public void timed(Services.TestRequest request, StreamObserver response) { } @Override - public void metered(String request, StreamObserver response) { + public void metered(Services.TestRequest request, StreamObserver response) { } @Override - public void simplyTimed(String request, StreamObserver response) { + public void simplyTimed(Services.TestRequest request, StreamObserver response) { } @Override - public void concurrentGauge(String request, StreamObserver response) { + public void concurrentGauge(Services.TestRequest request, StreamObserver response) { } } + @Typed(ServiceThree.class) // disambiguate from ServiceOne for CDI public static class ServiceThree extends ServiceOne { @Override @Counted(name = "foo") - public void counted(String request, StreamObserver response) { + public void counted(Services.TestRequest request, StreamObserver response) { super.counted(request, response); } @Override - public void timed(String request, StreamObserver response) { + public void timed(Services.TestRequest request, StreamObserver response) { super.timed(request, response); } } - private static MetricRegistry updReg() { - MyMetricsCdiExtension.registerGrpcMetrics(); - return registry; - } - - /** - * Used only for very limited testing purposes. - */ - @Deprecated - private static class MyMetricsCdiExtension extends io.helidon.microprofile.metrics.MetricsCdiExtension { + static class ServiceTwoProducer { - static void registerGrpcMetrics() { - MyMetricsCdiExtension.registerMetricsForAnnotatedSitesFromGrpcTest(); + @Produces + public ServiceTwo create() { + return new ServiceTwoImpl(); } } } diff --git a/tests/integration/mp-grpc/src/test/java/io/helidon/microprofile/grpc/metrics/NonGrpcMetricAnnotatedBean.java b/tests/integration/mp-grpc/src/test/java/io/helidon/microprofile/grpc/metrics/NonGrpcMetricAnnotatedBean.java new file mode 100644 index 00000000000..e3098a1faa9 --- /dev/null +++ b/tests/integration/mp-grpc/src/test/java/io/helidon/microprofile/grpc/metrics/NonGrpcMetricAnnotatedBean.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 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 jakarta.enterprise.context.ApplicationScoped; +import org.eclipse.microprofile.metrics.annotation.Counted; +import org.eclipse.microprofile.metrics.annotation.SimplyTimed; + +@ApplicationScoped +@Counted +public class NonGrpcMetricAnnotatedBean { + + static final String MESSAGE_SIMPLE_TIMER = "messageSimpleTimer"; + + // Do not add other metrics annotations to this method! + public String message() { + return "Hello World"; + } + + @SimplyTimed(name = MESSAGE_SIMPLE_TIMER, absolute = true) + public String messageWithArg(String input){ + return "Hello World, " + input; + } +} diff --git a/tests/integration/mp-grpc/src/test/java/io/helidon/microprofile/grpc/server/GrpcServerTestProducer.java b/tests/integration/mp-grpc/src/test/java/io/helidon/microprofile/grpc/server/GrpcServerTestProducer.java new file mode 100644 index 00000000000..6a89183b4be --- /dev/null +++ b/tests/integration/mp-grpc/src/test/java/io/helidon/microprofile/grpc/server/GrpcServerTestProducer.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022 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.server; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import io.helidon.grpc.server.GrpcServer; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; + +@ApplicationScoped +public class GrpcServerTestProducer { + + +// GrpcServer serverFactory() throws ExecutionException, InterruptedException, TimeoutException { +// return AnnotatedServiceTest.createServer(); +// } +} diff --git a/tests/integration/mp-grpc/src/test/java/io/helidon/microprofile/grpc/server/InterceptorsTest.java b/tests/integration/mp-grpc/src/test/java/io/helidon/microprofile/grpc/server/InterceptorsTest.java index 2c45c7e01ac..737b953b921 100644 --- a/tests/integration/mp-grpc/src/test/java/io/helidon/microprofile/grpc/server/InterceptorsTest.java +++ b/tests/integration/mp-grpc/src/test/java/io/helidon/microprofile/grpc/server/InterceptorsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019,2020 Oracle and/or its affiliates. + * Copyright (c) 2019, 2022 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. @@ -27,6 +27,7 @@ import io.helidon.microprofile.grpc.core.GrpcInterceptorBinding; import io.helidon.microprofile.grpc.core.GrpcInterceptors; import io.helidon.microprofile.grpc.core.Unary; +import io.helidon.microprofile.tests.junit5.HelidonTest; import io.grpc.Metadata; import io.grpc.ServerCall; @@ -34,11 +35,9 @@ import io.grpc.ServerInterceptor; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.spi.BeanManager; -import org.jboss.weld.junit5.WeldInitiator; -import org.jboss.weld.junit5.WeldJunit5Extension; -import org.jboss.weld.junit5.WeldSetup; +import jakarta.enterprise.inject.spi.CDI; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; @@ -49,25 +48,19 @@ /** * Functional test for server side interceptors using annotations. */ -@ExtendWith(WeldJunit5Extension.class) +@HelidonTest @SuppressWarnings("unchecked") public class InterceptorsTest { - @WeldSetup - public WeldInitiator weld = WeldInitiator.of(ServerInterceptorOne.class, - ServerInterceptorTwo.class, - ServerInterceptorThree.class, - ServerInterceptorFour.class, - InterceptedServiceOne.class, - InterceptedServiceTwo.class, - InterceptedServiceThree.class, - InterceptedServiceFour.class, - InterceptedServiceFive.class, - InterceptedServiceSix.class); + private CDI cdi; + @BeforeEach + void setup() { + cdi = CDI.current(); + } @Test public void shouldDiscoverServiceInterceptor() { - BeanManager beanManager = weld.getBeanManager(); + BeanManager beanManager = cdi.getBeanManager(); GrpcServiceBuilder builder = GrpcServiceBuilder.create(InterceptedServiceOne.class, beanManager); ServiceDescriptor descriptor = builder.build(); @@ -81,7 +74,7 @@ public void shouldDiscoverServiceInterceptor() { @Test public void shouldDiscoverMethodInterceptor() { - BeanManager beanManager = weld.getBeanManager(); + BeanManager beanManager = cdi.getBeanManager(); GrpcServiceBuilder builder = GrpcServiceBuilder.create(InterceptedServiceTwo.class, beanManager); ServiceDescriptor descriptor = builder.build(); @@ -99,7 +92,7 @@ public void shouldDiscoverMethodInterceptor() { @Test public void shouldDiscoverServiceAndMethodInterceptor() { - BeanManager beanManager = weld.getBeanManager(); + BeanManager beanManager = cdi.getBeanManager(); GrpcServiceBuilder builder = GrpcServiceBuilder.create(InterceptedServiceThree.class, beanManager); ServiceDescriptor descriptor = builder.build(); @@ -122,7 +115,7 @@ public void shouldDiscoverServiceAndMethodInterceptor() { @Test public void shouldDiscoverServiceInterceptorBasedOnAnnotationMemberValue() { - BeanManager beanManager = weld.getBeanManager(); + BeanManager beanManager = cdi.getBeanManager(); GrpcServiceBuilder builder = GrpcServiceBuilder.create(InterceptedServiceFour.class, beanManager); ServiceDescriptor descriptor = builder.build(); @@ -136,7 +129,7 @@ public void shouldDiscoverServiceInterceptorBasedOnAnnotationMemberValue() { @Test public void shouldUseSpecificServiceInterceptorBean() { - BeanManager beanManager = weld.getBeanManager(); + BeanManager beanManager = cdi.getBeanManager(); GrpcServiceBuilder builder = GrpcServiceBuilder.create(InterceptedServiceFive.class, beanManager); ServiceDescriptor descriptor = builder.build(); @@ -156,7 +149,7 @@ public void shouldUseSpecificServiceInterceptorBean() { @Test public void shouldUseSpecificMethodInterceptorBean() { - BeanManager beanManager = weld.getBeanManager(); + BeanManager beanManager = cdi.getBeanManager(); GrpcServiceBuilder builder = GrpcServiceBuilder.create(InterceptedServiceSix.class, beanManager); ServiceDescriptor descriptor = builder.build(); @@ -177,7 +170,7 @@ public void shouldUseSpecificMethodInterceptorBean() { @Test public void shouldUseSpecificNonCdiServiceInterceptor() { - BeanManager beanManager = weld.getBeanManager(); + BeanManager beanManager = cdi.getBeanManager(); GrpcServiceBuilder builder = GrpcServiceBuilder.create(InterceptedServiceSeven.class, beanManager); ServiceDescriptor descriptor = builder.build(); @@ -192,7 +185,7 @@ public void shouldUseSpecificNonCdiServiceInterceptor() { @Test public void shouldUseSpecificNonCdiMethodInterceptor() { - BeanManager beanManager = weld.getBeanManager(); + BeanManager beanManager = cdi.getBeanManager(); GrpcServiceBuilder builder = GrpcServiceBuilder.create(InterceptedServiceEight.class, beanManager); ServiceDescriptor descriptor = builder.build(); diff --git a/tests/integration/mp-grpc/src/test/resources/META-INF/microprofile-config.properties b/tests/integration/mp-grpc/src/test/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000000..da7e8dad824 --- /dev/null +++ b/tests/integration/mp-grpc/src/test/resources/META-INF/microprofile-config.properties @@ -0,0 +1,17 @@ +# +# Copyright (c) 2022 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. +# + +grpc.port=0 \ No newline at end of file