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