From 23daee4aff75463d045c343f38c1ce5feab74173 Mon Sep 17 00:00:00 2001 From: Jan Martiska Date: Fri, 17 Jan 2020 10:20:37 +0100 Subject: [PATCH] JAX-RS metrics --- .../resteasy-server-common/deployment/pom.xml | 4 + .../ResteasyServerCommonProcessor.java | 39 ++++++ .../deployment/DevModeMetricsTest.java | 2 +- .../metrics/jaxrs/JaxRsMetricsTestCase.java | 127 ++++++++++++++++++ .../metrics/jaxrs/MetricsResource.java | 58 ++++++++ extensions/smallrye-metrics/runtime/pom.xml | 5 + .../runtime/QuarkusJaxRsMetricsFilter.java | 101 ++++++++++++++ tcks/microprofile-metrics/optional/pom.xml | 74 ++++++++++ tcks/microprofile-metrics/pom.xml | 1 + 9 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/jaxrs/JaxRsMetricsTestCase.java create mode 100644 extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/jaxrs/MetricsResource.java create mode 100644 extensions/smallrye-metrics/runtime/src/main/java/io/quarkus/smallrye/metrics/runtime/QuarkusJaxRsMetricsFilter.java create mode 100644 tcks/microprofile-metrics/optional/pom.xml diff --git a/extensions/resteasy-server-common/deployment/pom.xml b/extensions/resteasy-server-common/deployment/pom.xml index eb4ca6cfd..fcd63e788 100644 --- a/extensions/resteasy-server-common/deployment/pom.xml +++ b/extensions/resteasy-server-common/deployment/pom.xml @@ -34,6 +34,10 @@ io.quarkus quarkus-resteasy-server-common + + io.quarkus + quarkus-undertow-spi + diff --git a/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java b/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java index f90bb2c48..0c3ce5899 100755 --- a/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java +++ b/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java @@ -16,6 +16,8 @@ import java.util.Set; import java.util.function.BiFunction; import java.util.stream.Collectors; +import javax.servlet.DispatcherType; + import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.AnnotationTarget.Kind; @@ -51,6 +53,7 @@ import io.quarkus.arc.processor.AnnotationsTransformer; import io.quarkus.arc.processor.BuiltinScope; import io.quarkus.arc.processor.DotNames; import io.quarkus.arc.processor.Transformation; +import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem; @@ -66,11 +69,13 @@ import io.quarkus.resteasy.common.deployment.JaxrsProvidersToRegisterBuildItem; import io.quarkus.resteasy.common.deployment.ResteasyCommonProcessor.ResteasyCommonConfig; import io.quarkus.resteasy.common.deployment.ResteasyDotNames; import io.quarkus.resteasy.common.runtime.QuarkusInjectorFactory; +import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; import io.quarkus.resteasy.server.common.spi.AdditionalJaxRsResourceDefiningAnnotationBuildItem; import io.quarkus.resteasy.server.common.spi.AdditionalJaxRsResourceMethodAnnotationsBuildItem; import io.quarkus.resteasy.server.common.spi.AdditionalJaxRsResourceMethodParamAnnotations; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; +import io.quarkus.undertow.deployment.FilterBuildItem; /** * Processor that builds the RESTEasy server configuration. @@ -135,6 +140,13 @@ public class ResteasyServerCommonProcessor { */ @ConfigItem(defaultValue = "/") String path; + + /** + * Whether or not JAX-RS metrics should be enabled if the Metrics capability is present and Vert.x is being used. + */ + @ConfigItem(name = "metrics.enabled", defaultValue = "false") + public boolean metricsEnabled; + } @BuildStep @@ -412,6 +424,33 @@ public class ResteasyServerCommonProcessor { BuiltinScope.SINGLETON.getName())); } + @BuildStep + void enableMetrics(ResteasyConfig buildConfig, + BuildProducer jaxRsProviders, + BuildProducer servletFilters, + Capabilities capabilities) { + if (buildConfig.metricsEnabled && capabilities.isCapabilityPresent(Capabilities.METRICS)) { + if (capabilities.isCapabilityPresent(Capabilities.SERVLET)) { + // if running with servlet, use the MetricsFilter implementation from SmallRye + jaxRsProviders.produce( + new ResteasyJaxrsProviderBuildItem("io.smallrye.metrics.jaxrs.JaxRsMetricsFilter")); + servletFilters.produce( + FilterBuildItem.builder("metricsFilter", "io.smallrye.metrics.jaxrs.JaxRsMetricsServletFilter") + .setAsyncSupported(true) + .addFilterUrlMapping("*", DispatcherType.FORWARD) + .addFilterUrlMapping("*", DispatcherType.INCLUDE) + .addFilterUrlMapping("*", DispatcherType.REQUEST) + .addFilterUrlMapping("*", DispatcherType.ASYNC) + .addFilterUrlMapping("*", DispatcherType.ERROR) + .build()); + } else { + // if running with vert.x, use the MetricsFilter implementation from Quarkus codebase + jaxRsProviders.produce( + new ResteasyJaxrsProviderBuildItem("io.quarkus.smallrye.metrics.runtime.QuarkusJaxRsMetricsFilter")); + } + } + } + private boolean hasAutoInjectAnnotation(Set autoInjectAnnotationNames, ClassInfo clazz) { for (DotName name : autoInjectAnnotationNames) { List instances = clazz.annotations().get(name); diff --git a/extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/deployment/DevModeMetricsTest.java b/extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/deployment/DevModeMetricsTest.java index 04c37ccdc..cf021b6e5 100644 --- a/extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/deployment/DevModeMetricsTest.java +++ b/extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/deployment/DevModeMetricsTest.java @@ -1,7 +1,7 @@ package io.quarkus.smallrye.metrics.deployment; import static io.restassured.RestAssured.when; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.equalTo; import javax.inject.Inject; import javax.ws.rs.GET; diff --git a/extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/jaxrs/JaxRsMetricsTestCase.java b/extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/jaxrs/JaxRsMetricsTestCase.java new file mode 100644 index 000000000..f454e75e8 --- /dev/null +++ b/extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/jaxrs/JaxRsMetricsTestCase.java @@ -0,0 +1,127 @@ +package io.quarkus.smallrye.metrics.jaxrs; + +import static io.restassured.RestAssured.when; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.inject.Inject; + +import org.eclipse.microprofile.metrics.MetricRegistry; +import org.eclipse.microprofile.metrics.SimpleTimer; +import org.eclipse.microprofile.metrics.Tag; +import org.eclipse.microprofile.metrics.annotation.RegistryType; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class JaxRsMetricsTestCase { + + final String METRIC_RESOURCE_CLASS_NAME = MetricsResource.class.getName(); + + @RegisterExtension + static QuarkusUnitTest TEST = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource(new StringAsset("quarkus.resteasy.metrics.enabled=true"), + "application.properties") + .addClasses(MetricsResource.class)); + + @Inject + @RegistryType(type = MetricRegistry.Type.BASE) + MetricRegistry metricRegistry; + + @Test + public void testBasic() { + when() + .get("/hello/joe") + .then() + .statusCode(200); + SimpleTimer metric = metricRegistry.simpleTimer("REST.request", + new Tag("class", METRIC_RESOURCE_CLASS_NAME), + new Tag("method", "hello_java.lang.String")); + assertEquals(1, metric.getCount()); + assertTrue(metric.getElapsedTime().toNanos() > 0); + } + + @Test + public void testMethodReturningServerError() throws InterruptedException { + when() + .get("/error") + .then() + .statusCode(500); + SimpleTimer metric = metricRegistry.simpleTimer("REST.request", + new Tag("class", METRIC_RESOURCE_CLASS_NAME), + new Tag("method", "error")); + assertEquals(1, metric.getCount()); + assertTrue(metric.getElapsedTime().toNanos() > 0); + } + + @Test + public void testMethodThrowingException() { + when() + .get("/exception") + .then() + .statusCode(500); + SimpleTimer metric = metricRegistry.simpleTimer("REST.request", + new Tag("class", METRIC_RESOURCE_CLASS_NAME), + new Tag("method", "exception")); + assertEquals(1, metric.getCount()); + assertTrue(metric.getElapsedTime().toNanos() > 0); + } + + @Test + public void testMethodTakingList() { + when() + .get("/a/b/c/list") + .then() + .statusCode(200); + SimpleTimer metric = metricRegistry.simpleTimer("REST.request", + new Tag("class", METRIC_RESOURCE_CLASS_NAME), + new Tag("method", "list_java.util.List")); + assertEquals(1, metric.getCount()); + assertTrue(metric.getElapsedTime().toNanos() > 0); + } + + @Test + public void testMethodTakingArray() { + when() + .get("/a/b/c/array") + .then() + .statusCode(200); + SimpleTimer metric = metricRegistry.simpleTimer("REST.request", + new Tag("class", METRIC_RESOURCE_CLASS_NAME), + new Tag("method", "array_javax.ws.rs.core.PathSegment[]")); + assertEquals(1, metric.getCount()); + assertTrue(metric.getElapsedTime().toNanos() > 0); + } + + @Test + public void testMethodTakingVarargs() { + when() + .get("/a/b/c/varargs") + .then() + .statusCode(200); + SimpleTimer metric = metricRegistry.simpleTimer("REST.request", + new Tag("class", METRIC_RESOURCE_CLASS_NAME), + new Tag("method", "varargs_javax.ws.rs.core.PathSegment[]")); + assertEquals(1, metric.getCount()); + assertTrue(metric.getElapsedTime().toNanos() > 0); + } + + @Test + public void testAsyncMethod() { + when() + .get("/async") + .then() + .statusCode(200); + SimpleTimer metric = metricRegistry.simpleTimer("REST.request", + new Tag("class", METRIC_RESOURCE_CLASS_NAME), + new Tag("method", "async")); + assertEquals(1, metric.getCount()); + assertTrue(metric.getElapsedTime().toNanos() > 0); + } + +} diff --git a/extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/jaxrs/MetricsResource.java b/extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/jaxrs/MetricsResource.java new file mode 100644 index 000000000..0b7d47306 --- /dev/null +++ b/extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/jaxrs/MetricsResource.java @@ -0,0 +1,58 @@ +package io.quarkus.smallrye.metrics.jaxrs; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.core.PathSegment; +import javax.ws.rs.core.Response; + +@Path("/") +public class MetricsResource { + + @Path("/hello/{name}") + @GET + public String hello(@PathParam("name") String name) { + return "hello " + name; + } + + @Path("/error") + @GET + public Response error() { + return Response.serverError().build(); + } + + @Path("/exception") + @GET + public Long exception() { + throw new RuntimeException("!!!"); + } + + @GET + @Path("{segment}/{other}/{segment}/list") + public Response list(@PathParam("segment") List segments) { + return Response.ok().build(); + } + + @GET + @Path("{segment}/{other}/{segment}/array") + public Response array(@PathParam("segment") PathSegment[] segments) { + return Response.ok().build(); + } + + @GET + @Path("{segment}/{other}/{segment}/varargs") + public Response varargs(@PathParam("segment") PathSegment... segments) { + return Response.ok().build(); + } + + @Path("/async") + @GET + public CompletionStage async() { + return CompletableFuture.supplyAsync(() -> "Hello"); + } + +} diff --git a/extensions/smallrye-metrics/runtime/pom.xml b/extensions/smallrye-metrics/runtime/pom.xml index b3da3907b..8837f947e 100644 --- a/extensions/smallrye-metrics/runtime/pom.xml +++ b/extensions/smallrye-metrics/runtime/pom.xml @@ -39,6 +39,11 @@ org.eclipse.microprofile.metrics microprofile-metrics-api + + io.quarkus + quarkus-resteasy + true + diff --git a/extensions/smallrye-metrics/runtime/src/main/java/io/quarkus/smallrye/metrics/runtime/QuarkusJaxRsMetricsFilter.java b/extensions/smallrye-metrics/runtime/src/main/java/io/quarkus/smallrye/metrics/runtime/QuarkusJaxRsMetricsFilter.java new file mode 100644 index 000000000..20c397657 --- /dev/null +++ b/extensions/smallrye-metrics/runtime/src/main/java/io/quarkus/smallrye/metrics/runtime/QuarkusJaxRsMetricsFilter.java @@ -0,0 +1,101 @@ +/* + * Copyright 2019 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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.quarkus.smallrye.metrics.runtime; + +import java.lang.reflect.Method; +import java.time.Duration; +import java.util.Arrays; +import java.util.stream.Collectors; + +import javax.enterprise.inject.spi.CDI; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.Context; + +import org.eclipse.microprofile.metrics.Metadata; +import org.eclipse.microprofile.metrics.MetricID; +import org.eclipse.microprofile.metrics.MetricRegistry; +import org.eclipse.microprofile.metrics.MetricUnits; +import org.eclipse.microprofile.metrics.Tag; + +import io.quarkus.vertx.http.runtime.CurrentVertxRequest; +import io.smallrye.metrics.MetricRegistries; +import io.vertx.ext.web.RoutingContext; + +/** + * A JAX-RS filter that computes the REST.request metrics from REST traffic over time. + * This one depends on Vert.x to be able to hook into response even in cases when the request ended with an unmapped exception. + */ +public class QuarkusJaxRsMetricsFilter implements ContainerRequestFilter { + + @Context + ResourceInfo resourceInfo; + + @Override + public void filter(final ContainerRequestContext requestContext) { + Long start = System.nanoTime(); + final Class resourceClass = resourceInfo.getResourceClass(); + final Method resourceMethod = resourceInfo.getResourceMethod(); + /* + * The reason for using a Vert.x handler instead of ContainerResponseFilter is that + * RESTEasy does not call the response filter for requests that ended up with an unmapped exception. + * This way we can capture these responses as well and update the metrics accordingly. + */ + RoutingContext routingContext = CDI.current().select(CurrentVertxRequest.class).get().getCurrent(); + routingContext.addBodyEndHandler( + event -> finishRequest(start, resourceClass, resourceMethod)); + } + + private void finishRequest(Long start, Class resourceClass, Method resourceMethod) { + long value = System.nanoTime() - start; + MetricID metricID = getMetricID(resourceClass, resourceMethod); + + MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.BASE); + if (!registry.getMetadata().containsKey(metricID.getName())) { + // if no metric with this name exists yet, register it + Metadata metadata = Metadata.builder() + .withName(metricID.getName()) + .withDescription( + "The number of invocations and total response time of this RESTful resource method since the start of the server.") + .withUnit(MetricUnits.NANOSECONDS) + .build(); + registry.simpleTimer(metadata, metricID.getTagsAsArray()); + } + registry.simpleTimer(metricID.getName(), metricID.getTagsAsArray()) + .update(Duration.ofNanos(value)); + } + + private MetricID getMetricID(Class resourceClass, Method resourceMethod) { + Tag classTag = new Tag("class", resourceClass.getName()); + String methodName = resourceMethod.getName(); + String encodedParameterNames = Arrays.stream(resourceMethod.getParameterTypes()) + .map(clazz -> { + if (clazz.isArray()) { + return clazz.getComponentType().getName() + "[]"; + } else { + return clazz.getName(); + } + }) + .collect(Collectors.joining("_")); + String methodTagValue = encodedParameterNames.isEmpty() ? methodName : methodName + "_" + encodedParameterNames; + Tag methodTag = new Tag("method", methodTagValue); + return new MetricID("REST.request", classTag, methodTag); + } + +} diff --git a/tcks/microprofile-metrics/optional/pom.xml b/tcks/microprofile-metrics/optional/pom.xml new file mode 100644 index 000000000..262f3d4b5 --- /dev/null +++ b/tcks/microprofile-metrics/optional/pom.xml @@ -0,0 +1,74 @@ + + + quarkus-tck-microprofile-metrics-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-tck-microprofile-metrics-optional + Quarkus - TCK - MicroProfile Metrics Optional tests + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + false + true + + + + + + org.eclipse.microprofile.metrics:microprofile-metrics-optional-tck + + + tier=integration + + + + + + + + io.quarkus + quarkus-arquillian + + + io.quarkus + quarkus-smallrye-metrics + + + org.eclipse.microprofile.metrics + microprofile-metrics-optional-tck + ${microprofile-metrics-api.version} + + + org.jboss.shrinkwrap.resolver + shrinkwrap-resolver-impl-maven + + + jakarta.xml.bind + jakarta.xml.bind-api + + + + + org.jboss.spec.javax.xml.bind + jboss-jaxb-api_2.3_spec + + + io.quarkus + quarkus-resteasy + + + + diff --git a/tcks/microprofile-metrics/pom.xml b/tcks/microprofile-metrics/pom.xml index 1bdb43ef3..bf26c0ccc 100644 --- a/tcks/microprofile-metrics/pom.xml +++ b/tcks/microprofile-metrics/pom.xml @@ -16,6 +16,7 @@ api rest + optional