JAX-RS metrics

This commit is contained in:
Jan Martiska
2020-01-17 10:20:37 +01:00
parent 12351babb1
commit 23daee4aff
9 changed files with 410 additions and 1 deletions

View File

@@ -34,6 +34,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-server-common</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-undertow-spi</artifactId>
</dependency>
</dependencies>
<build>

View File

@@ -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<ResteasyJaxrsProviderBuildItem> jaxRsProviders,
BuildProducer<FilterBuildItem> 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<DotName> autoInjectAnnotationNames, ClassInfo clazz) {
for (DotName name : autoInjectAnnotationNames) {
List<AnnotationInstance> instances = clazz.annotations().get(name);

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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<PathSegment> 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<String> async() {
return CompletableFuture.supplyAsync(() -> "Hello");
}
}

View File

@@ -39,6 +39,11 @@
<groupId>org.eclipse.microprofile.metrics</groupId>
<artifactId>microprofile-metrics-api</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>

View File

@@ -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);
}
}

View File

@@ -0,0 +1,74 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>quarkus-tck-microprofile-metrics-parent</artifactId>
<groupId>io.quarkus</groupId>
<version>999-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>quarkus-tck-microprofile-metrics-optional</artifactId>
<name>Quarkus - TCK - MicroProfile Metrics Optional tests</name>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<!-- Disable quarkus optimization -->
<quarkus.arc.remove-unused-beans>false</quarkus.arc.remove-unused-beans>
<quarkus.resteasy.metrics.enabled>true</quarkus.resteasy.metrics.enabled>
<context.root/>
</systemPropertyVariables>
<!-- This workaround allows us to run a single test using
the "test" system property -->
<!-- https://issues.apache.org/jira/browse/SUREFIRE-569 -->
<dependenciesToScan>
<dependency>org.eclipse.microprofile.metrics:microprofile-metrics-optional-tck</dependency>
</dependenciesToScan>
<environmentVariables>
<MP_METRICS_TAGS>tier=integration</MP_METRICS_TAGS>
</environmentVariables>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arquillian</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-metrics</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.metrics</groupId>
<artifactId>microprofile-metrics-optional-tck</artifactId>
<version>${microprofile-metrics-api.version}</version>
<exclusions>
<exclusion>
<groupId>org.jboss.shrinkwrap.resolver</groupId>
<artifactId>shrinkwrap-resolver-impl-maven</artifactId>
</exclusion>
<exclusion>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.xml.bind</groupId>
<artifactId>jboss-jaxb-api_2.3_spec</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -16,6 +16,7 @@
<modules>
<module>api</module>
<module>rest</module>
<module>optional</module>
</modules>
</project>