diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/UriTagExemplarTest.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/UriTagExemplarTest.java new file mode 100644 index 0000000000000..2a4c18d5b6b18 --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/UriTagExemplarTest.java @@ -0,0 +1,154 @@ +package io.quarkus.micrometer.deployment.binder; + +import static io.restassured.RestAssured.when; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.quarkus.micrometer.test.HelloResource; +import io.quarkus.micrometer.test.PingPongResource; +import io.quarkus.micrometer.test.ServletEndpoint; +import io.quarkus.micrometer.test.Util; +import io.quarkus.micrometer.test.VertxWebEndpoint; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class UriTagExemplarTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("test-logging.properties") + .overrideConfigKey("quarkus.micrometer.binder-enabled-default", "false") + .overrideConfigKey("quarkus.micrometer.binder.http-client.enabled", "true") + .overrideConfigKey("quarkus.micrometer.binder.http-server.enabled", "true") + .overrideConfigKey("quarkus.micrometer.binder.http-server.match-patterns", "/one=/two") + .overrideConfigKey("quarkus.micrometer.binder.http-server.ignore-patterns", "/two") + .overrideConfigKey("quarkus.micrometer.binder.vertx.enabled", "true") + .overrideConfigKey("pingpong/mp-rest/url", "${test.url}") + .overrideConfigKey("quarkus.redis.devservices.enabled", "false") + .withApplicationRoot((jar) -> jar + .addClasses(Util.class, + PingPongResource.class, + PingPongResource.PingPongRestClient.class, + ServletEndpoint.class, + VertxWebEndpoint.class, + HelloResource.class)); + + static SimpleMeterRegistry registry = new SimpleMeterRegistry(); + + @BeforeAll + static void setRegistry() { + registry = new SimpleMeterRegistry(); + Metrics.addRegistry(registry); + } + + @AfterAll() + static void removeRegistry() { + Metrics.removeRegistry(registry); + } + + @Test + public void testRequestUris() throws Exception { + RestAssured.basePath = "/"; + + // Server paths (templated) + when().get("/one").then().statusCode(200); + when().get("/two").then().statusCode(200); + when().get("/vertx/item/123").then().statusCode(200); + when().get("/vertx/item/1/123").then().statusCode(200); + when().get("/servlet/12345").then().statusCode(200); + + // Server GET vs. HEAD methods -- templated + when().get("/hello/one").then().statusCode(200); + when().get("/hello/two").then().statusCode(200); + when().head("/hello/three").then().statusCode(200); + when().head("/hello/four").then().statusCode(200); + when().get("/vertx/echo/thing1").then().statusCode(200); + when().get("/vertx/echo/thing2").then().statusCode(200); + when().head("/vertx/echo/thing3").then().statusCode(200); + when().head("/vertx/echo/thing4").then().statusCode(200); + + // Server -> Rest client -> Server (templated) + when().get("/ping/one").then().statusCode(200); + when().get("/ping/two").then().statusCode(200); + when().get("/ping/three").then().statusCode(200); + when().get("/ping/400").then().statusCode(200); + when().get("/ping/500").then().statusCode(200); + when().get("/async-ping/one").then().statusCode(200); + when().get("/async-ping/two").then().statusCode(200); + when().get("/async-ping/three").then().statusCode(200); + + // Try to let metrics gathering finish. + // Looking for server request timers: /vertx/item/{id}, /vertx/item/{id}/{sub}, /servlet/, + // /ping/{message}, /async-ping/{message}, /pong/{message}, and 2 of both /hello/{message} and /vertx/echo/{msg} + // Looking for client request: /pong/{message} + Util.waitForMeters(registry.find("http.server.requests").timers(), 10); + Util.waitForMeters(registry.find("http.client.requests").timers(), 1); + + System.out.println("Server paths\n" + Util.listMeters(registry, "http.server.requests")); + System.out.println("Client paths\n" + Util.listMeters(registry, "http.client.requests")); + + // /one should map to /two, which is ignored. Neither should exist w/ timers + Assertions.assertEquals(0, registry.find("http.server.requests").tag("uri", "/one").timers().size(), + Util.foundServerRequests(registry, "/one is mapped to /two, which should be ignored.")); + Assertions.assertEquals(0, registry.find("http.server.requests").tag("uri", "/two").timers().size(), + Util.foundServerRequests(registry, "/two should be ignored.")); + + // URIs for server: /vertx/item/{id}, /vertx/item/{id}/{sub}, /servlet/ + Assertions.assertEquals(1, registry.find("http.server.requests").tag("uri", "/vertx/item/{id}").timers().size(), + Util.foundServerRequests(registry, + "Vert.x Web template path (/vertx/item/:id) should be detected/translated to /vertx/item/{id}.")); + Assertions.assertEquals(1, registry.find("http.server.requests").tag("uri", "/vertx/item/{id}/{sub}").timers().size(), + Util.foundServerRequests(registry, + "Vert.x Web template path (/vertx/item/:id/:sub) should be detected/translated to /vertx/item/{id}/{sub}.")); + Assertions.assertEquals(1, registry.find("http.server.requests").tag("uri", "/servlet").timers().size(), + Util.foundServerRequests(registry, "Servlet path (/servlet) should be used for servlet")); + + // GET and HEAD are two different methods, there should be two timers for each of these URI tag values + Assertions.assertEquals(2, registry.find("http.server.requests").tag("uri", "/hello/{message}").timers().size(), + Util.foundServerRequests(registry, "/hello/{message} should have two timers (GET and HEAD).")); + Assertions.assertEquals(2, registry.find("http.server.requests").tag("uri", "/vertx/echo/{msg}").timers().size(), + Util.foundServerRequests(registry, "/vertx/echo/{msg} should have two timers (GET and HEAD).")); + + // URIs to trigger REST request: /ping/{message}, /async-ping/{message}, + Assertions.assertEquals(1, registry.find("http.server.requests").tag("uri", "/ping/{message}").timers().size(), + Util.foundServerRequests(registry, "/ping/{message} should be returned by JAX-RS.")); + Assertions.assertEquals(1, registry.find("http.server.requests").tag("uri", "/async-ping/{message}").timers().size(), + Util.foundServerRequests(registry, "/async-ping/{message} should be returned by JAX-RS.")); + + // Server response for /pong/{message} + Assertions.assertEquals(1, + registry.find("http.server.requests").tag("uri", "/pong/{message}").tag("outcome", "SUCCESS").timers().size(), + Util.foundServerRequests(registry, "/pong/{message} with tag outcome=SUCCESS should be returned by JAX-RS.")); + Assertions.assertEquals(1, + registry.find("http.server.requests").tag("uri", "/pong/{message}").tag("outcome", "CLIENT_ERROR").timers() + .size(), + Util.foundServerRequests(registry, + "/pong/{message} with tag outcome=CLIENT_ERROR should be returned by JAX-RS.")); + Assertions.assertEquals(1, + registry.find("http.server.requests").tag("uri", "/pong/{message}").tag("outcome", "SERVER_ERROR").timers() + .size(), + Util.foundServerRequests(registry, + "/pong/{message} with tag outcome=SERVER_ERROR should be returned by JAX-RS.")); + + // URI for outbound client request: /pong/{message} + Assertions.assertEquals(1, + registry.find("http.client.requests").tag("uri", "/pong/{message}").tag("outcome", "SUCCESS").timers().size(), + Util.foundClientRequests(registry, + "/pong/{message} with tag outcome=SUCCESS should be returned by Rest client.")); + Assertions.assertEquals(1, + registry.find("http.client.requests").tag("uri", "/pong/{message}").tag("outcome", "CLIENT_ERROR").timers() + .size(), + Util.foundClientRequests(registry, + "/pong/{message} with tag outcome=CLIENT_ERROR should be returned by Rest client.")); + Assertions.assertEquals(1, + registry.find("http.client.requests").tag("uri", "/pong/{message}").tag("outcome", "SERVER_ERROR").timers() + .size(), + Util.foundClientRequests(registry, + "/pong/{message} with tag outcome=SERVER_ERROR should be returned by Rest client.")); + } +} diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpServerMetrics.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpServerMetrics.java index a7993195d294a..657a776969b91 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpServerMetrics.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxHttpServerMetrics.java @@ -4,6 +4,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.LongAdder; +import java.util.function.Function; import org.jboss.logging.Logger; @@ -15,11 +16,14 @@ import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.binder.http.Outcome; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.micrometer.runtime.HttpServerMetricsTagsContributor; import io.quarkus.micrometer.runtime.binder.HttpBinderConfiguration; import io.quarkus.micrometer.runtime.binder.HttpCommonTags; +import io.quarkus.opentelemetry.runtime.QuarkusContextStorage; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.ServerWebSocket; @@ -164,12 +168,14 @@ public void requestReset(HttpRequestMetric requestMetric) { if (path != null) { Timer.Sample sample = requestMetric.getSample(); - sample.stop(requestsTimer - .withTags(Tags.of( - VertxMetricsTags.method(requestMetric.request().method()), - HttpCommonTags.uri(path, requestMetric.initialPath, 0), - Outcome.CLIENT_ERROR.asTag(), - HttpCommonTags.STATUS_RESET))); + executeInContext(sample::stop, + requestsTimer + .withTags(Tags.of( + VertxMetricsTags.method(requestMetric.request().method()), + HttpCommonTags.uri(path, requestMetric.initialPath, 0), + Outcome.CLIENT_ERROR.asTag(), + HttpCommonTags.STATUS_RESET)), + requestMetric.request().context()); } requestMetric.requestEnded(); } @@ -207,11 +213,43 @@ public void responseEnd(HttpRequestMetric requestMetric, HttpResponse response, } } - sample.stop(requestsTimer.withTags(allTags)); + executeInContext(sample::stop, requestsTimer.withTags(allTags), requestMetric.request().context()); } requestMetric.requestEnded(); } + /** + * Called when an HTTP server response has ended. + * Makes sure exemplars are produced because they have an OTel context. + * + * @param methodReference Ex: Sample stop method reference + * @param parameter The parameter to pass to the method + * @param requestContext The request context + * @param
The parameter type is a type of metric, ex: Timer
+ * @param R executeInContext(Function methodReference, P parameter, io.vertx.core.Context requestContext) {
+ if (requestContext == null) {
+ return methodReference.apply(parameter);
+ }
+
+ Context newContext = QuarkusContextStorage.getContext(requestContext);
+
+ if (newContext == null) {
+ return methodReference.apply(parameter);
+ }
+
+ io.opentelemetry.context.Context oldContext = QuarkusContextStorage.INSTANCE.current();
+ try (Scope scope = QuarkusContextStorage.INSTANCE.attach(newContext)) {
+ return methodReference.apply(parameter);
+ } finally {
+ if (oldContext != null) {
+ QuarkusContextStorage.INSTANCE.attach(oldContext);
+ }
+ }
+ }
+
/**
* Called when a server web socket connects.
*
diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java
index bdbb88e786c94..1f9d8ab6704ef 100644
--- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java
+++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/instrumentation/InstrumentationProcessor.java
@@ -68,6 +68,16 @@ public boolean getAsBoolean() {
}
}
+ static class VertxHttpAvailable implements BooleanSupplier {
+ private static final boolean IS_VERTX_HTTP_AVAILABLE = isClassPresentAtRuntime(
+ "io.quarkus.vertx.http.runtime.VertxHttpRecorder");
+
+ @Override
+ public boolean getAsBoolean() {
+ return IS_VERTX_HTTP_AVAILABLE;
+ }
+ }
+
@BuildStep(onlyIf = GrpcExtensionAvailable.class)
void grpcTracers(BuildProducer
+ * Right now, it is not possible to register multiple
+ *
* Right now, it is not possible to register multiple http.route
attribute. Right now, there is no other way to retrieve the route name from Vert.x using the
+ * Telemetry SPI, so we need to rely on the Metrics SPI.
+ * VertxMetrics
, meaning that only a single one is
+ * available per Quarkus instance. To avoid clashing with other extensions that provide Metrics data (like the
+ * Micrometer extension), we only register the {@link OpenTelemetryVertxHttpMetricsFactory} if the
+ * VertxHttpServerMetrics
is not available in the runtime.
+ */
+public class OpenTelemetryVertxHttpMetricsFactory implements VertxMetricsFactory {
+ @Override
+ public VertxMetrics metrics(final VertxOptions options) {
+ return new OpenTelemetryVertxHttpServerMetrics();
+ }
+
+ public static class OpenTelemetryVertxHttpServerMetrics
+ implements HttpServerMetricshttp.route
attribute. Right now, there is no other way to retrieve the route name from Vert.x using the
* Telemetry SPI, so we need to rely on the Metrics SPI.
- * VertxMetrics
, meaning that only a single one is
* available per Quarkus instance. To avoid clashing with other extensions that provide Metrics data (like the
* Micrometer extension), we only register the {@link OpenTelemetryVertxMetricsFactory} if the
@@ -26,19 +21,17 @@
public class OpenTelemetryVertxMetricsFactory implements VertxMetricsFactory {
@Override
public VertxMetrics metrics(final VertxOptions options) {
- return new OpenTelemetryHttpServerMetrics();
+ return new VertxMetrics() {
+ @Override
+ public HttpServerMetrics, ?, ?> createHttpServerMetrics(final HttpServerOptions options,
+ final SocketAddress localAddress) {
+ return new OpenTelemetryVertxServerMetrics();
+ }
+ };
}
- public static class OpenTelemetryHttpServerMetrics
- implements HttpServerMetrics