diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/HttpBinderProcessor.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/HttpBinderProcessor.java index dd927999a8652..13ea9bf2c5725 100644 --- a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/HttpBinderProcessor.java +++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/binder/HttpBinderProcessor.java @@ -22,8 +22,6 @@ import io.quarkus.micrometer.runtime.config.runtime.HttpClientConfig; import io.quarkus.micrometer.runtime.config.runtime.HttpServerConfig; import io.quarkus.micrometer.runtime.config.runtime.VertxConfig; -import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; -import io.quarkus.resteasy.reactive.spi.CustomContainerRequestFilterBuildItem; import io.quarkus.vertx.http.deployment.FilterBuildItem; /** @@ -94,21 +92,9 @@ SyntheticBeanBuildItem enableHttpBinders(MicrometerRecorder recorder, @BuildStep(onlyIf = HttpServerBinderEnabled.class) void enableHttpServerSupport(Capabilities capabilities, - BuildProducer resteasyJaxrsProviders, - BuildProducer customContainerRequestFilter, BuildProducer servletFilters, BuildProducer additionalBeans) { - // Will have one or the other of these (exclusive) - if (capabilities.isPresent(Capability.RESTEASY)) { - resteasyJaxrsProviders.produce(new ResteasyJaxrsProviderBuildItem(RESTEASY_CONTAINER_FILTER_CLASS_NAME)); - createAdditionalBean(additionalBeans, RESTEASY_CONTAINER_FILTER_CLASS_NAME); - } else if (capabilities.isPresent(Capability.RESTEASY_REACTIVE)) { - customContainerRequestFilter - .produce(new CustomContainerRequestFilterBuildItem(RESTEASY_REACTIVE_CONTAINER_FILTER_CLASS_NAME)); - createAdditionalBean(additionalBeans, RESTEASY_REACTIVE_CONTAINER_FILTER_CLASS_NAME); - } - // But this might be present as well (fallback. Rest URI processing preferred) if (capabilities.isPresent(Capability.SERVLET)) { servletFilters.produce( diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/HttpDevModeConfigTest.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/HttpDevModeConfigTest.java index 0b63a32b33e59..2dedf10dd0752 100644 --- a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/HttpDevModeConfigTest.java +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/HttpDevModeConfigTest.java @@ -1,8 +1,9 @@ package io.quarkus.micrometer.deployment.binder; import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; -import org.hamcrest.Matchers; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; @@ -30,7 +31,8 @@ public void test() throws Exception { when().get("/hello/two").then().statusCode(200); when().get("/hello/three").then().statusCode(200); when().get("/q/metrics").then().statusCode(200) - .body(Matchers.containsString("/hello/{message}")); + .body(containsString("/hello/{message}")) + .body(not(containsString("/goodbye/{message}"))); test.modifyResourceFile("application.properties", s -> s.replace("quarkus.micrometer.binder.http-server.ignore-patterns=/http", @@ -40,7 +42,7 @@ public void test() throws Exception { when().get("/hello/two").then().statusCode(200); when().get("/hello/three").then().statusCode(200); when().get("/q/metrics").then().statusCode(200) - .body(Matchers.containsString("/goodbye/{message}")); + .body(containsString("/goodbye/{message}")); } } diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/UriTagTest.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/UriTagTest.java index 5a773184eb8c0..b224c68a32f52 100644 --- a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/UriTagTest.java +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/UriTagTest.java @@ -53,20 +53,34 @@ static void removeRegistry() { } @Test - public void testGetRequests() throws Exception { + public void testRequestUris() throws Exception { RestAssured.basePath = "/"; - // If you invoke requests, http server and client meters should be registered - + // Server paths (templated) when().get("/one").then().statusCode(200); when().get("/two").then().statusCode(200); - when().get("/ping/one").then().statusCode(200); - when().get("/ping/two").then().statusCode(200); - when().get("/ping/three").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("/async-ping/one").then().statusCode(200); + when().get("/async-ping/two").then().statusCode(200); + when().get("/async-ping/three").then().statusCode(200); + System.out.println("Server paths\n" + Util.listMeters(registry, "http.server.requests")); System.out.println("Client paths\n" + Util.listMeters(registry, "http.client.requests")); @@ -76,11 +90,7 @@ public void testGetRequests() throws Exception { Assertions.assertEquals(0, registry.find("http.server.requests").tag("uri", "/two").timers().size(), Util.foundServerRequests(registry, "/two should be ignored.")); - // URIs for server: /ping/{message}, /pong/{message}, /vertx/item/{id}, /vertx/item/{id}/{sub}, /servlet/ - 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", "/pong/{message}").timers().size(), - Util.foundServerRequests(registry, "/pong/{message} should be returned by JAX-RS.")); + // 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}.")); @@ -90,24 +100,23 @@ public void testGetRequests() throws Exception { Assertions.assertEquals(1, registry.find("http.server.requests").tag("uri", "/servlet").timers().size(), Util.foundServerRequests(registry, "Servlet path (/servlet) should be used for servlet")); - // TODO: #15231 - // URIs For client: /pong/{message} - // Assertions.assertEquals(1, registry.find("http.client.requests").tag("uri", "/pong/{message}").timers().size(), - // Util.foundClientRequests(registry, "/pong/{message} should be returned by Rest client.")); - - 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); - - // GET and HEAD are two different methods, so double these up + // 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}, + // URIs for inbound rest client request: /pong/{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.")); + Assertions.assertEquals(1, registry.find("http.server.requests").tag("uri", "/pong/{message}").timers().size(), + Util.foundServerRequests(registry, "/pong/{message} 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}").timers().size(), + Util.foundClientRequests(registry, "/pong/{message} should be returned by Rest client.")); } } diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/UriTagWithHttpApplicationRootTest.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/UriTagWithHttpApplicationRootTest.java index 221727d6f67ef..c33a5b6ca4b29 100644 --- a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/UriTagWithHttpApplicationRootTest.java +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/UriTagWithHttpApplicationRootTest.java @@ -2,6 +2,8 @@ import static io.restassured.RestAssured.when; +import java.util.concurrent.CompletionStage; + import javax.inject.Inject; import javax.inject.Singleton; import javax.ws.rs.ApplicationPath; @@ -47,31 +49,44 @@ public class UriTagWithHttpApplicationRootTest { MeterRegistry registry; @Test - public void test() throws Exception { + public void testRequestUris() throws Exception { RestAssured.basePath = "/"; // If you invoke requests, http server and client meters should be registered // Leading context root (/foo) should be stripped from resulting _server_ tag // Application path (/bar) only impacts REST endpoints + when().get("/foo/vertx/item/123").then().statusCode(200); + when().get("/foo/servlet/12345").then().statusCode(200); + + // Server -> Rest client -> Server (templated) when().get("/foo/bar/ping/one").then().statusCode(200); when().get("/foo/bar/ping/two").then().statusCode(200); when().get("/foo/bar/ping/three").then().statusCode(200); - when().get("/foo/vertx/item/123").then().statusCode(200); - when().get("/foo/servlet/12345").then().statusCode(200); + when().get("/foo/bar/async-ping/one").then().statusCode(200); + when().get("/foo/bar/async-ping/two").then().statusCode(200); + when().get("/foo/bar/async-ping/three").then().statusCode(200); - // TODO: These are the current answers, but they aren't quite right + // Application Path does not apply to non-rest endpoints: /vertx/item/{id}, /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", "/servlet").timers().size(), + Util.foundServerRequests(registry, "Servlet path (/servlet) should be used for servlet")); - // URIs for server should include Application Path: /bar/ping/{message}, /bar/pong/{message} - Assertions.assertEquals(1, registry.find("http.server.requests").tag("uri", "/ping/{message}").timers().size(), + // URIs for server should include Application Path: /bar/ping/{message}, /bar/async-ping/{message} + // URIs for inbound rest client request should include Application Path: /bar/pong/{message} + Assertions.assertEquals(1, registry.find("http.server.requests").tag("uri", "/bar/ping/{message}").timers().size(), Util.foundServerRequests(registry, "/bar/ping/{message} should be returned by JAX-RS")); - Assertions.assertEquals(1, registry.find("http.server.requests").tag("uri", "/pong/{message}").timers().size(), + Assertions.assertEquals(1, + registry.find("http.server.requests").tag("uri", "/bar/async-ping/{message}").timers().size(), + Util.foundServerRequests(registry, "/bar/async-ping/{message} should be returned by JAX-RS.")); + Assertions.assertEquals(1, registry.find("http.server.requests").tag("uri", "/bar/pong/{message}").timers().size(), Util.foundServerRequests(registry, "/bar/pong/{message} should be returned by JAX-RS")); - // Application Path does not apply to non-rest endpoints: /vertx/item/{id} - 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}.")); + // URIs For client: /foo/pong/{message} + Assertions.assertEquals(1, registry.find("http.client.requests").tag("uri", "/foo/bar/pong/{message}").timers().size(), + Util.foundClientRequests(registry, "/foo/bar/pong/{message} should be returned by Rest client.")); } @ApplicationPath("/bar") @@ -88,6 +103,10 @@ public interface PingPongRestClient { @Path("/foo/bar/pong/{message}") @GET String pingpong(@PathParam("message") String message); + + @GET + @Path("/foo/bar/pong/{message}") + CompletionStage asyncPingPong(@PathParam("message") String message); } @Inject @@ -105,5 +124,11 @@ public String pong(@PathParam("message") String message) { public String ping(@PathParam("message") String message) { return pingRestClient.pingpong(message); } + + @GET + @Path("async-ping/{message}") + public CompletionStage asyncPing(@PathParam("message") String message) { + return pingRestClient.asyncPingPong(message); + } } } diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/UriTagWithHttpRootTest.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/UriTagWithHttpRootTest.java index 57d1f9d52669a..5ba092d7f6da0 100644 --- a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/UriTagWithHttpRootTest.java +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/binder/UriTagWithHttpRootTest.java @@ -2,6 +2,8 @@ import static io.restassured.RestAssured.when; +import java.util.concurrent.CompletionStage; + import javax.inject.Inject; import javax.inject.Singleton; import javax.ws.rs.GET; @@ -44,26 +46,45 @@ public class UriTagWithHttpRootTest { MeterRegistry registry; @Test - public void test() throws Exception { + public void testRequestUris() throws Exception { RestAssured.basePath = "/"; // If you invoke requests, http server and client meters should be registered // Leading context root (/foo) should be stripped from resulting _server_ tag + when().get("/foo/vertx/item/123").then().statusCode(200); + when().get("/foo/servlet/12345").then().statusCode(200); + + // Server -> Rest client -> Server (templated) when().get("/foo/ping/one").then().statusCode(200); when().get("/foo/ping/two").then().statusCode(200); when().get("/foo/ping/three").then().statusCode(200); - when().get("/foo/vertx/item/123").then().statusCode(200); - when().get("/foo/servlet/12345").then().statusCode(200); + when().get("/foo/async-ping/one").then().statusCode(200); + when().get("/foo/async-ping/two").then().statusCode(200); + when().get("/foo/async-ping/three").then().statusCode(200); + + System.out.println("Server paths\n" + Util.listMeters(registry, "http.server.requests")); + System.out.println("Client paths\n" + Util.listMeters(registry, "http.client.requests")); + + // URIs for server: /vertx/item/{id}, /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", "/servlet").timers().size(), + Util.foundServerRequests(registry, "Servlet path (/servlet) should be used for servlet")); - // URIs for server: /ping/{message}, /pong/{message}, /vertx/item/{id} + // URIs to trigger REST request: /ping/{message}, /async-ping/{message}, + // URIs for inbound rest client request: /pong/{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.")); Assertions.assertEquals(1, registry.find("http.server.requests").tag("uri", "/pong/{message}").timers().size(), Util.foundServerRequests(registry, "/pong/{message} should be returned by JAX-RS.")); - 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}.")); + + // URI for outbound client request: /foo/pong/{message} + Assertions.assertEquals(1, registry.find("http.client.requests").tag("uri", "/foo/pong/{message}").timers().size(), + Util.foundClientRequests(registry, "/foo/pong/{message} should be returned by Rest client.")); } @Path("/") @@ -76,6 +97,10 @@ public interface PingPongRestClient { @Path("/foo/pong/{message}") @GET String pingpong(@PathParam("message") String message); + + @GET + @Path("/foo/pong/{message}") + CompletionStage asyncPingPong(@PathParam("message") String message); } @Inject @@ -93,5 +118,11 @@ public String pong(@PathParam("message") String message) { public String ping(@PathParam("message") String message) { return pingRestClient.pingpong(message); } + + @GET + @Path("async-ping/{message}") + public CompletionStage asyncPing(@PathParam("message") String message) { + return pingRestClient.asyncPingPong(message); + } } } diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/PingPongResource.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/PingPongResource.java index 76925e485483b..425695de014b2 100644 --- a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/PingPongResource.java +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/test/PingPongResource.java @@ -1,5 +1,7 @@ package io.quarkus.micrometer.test; +import java.util.concurrent.CompletionStage; + import javax.inject.Inject; import javax.inject.Singleton; import javax.ws.rs.GET; @@ -14,10 +16,13 @@ public class PingPongResource { @RegisterRestClient(configKey = "pingpong") public interface PingPongRestClient { - - @Path("pong/{message}") @GET + @Path("pong/{message}") String pingpong(@PathParam("message") String message); + + @GET + @Path("pong/{message}") + CompletionStage asyncPingPong(@PathParam("message") String message); } @Inject @@ -36,6 +41,12 @@ public String ping(@PathParam("message") String message) { return pingRestClient.pingpong(message); } + @GET + @Path("async-ping/{message}") + public CompletionStage asyncPing(@PathParam("message") String message) { + return pingRestClient.asyncPingPong(message); + } + @GET @Path("one") public String one() { diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/RestClientMetricsListener.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/RestClientMetricsListener.java index cae8d431525d4..82457606eb9e5 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/RestClientMetricsListener.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/RestClientMetricsListener.java @@ -2,6 +2,7 @@ import java.io.IOException; +import javax.ws.rs.Priorities; import javax.ws.rs.client.ClientRequestContext; import javax.ws.rs.client.ClientRequestFilter; import javax.ws.rs.client.ClientResponseContext; @@ -35,8 +36,10 @@ public class RestClientMetricsListener implements RestClientListener { @Override public void onNewClient(Class serviceInterface, RestClientBuilder builder) { if (prepClientMetrics()) { - builder.register(this.clientRequestFilter); - builder.register(this.clientResponseFilter); + // This must run AFTER the OpenTelmetry client request filter + builder.register(this.clientRequestFilter, Priorities.HEADER_DECORATOR + 1); + // This must run Before the OpenTelmetry client response filter + builder.register(this.clientResponseFilter, Priorities.HEADER_DECORATOR + 1); } } @@ -75,10 +78,13 @@ class MetricsClientResponseFilter implements ClientResponseFilter { public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { RequestMetricInfo requestMetric = getRequestMetric(requestContext); if (requestMetric != null) { + String templatePath = (String) requestContext.getProperty("UrlPathTemplate"); + String requestPath = requestMetric.getNormalizedUriPath( httpMetricsConfig.getClientMatchPatterns(), httpMetricsConfig.getClientIgnorePatterns(), - requestContext.getUri().getPath()); + templatePath == null ? requestContext.getUri().getPath() : templatePath); + if (requestPath != null) { Timer.Sample sample = requestMetric.getSample(); int statusCode = responseContext.getStatus(); diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/HttpRequestMetric.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/HttpRequestMetric.java index 3a974fd259e85..f78ec117373ed 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/HttpRequestMetric.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/HttpRequestMetric.java @@ -39,9 +39,10 @@ public String getNormalizedUriPath(Map matchPatterns, List pathParameters = info.getPathParameters(); - if (!pathParameters.isEmpty()) { - // Replace parameter values in the URI with {key}: /items/123 -> /items/{id} - for (Map.Entry> entry : pathParameters.entrySet()) { - for (String value : entry.getValue()) { - path = path.replace(value, "{" + entry.getKey() + "}"); - } - } - log.debugf("Saving parameterized path %s in %s", path, routingContext); - metric.setTemplatePath(path); - } - return metric; - } -} diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderQuarkusRestContainerFilter.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderQuarkusRestContainerFilter.java deleted file mode 100644 index d118e4dd42941..0000000000000 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderQuarkusRestContainerFilter.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.quarkus.micrometer.runtime.binder.vertx; - -import javax.ws.rs.core.UriInfo; - -import org.jboss.resteasy.reactive.server.ServerRequestFilter; - -import io.vertx.ext.web.RoutingContext; - -public class VertxMeterBinderQuarkusRestContainerFilter { - - @ServerRequestFilter - public void filter(UriInfo uriInfo, RoutingContext routingContext) { - VertxMeterBinderContainerFilterUtil.doFilter(routingContext, uriInfo); - } -} diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderRestEasyContainerFilter.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderRestEasyContainerFilter.java deleted file mode 100644 index 8e32aeaddd146..0000000000000 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderRestEasyContainerFilter.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.quarkus.micrometer.runtime.binder.vertx; - -import javax.inject.Inject; -import javax.ws.rs.container.ContainerRequestContext; -import javax.ws.rs.container.ContainerRequestFilter; - -import io.vertx.ext.web.RoutingContext; - -public class VertxMeterBinderRestEasyContainerFilter implements ContainerRequestFilter { - - @Inject - RoutingContext routingContext; - - @Override - public void filter(final ContainerRequestContext requestContext) { - VertxMeterBinderContainerFilterUtil.doFilter(routingContext, requestContext.getUriInfo()); - } -} diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderRestEasyReactiveContainerFilter.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderRestEasyReactiveContainerFilter.java deleted file mode 100644 index a8d9368788a69..0000000000000 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/vertx/VertxMeterBinderRestEasyReactiveContainerFilter.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.quarkus.micrometer.runtime.binder.vertx; - -import javax.inject.Inject; -import javax.ws.rs.core.UriInfo; - -import org.jboss.resteasy.reactive.server.ServerRequestFilter; - -import io.vertx.ext.web.RoutingContext; - -public class VertxMeterBinderRestEasyReactiveContainerFilter { - - @Inject - RoutingContext routingContext; - - @ServerRequestFilter - public void filter(UriInfo uriInfo, RoutingContext routingContext) { - VertxMeterBinderContainerFilterUtil.doFilter(routingContext, uriInfo); - } -} diff --git a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/vertx/VertxTracingAdapter.java b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/vertx/VertxTracingAdapter.java index e1b41259a08da..4398119bbde6c 100644 --- a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/vertx/VertxTracingAdapter.java +++ b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/vertx/VertxTracingAdapter.java @@ -5,6 +5,7 @@ import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_FLAVOR; import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_HOST; import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_METHOD; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_ROUTE; import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_SCHEME; import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE; import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_TARGET; @@ -142,6 +143,13 @@ public void sendResponse( return; } + // Update Span name if parameterized path present + String pathTemplate = context.getLocal("UrlPathTemplate"); + if (pathTemplate != null && !pathTemplate.isEmpty()) { + span.updateName(pathTemplate.substring(1)); + span.setAttribute(HTTP_ROUTE, pathTemplate); + } + ((ContextInternal) context).dispatch(() -> { if (failure != null) { span.setStatus(StatusCode.ERROR); diff --git a/extensions/opentelemetry/rest-client/deployment/src/main/java/io/quarkus/opentelemetry/restclient/deployment/OpenTelemeteryRestClientProcessor.java b/extensions/opentelemetry/rest-client/deployment/src/main/java/io/quarkus/opentelemetry/restclient/deployment/OpenTelemeteryRestClientProcessor.java new file mode 100644 index 0000000000000..5c4e6e4e45904 --- /dev/null +++ b/extensions/opentelemetry/rest-client/deployment/src/main/java/io/quarkus/opentelemetry/restclient/deployment/OpenTelemeteryRestClientProcessor.java @@ -0,0 +1,21 @@ +package io.quarkus.opentelemetry.restclient.deployment; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; + +public class OpenTelemeteryRestClientProcessor { + + private static final String QUARKUS_REST_CLIENT_LISTENER = "io.quarkus.opentelemetry.restclient.QuarkusRestClientListener"; + + @BuildStep + void registerRestClientListener(BuildProducer resource, + BuildProducer reflectiveClass) { + resource.produce(new NativeImageResourceBuildItem( + "META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientListener")); + reflectiveClass + .produce(new ReflectiveClassBuildItem(true, true, QUARKUS_REST_CLIENT_LISTENER)); + } + +} diff --git a/extensions/opentelemetry/rest-client/runtime/src/main/java/io/quarkus/opentelemetry/restclient/ClientTracingFilter.java b/extensions/opentelemetry/rest-client/runtime/src/main/java/io/quarkus/opentelemetry/restclient/ClientTracingFilter.java index c23fb33077010..51ff45ece368e 100644 --- a/extensions/opentelemetry/rest-client/runtime/src/main/java/io/quarkus/opentelemetry/restclient/ClientTracingFilter.java +++ b/extensions/opentelemetry/rest-client/runtime/src/main/java/io/quarkus/opentelemetry/restclient/ClientTracingFilter.java @@ -44,7 +44,6 @@ public void filter(ClientRequestContext requestContext) throws IOException { // Add attributes builder.setAttribute(SemanticAttributes.HTTP_METHOD, ((ClientRequestContextImpl) requestContext).getInvocation().getMethod()); - // builder.setAttribute(SemanticAttributes.HTTP_URL, ((ClientRequestContextImpl) requestContext).getInvocation().getActualTarget().getUri().) builder.setAttribute(SemanticAttributes.HTTP_URL, requestContext.getUri().toString()); clientSpan = builder.startSpan(); @@ -54,6 +53,11 @@ public void filter(ClientRequestContext requestContext) throws IOException { @Override public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { if (clientSpan != null) { + String pathTemplate = (String) requestContext.getProperty("UrlPathTemplate"); + if (pathTemplate != null && !pathTemplate.isEmpty()) { + clientSpan.updateName(pathTemplate.substring(1)); + } + clientSpan.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, responseContext.getStatus()); if (!responseContext.getStatusInfo().getFamily().equals(Response.Status.Family.SUCCESSFUL)) { diff --git a/extensions/resteasy-classic/rest-client/deployment/src/main/java/io/quarkus/restclient/deployment/RestClientProcessor.java b/extensions/resteasy-classic/rest-client/deployment/src/main/java/io/quarkus/restclient/deployment/RestClientProcessor.java index cd003275e7e7d..69368a6711e28 100644 --- a/extensions/resteasy-classic/rest-client/deployment/src/main/java/io/quarkus/restclient/deployment/RestClientProcessor.java +++ b/extensions/resteasy-classic/rest-client/deployment/src/main/java/io/quarkus/restclient/deployment/RestClientProcessor.java @@ -70,16 +70,21 @@ import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; +import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; import io.quarkus.deployment.pkg.PackageConfig; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; import io.quarkus.restclient.NoopHostnameVerifier; +import io.quarkus.restclient.runtime.PathFeatureHandler; +import io.quarkus.restclient.runtime.PathTemplateInjectionFilter; import io.quarkus.restclient.runtime.RestClientBase; import io.quarkus.restclient.runtime.RestClientRecorder; import io.quarkus.resteasy.common.deployment.JaxrsProvidersToRegisterBuildItem; import io.quarkus.resteasy.common.deployment.RestClientBuildItem; import io.quarkus.resteasy.common.deployment.ResteasyInjectionReadyBuildItem; import io.quarkus.resteasy.common.spi.ResteasyDotNames; +import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; +import io.quarkus.runtime.metrics.MetricsFactory; class RestClientProcessor { private static final Logger log = Logger.getLogger(RestClientProcessor.class); @@ -135,12 +140,6 @@ void registerRestClientListenerForTracing( "META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientListener")); reflectiveClass .produce(new ReflectiveClassBuildItem(true, true, "io.smallrye.opentracing.SmallRyeRestClientListener")); - } else if (capabilities.isPresent(Capability.OPENTELEMETRY_TRACER)) { - resource.produce(new NativeImageResourceBuildItem( - "META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientListener")); - reflectiveClass - .produce(new ReflectiveClassBuildItem(true, true, - "io.quarkus.opentelemetry.runtime.tracing.client.QuarkusRestClientListener")); } } @@ -175,6 +174,7 @@ void processInterfaces( CombinedIndexBuildItem combinedIndexBuildItem, BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, Capabilities capabilities, + Optional metricsCapability, PackageConfig packageConfig, List restClientAnnotationProviders, BuildProducer proxyDefinition, @@ -271,6 +271,22 @@ void processInterfaces( } } + @BuildStep + void clientTracingFeature(Capabilities capabilities, + Optional metricsCapability, BuildProducer producer) { + if (isRequired(capabilities, metricsCapability)) { + producer.produce(new ResteasyJaxrsProviderBuildItem(PathFeatureHandler.class.getName())); + producer.produce(new ResteasyJaxrsProviderBuildItem(PathTemplateInjectionFilter.class.getName())); + } + } + + private boolean isRequired(Capabilities capabilities, + Optional metricsCapability) { + return (capabilities.isPresent(Capability.OPENTELEMETRY_TRACER) || + (metricsCapability.isPresent() + && metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER))); + } + private static List> checkAnnotationProviders(ClassInfo classInfo, List restClientAnnotationProviders) { return restClientAnnotationProviders.stream().filter(p -> (classInfo.classAnnotation(p.getAnnotationName()) != null)) diff --git a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/PathFeatureHandler.java b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/PathFeatureHandler.java new file mode 100644 index 0000000000000..e9df28a93dbee --- /dev/null +++ b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/PathFeatureHandler.java @@ -0,0 +1,61 @@ +package io.quarkus.restclient.runtime; + +import java.lang.reflect.Method; +import java.util.regex.Pattern; + +import javax.ws.rs.ConstrainedTo; +import javax.ws.rs.Path; +import javax.ws.rs.RuntimeType; +import javax.ws.rs.container.DynamicFeature; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.FeatureContext; + +/** + * feature that set's the URLConfig + */ +@ConstrainedTo(RuntimeType.CLIENT) +public class PathFeatureHandler implements DynamicFeature { + private static final Pattern MULTIPLE_SLASH_PATTERN = Pattern.compile("//+"); + + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + context.property("UrlPathTemplate", constructPath(resourceInfo.getResourceMethod())); + } + + private String constructPath(Method methodInfo) { + Path annotation = methodInfo.getAnnotation(Path.class); + + StringBuilder stringBuilder; + if (annotation != null) { + stringBuilder = new StringBuilder(slashify(annotation.value())); + } else { + stringBuilder = new StringBuilder(); + } + + // Look for @Path annotation on the class + annotation = methodInfo.getDeclaringClass().getAnnotation(Path.class); + if (annotation != null) { + stringBuilder.insert(0, slashify(annotation.value())); + } + + // Now make sure there is a leading path, and no duplicates + return MULTIPLE_SLASH_PATTERN.matcher('/' + stringBuilder.toString()).replaceAll("/"); + } + + String slashify(String path) { + // avoid doubles later. Empty for now + if (path == null || path.isEmpty() || "/".equals(path)) { + return ""; + } + // remove doubles + path = MULTIPLE_SLASH_PATTERN.matcher(path).replaceAll("/"); + // Label value consistency: result should not end with a slash + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + if (path.isEmpty() || path.startsWith("/")) { + return path; + } + return '/' + path; + } +} diff --git a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/PathTemplateInjectionFilter.java b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/PathTemplateInjectionFilter.java new file mode 100644 index 0000000000000..9c98c2a5ca08e --- /dev/null +++ b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/PathTemplateInjectionFilter.java @@ -0,0 +1,17 @@ +package io.quarkus.restclient.runtime; + +import javax.annotation.Priority; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; + +@Priority(Integer.MIN_VALUE) +public class PathTemplateInjectionFilter implements ClientRequestFilter { + + @Override + public void filter(ClientRequestContext requestContext) { + Object prop = requestContext.getConfiguration().getProperty("UrlPathTemplate"); + if (prop != null) { + requestContext.setProperty("UrlPathTemplate", prop); + } + } +} diff --git a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java index e9c6ffb75a921..57dfa99225b55 100644 --- a/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java +++ b/extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java @@ -304,6 +304,7 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi this.executorService = Executors.newCachedThreadPool(); resteasyClientBuilder.executorService(executorService, true); } + resteasyClientBuilder.register(DEFAULT_MEDIA_TYPE_FILTER); resteasyClientBuilder.register(METHOD_INJECTION_FILTER); resteasyClientBuilder.register(HEADERS_REQUEST_FILTER); diff --git a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java new file mode 100644 index 0000000000000..16b3161928558 --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java @@ -0,0 +1,138 @@ +package io.quarkus.resteasy.deployment; + +import java.util.Optional; +import java.util.regex.Pattern; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.MethodInfo; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; +import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; +import io.quarkus.resteasy.runtime.QuarkusRestPathTemplate; +import io.quarkus.resteasy.runtime.QuarkusRestPathTemplateInterceptor; +import io.quarkus.resteasy.server.common.spi.ResteasyJaxrsConfigBuildItem; +import io.quarkus.runtime.metrics.MetricsFactory; + +public class RestPathAnnotationProcessor { + + static final DotName REST_PATH = DotName.createSimple("javax.ws.rs.Path"); + static final DotName REGISTER_REST_CLIENT = DotName + .createSimple("org.eclipse.microprofile.rest.client.inject.RegisterRestClient"); + static final DotName TEMPLATE_PATH = DotName.createSimple(QuarkusRestPathTemplate.class.getName()); + static final DotName TEMPLATE_PATH_INTERCEPTOR = DotName.createSimple(QuarkusRestPathTemplateInterceptor.class.getName()); + + public static final Pattern MULTIPLE_SLASH_PATTERN = Pattern.compile("//+"); + + @BuildStep() + AdditionalBeanBuildItem registerBeanClasses(Capabilities capabilities, + Optional metricsCapability) { + if (notRequired(capabilities, metricsCapability)) { + return null; + } + + return AdditionalBeanBuildItem.builder() + .addBeanClass(TEMPLATE_PATH.toString()) + .addBeanClass(TEMPLATE_PATH_INTERCEPTOR.toString()) + .build(); + } + + @BuildStep + void findRestPaths( + Capabilities capabilities, Optional metricsCapability, + BuildProducer transformers, + Optional restApplicationPathBuildItem) { + + if (notRequired(capabilities, metricsCapability)) { + // Don't create transformer if Micrometer or OpenTelemetry are not present + return; + } + + String pathPrefix = null; + if (restApplicationPathBuildItem.isPresent()) { + ResteasyJaxrsConfigBuildItem pathItem = restApplicationPathBuildItem.get(); + if (!pathItem.getDefaultPath().equals(pathItem.getRootPath())) { + pathPrefix = slashify(pathItem.getDefaultPath()); // This is just the @ApplicationPath + } + } + + final String restPathPrefix = pathPrefix; + + transformers.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { + @Override + public boolean appliesTo(AnnotationTarget.Kind kind) { + return kind == AnnotationTarget.Kind.METHOD; + } + + @Override + public void transform(TransformationContext ctx) { + AnnotationTarget target = ctx.getTarget(); + MethodInfo methodInfo = target.asMethod(); + ClassInfo classInfo = methodInfo.declaringClass(); + + AnnotationInstance annotation = methodInfo.annotation(REST_PATH); + if (annotation == null) { + return; + } + // Don't create annotations for rest clients + if (classInfo.classAnnotation(REGISTER_REST_CLIENT) != null) { + return; + } + + StringBuilder stringBuilder = new StringBuilder(slashify(annotation.value().asString())); + + // Look for @Path annotation on the class + annotation = classInfo.classAnnotation(REST_PATH); + if (annotation != null) { + stringBuilder.insert(0, slashify(annotation.value().asString())); + } + + if (restPathPrefix != null) { + stringBuilder.insert(0, restPathPrefix); + } + + // Now make sure there is a leading path, and no duplicates + String templatePath = MULTIPLE_SLASH_PATTERN.matcher('/' + stringBuilder.toString()).replaceAll("/"); + + // resulting path (used as observability attributes) should start with a '/' + ctx.transform() + .add(TEMPLATE_PATH, AnnotationValue.createStringValue("value", templatePath)) + .done(); + } + })); + } + + String slashify(String path) { + // avoid doubles later. Empty for now + if (path == null || path.isEmpty() || "/".equals(path)) { + return ""; + } + // remove doubles + path = MULTIPLE_SLASH_PATTERN.matcher(path).replaceAll("/"); + // Label value consistency: result should not end with a slash + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + if (path.isEmpty() || path.startsWith("/")) { + return path; + } + return '/' + path; + } + + private boolean notRequired(Capabilities capabilities, + Optional metricsCapability) { + return capabilities.isMissing(Capability.RESTEASY) || + (capabilities.isMissing(Capability.OPENTELEMETRY_TRACER) && + !(metricsCapability.isPresent() + && metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER))); + } +} diff --git a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/QuarkusRestPathTemplate.java b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/QuarkusRestPathTemplate.java new file mode 100644 index 0000000000000..531231ef3df18 --- /dev/null +++ b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/QuarkusRestPathTemplate.java @@ -0,0 +1,19 @@ +package io.quarkus.resteasy.runtime; + +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 javax.enterprise.util.Nonbinding; +import javax.interceptor.InterceptorBinding; + +@Inherited +@InterceptorBinding +@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface QuarkusRestPathTemplate { + @Nonbinding + String value() default ""; +} diff --git a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/QuarkusRestPathTemplateInterceptor.java b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/QuarkusRestPathTemplateInterceptor.java new file mode 100644 index 0000000000000..6c92300190a70 --- /dev/null +++ b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/QuarkusRestPathTemplateInterceptor.java @@ -0,0 +1,46 @@ +package io.quarkus.resteasy.runtime; + +import java.lang.annotation.Annotation; +import java.util.Set; + +import javax.annotation.Priority; +import javax.inject.Inject; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; + +import io.quarkus.arc.ArcInvocationContext; +import io.quarkus.vertx.http.runtime.CurrentVertxRequest; +import io.vertx.core.http.impl.HttpServerRequestInternal; + +@SuppressWarnings("unused") +@QuarkusRestPathTemplate +@Interceptor +@Priority(Interceptor.Priority.LIBRARY_BEFORE + 10) +public class QuarkusRestPathTemplateInterceptor { + @Inject + CurrentVertxRequest request; + + @AroundInvoke + Object restMethodInvoke(InvocationContext context) throws Exception { + QuarkusRestPathTemplate annotation = getAnnotation(context); + if (annotation != null) { + ((HttpServerRequestInternal) request.getCurrent().request()).context().putLocal("UrlPathTemplate", + annotation.value()); + } + return context.proceed(); + } + + @SuppressWarnings("unchecked") + static QuarkusRestPathTemplate getAnnotation(InvocationContext context) { + Set annotations = (Set) context.getContextData() + .get(ArcInvocationContext.KEY_INTERCEPTOR_BINDINGS); + + for (Annotation a : annotations) { + if (a instanceof QuarkusRestPathTemplate) { + return (QuarkusRestPathTemplate) a; + } + } + return null; + } +} diff --git a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java index 371e9b24ef7bd..a11199347a4fe 100644 --- a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java @@ -51,10 +51,12 @@ import org.jboss.jandex.PrimitiveType; import org.jboss.jandex.Type; import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.client.handlers.ClientObservabilityHandler; import org.jboss.resteasy.reactive.client.impl.AsyncInvokerImpl; import org.jboss.resteasy.reactive.client.impl.ClientImpl; import org.jboss.resteasy.reactive.client.impl.UniInvoker; import org.jboss.resteasy.reactive.client.impl.WebTargetImpl; +import org.jboss.resteasy.reactive.client.spi.ClientRestHandler; import org.jboss.resteasy.reactive.common.core.GenericTypeMapping; import org.jboss.resteasy.reactive.common.core.ResponseBuilderFactory; import org.jboss.resteasy.reactive.common.core.Serialisers; @@ -78,6 +80,8 @@ import io.quarkus.arc.processor.DotNames; import io.quarkus.arc.processor.MethodDescriptors; import io.quarkus.arc.processor.Types; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.deployment.GeneratedClassGizmoAdaptor; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -90,6 +94,7 @@ import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; +import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; import io.quarkus.deployment.recording.RecorderContext; import io.quarkus.deployment.util.JandexUtil; import io.quarkus.gizmo.AssignableResultHandle; @@ -121,6 +126,7 @@ import io.quarkus.resteasy.reactive.spi.MessageBodyWriterBuildItem; import io.quarkus.resteasy.reactive.spi.MessageBodyWriterOverrideBuildItem; import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.metrics.MetricsFactory; import io.smallrye.mutiny.Uni; import io.vertx.core.buffer.Buffer; import io.vertx.ext.web.multipart.MultipartForm; @@ -171,6 +177,7 @@ void setupClientProxies(JaxrsClientReactiveRecorder recorder, List enricherBuildItems, BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, Optional resourceScanningResultBuildItem, + Capabilities capabilities, Optional metricsCapability, ResteasyReactiveConfig config, RecorderContext recorderContext, BuildProducer generatedClassBuildItemBuildProducer, @@ -215,6 +222,10 @@ void setupClientProxies(JaxrsClientReactiveRecorder recorder, .setSmartDefaultProduces(disableSmartDefaultProduces.isEmpty()) .build(); + boolean observabilityIntegrationNeeded = (capabilities.isPresent(Capability.OPENTELEMETRY_TRACER) || + (metricsCapability.isPresent() + && metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER))); + Map>> clientImplementations = new HashMap<>(); Map failures = new HashMap<>(); for (Map.Entry i : result.getClientInterfaces().entrySet()) { @@ -228,7 +239,7 @@ void setupClientProxies(JaxrsClientReactiveRecorder recorder, try { RuntimeValue> proxyProvider = generateClientInvoker(recorderContext, clientProxy, enricherBuildItems, generatedClassBuildItemBuildProducer, clazz, index, defaultConsumesType, - result.getHttpAnnotationToMethod()); + result.getHttpAnnotationToMethod(), observabilityIntegrationNeeded); if (proxyProvider != null) { clientImplementations.put(clientProxy.getClassName(), proxyProvider); } @@ -419,7 +430,8 @@ A more full example of generated client (with sub-resource) can is at the bottom private RuntimeValue> generateClientInvoker(RecorderContext recorderContext, RestClientInterface restClientInterface, List enrichers, BuildProducer generatedClasses, ClassInfo interfaceClass, - IndexView index, String defaultMediaType, Map httpAnnotationToMethod) { + IndexView index, String defaultMediaType, Map httpAnnotationToMethod, + boolean observabilityIntegrationNeeded) { String name = restClientInterface.getClassName() + "$$QuarkusRestClientInterface"; MethodDescriptor ctorDesc = MethodDescriptor.ofConstructor(name, WebTarget.class.getName()); @@ -763,12 +775,22 @@ A more full example of generated client (with sub-resource) can is at the bottom } else { // constructor: initializing the immutable part of the method-specific web target - FieldDescriptor webTargetForMethod = FieldDescriptor.of(name, "target" + methodIndex, WebTarget.class); + FieldDescriptor webTargetForMethod = FieldDescriptor.of(name, "target" + methodIndex, WebTargetImpl.class); c.getFieldCreator(webTargetForMethod).setModifiers(Modifier.FINAL); webTargets.add(webTargetForMethod); AssignableResultHandle constructorTarget = createWebTargetForMethod(constructor, baseTarget, method); constructor.writeInstanceField(webTargetForMethod, constructor.getThis(), constructorTarget); + if (observabilityIntegrationNeeded) { + String templatePath = restClientInterface.getPath() + method.getPath(); + constructor.invokeVirtualMethod( + MethodDescriptor.ofMethod(WebTargetImpl.class, "setPreClientSendHandler", void.class, + ClientRestHandler.class), + constructor.readInstanceField(webTargetForMethod, constructor.getThis()), + constructor.newInstance( + MethodDescriptor.ofConstructor(ClientObservabilityHandler.class, String.class), + constructor.load(templatePath))); + } // generate implementation for a method from jaxrs interface: MethodCreator methodCreator = c.getMethodCreator(method.getName(), method.getSimpleReturnType(), diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ObservabilityProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ObservabilityProcessor.java new file mode 100644 index 0000000000000..a0df593379570 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ObservabilityProcessor.java @@ -0,0 +1,40 @@ +package io.quarkus.resteasy.reactive.server.deployment; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.MethodInfo; +import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer; +import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner; + +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; +import io.quarkus.resteasy.reactive.server.runtime.observability.ObservabilityCustomizer; +import io.quarkus.runtime.metrics.MetricsFactory; + +public class ObservabilityProcessor { + + @BuildStep + MethodScannerBuildItem integrateObservability(Capabilities capabilities, + Optional metricsCapability) { + boolean integrationNeeded = (capabilities.isPresent(Capability.OPENTELEMETRY_TRACER) || + (metricsCapability.isPresent() + && metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER))); + if (!integrationNeeded) { + return null; + } + return new MethodScannerBuildItem(new MethodScanner() { + @Override + public List scan(MethodInfo method, ClassInfo actualEndpointClass, + Map methodContext) { + return Collections.singletonList(new ObservabilityCustomizer()); + } + }); + } + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index 9b893a9faaf08..5fe285deb2040 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -527,6 +527,7 @@ private boolean hasAnnotation(MethodInfo method, short paramPosition, DotName an } String deploymentPath = sanitizeApplicationPath(applicationPath); + // Handler used for both the default and non-default deployment path (specified as application path or resteasyConfig.path) // Routes use the order VertxHttpRecorder.DEFAULT_ROUTE_ORDER + 1 to ensure the default route is called before the resteasy one Class applicationClass = application == null ? Application.class : application.getClass(); @@ -713,5 +714,4 @@ private void registerReader(ResteasyReactiveRecorder recorder, ServerSerialisers reader.setConstraint(constraint); recorder.registerReader(serialisers, entityClass.getName(), reader); } - } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityCustomizer.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityCustomizer.java new file mode 100644 index 0000000000000..f5c29691f2d41 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityCustomizer.java @@ -0,0 +1,23 @@ +package io.quarkus.resteasy.reactive.server.runtime.observability; + +import java.util.Collections; +import java.util.List; + +import org.jboss.resteasy.reactive.common.model.ResourceClass; +import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer; +import org.jboss.resteasy.reactive.server.model.ServerResourceMethod; +import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; + +public class ObservabilityCustomizer implements HandlerChainCustomizer { + @Override + public List handlers(Phase phase, ResourceClass resourceClass, + ServerResourceMethod serverResourceMethod) { + if (phase.equals(Phase.AFTER_MATCH)) { + ObservabilityHandler observabilityHandler = new ObservabilityHandler(); + observabilityHandler + .setTemplatePath(resourceClass.getPath() + serverResourceMethod.getPath()); + return Collections.singletonList(observabilityHandler); + } + return Collections.emptyList(); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityHandler.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityHandler.java new file mode 100644 index 0000000000000..745b4fc2fcbb4 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityHandler.java @@ -0,0 +1,28 @@ +package io.quarkus.resteasy.reactive.server.runtime.observability; + +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; + +import io.vertx.core.http.impl.HttpServerRequestInternal; +import io.vertx.ext.web.RoutingContext; + +public class ObservabilityHandler implements ServerRestHandler { + + // make mutable to allow for bytecode serialization + private String templatePath; + + public String getTemplatePath() { + return templatePath; + } + + public void setTemplatePath(String templatePath) { + this.templatePath = templatePath; + } + + @Override + public void handle(ResteasyReactiveRequestContext requestContext) throws Exception { + + ((HttpServerRequestInternal) (requestContext.unwrap(RoutingContext.class).request())).context() + .putLocal("UrlPathTemplate", templatePath); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/SecurityContextOverrideHandler.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/SecurityContextOverrideHandler.java index d50d96a7a4051..640b9ae838a67 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/SecurityContextOverrideHandler.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/SecurityContextOverrideHandler.java @@ -10,8 +10,10 @@ import javax.ws.rs.core.SecurityContext; +import org.jboss.resteasy.reactive.common.model.ResourceClass; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer; +import org.jboss.resteasy.reactive.server.model.ServerResourceMethod; import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; import io.quarkus.arc.Arc; @@ -118,7 +120,8 @@ private InjectableInstance getCurrentIdentityAssocia public static class Customizer implements HandlerChainCustomizer { @Override - public List handlers(Phase phase) { + public List handlers(Phase phase, ResourceClass resourceClass, + ServerResourceMethod serverResourceMethod) { return Collections.singletonList(new SecurityContextOverrideHandler()); } } diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientObservabilityHandler.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientObservabilityHandler.java new file mode 100644 index 0000000000000..522297601f0be --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientObservabilityHandler.java @@ -0,0 +1,22 @@ +package org.jboss.resteasy.reactive.client.handlers; + +import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext; +import org.jboss.resteasy.reactive.client.spi.ClientRestHandler; + +/** + * This is added by the Reactive Rest Client if observability features are enabled + */ +@SuppressWarnings("unused") +public class ClientObservabilityHandler implements ClientRestHandler { + + private final String templatePath; + + public ClientObservabilityHandler(String templatePath) { + this.templatePath = templatePath; + } + + @Override + public void handle(RestClientRequestContext requestContext) throws Exception { + requestContext.getClientFilterProperties().put("UrlPathTemplate", templatePath); + } +} diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/HandlerChain.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/HandlerChain.java index 67beeba86fbe4..1859a440fa634 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/HandlerChain.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/HandlerChain.java @@ -22,6 +22,8 @@ class HandlerChain { private final ClientRestHandler clientResponseCompleteRestHandler; private final ClientRestHandler clientErrorHandler; + private ClientRestHandler preClientSendHandler = null; + public HandlerChain(boolean followRedirects) { this.clientSendHandler = new ClientSendRequestHandler(followRedirects); this.clientSetResponseEntityRestHandler = new ClientSetResponseEntityRestHandler(); @@ -29,6 +31,11 @@ public HandlerChain(boolean followRedirects) { this.clientErrorHandler = new ClientErrorHandler(); } + HandlerChain setPreClientSendHandler(ClientRestHandler preClientSendHandler) { + this.preClientSendHandler = preClientSendHandler; + return this; + } + ClientRestHandler[] createHandlerChain(ConfigurationImpl configuration) { List requestFilters = configuration.getRequestFilters(); List responseFilters = configuration.getResponseFilters(); @@ -36,10 +43,14 @@ ClientRestHandler[] createHandlerChain(ConfigurationImpl configuration) { return new ClientRestHandler[] { clientSendHandler, clientSetResponseEntityRestHandler, clientResponseCompleteRestHandler }; } - List result = new ArrayList<>(3 + requestFilters.size() + responseFilters.size()); + List result = new ArrayList<>( + (preClientSendHandler != null ? 4 : 3) + requestFilters.size() + responseFilters.size()); for (int i = 0; i < requestFilters.size(); i++) { result.add(new ClientRequestFilterRestHandler(requestFilters.get(i))); } + if (preClientSendHandler != null) { + result.add(preClientSendHandler); + } result.add(clientSendHandler); result.add(clientSetResponseEntityRestHandler); for (int i = 0; i < responseFilters.size(); i++) { diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java index 35bbc67e822af..94385b80de483 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java @@ -361,4 +361,8 @@ public RestClientRequestContext setAbortedWith(Response abortedWith) { public boolean isMultipart() { return entity != null && entity.getEntity() instanceof MultipartForm; } + + public Map getClientFilterProperties() { + return properties; + } } diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/WebTargetImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/WebTargetImpl.java index 117196067a389..3c25ddd03b2cb 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/WebTargetImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/WebTargetImpl.java @@ -10,6 +10,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.UriBuilder; +import org.jboss.resteasy.reactive.client.spi.ClientRestHandler; import org.jboss.resteasy.reactive.common.core.Serialisers; import org.jboss.resteasy.reactive.common.jaxrs.ConfigurationImpl; import org.jboss.resteasy.reactive.common.jaxrs.UriBuilderImpl; @@ -25,6 +26,10 @@ public class WebTargetImpl implements WebTarget { final HandlerChain handlerChain; final ThreadSetupAction requestContext; + // an additional handler that is passed to the handlerChain + // used to support observability features + private ClientRestHandler preClientSendHandler = null; + public WebTargetImpl(ClientImpl restClient, HttpClient client, UriBuilder uriBuilder, ConfigurationImpl configuration, HandlerChain handlerChain, @@ -260,8 +265,11 @@ public WebTargetImpl queryParamNoTemplate(String name, Object... values) throws protected WebTargetImpl newInstance(HttpClient client, UriBuilder uriBuilder, ConfigurationImpl configuration) { - return new WebTargetImpl(restClient, client, uriBuilder, configuration, handlerChain, + WebTargetImpl result = new WebTargetImpl(restClient, client, uriBuilder, configuration, + handlerChain.setPreClientSendHandler(preClientSendHandler), requestContext); + result.setPreClientSendHandler(preClientSendHandler); + return result; } @Override @@ -296,7 +304,8 @@ private void abortIfClosed() { protected InvocationBuilderImpl createQuarkusRestInvocationBuilder(HttpClient client, UriBuilder uri, ConfigurationImpl configuration) { - return new InvocationBuilderImpl(uri.build(), restClient, client, this, configuration, handlerChain, requestContext); + return new InvocationBuilderImpl(uri.build(), restClient, client, this, configuration, + handlerChain.setPreClientSendHandler(preClientSendHandler), requestContext); } @Override @@ -377,6 +386,11 @@ public ClientImpl getRestClient() { return restClient; } + @SuppressWarnings("unused") + public void setPreClientSendHandler(ClientRestHandler preClientSendHandler) { + this.preClientSendHandler = preClientSendHandler; + } + Serialisers getSerialisers() { return restClient.getClientContext().getSerialisers(); } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java index 352724cdcff1f..326acb5b8ec28 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java @@ -127,7 +127,7 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, Map pathParameterIndexes = buildParamIndexMap(classPathTemplate, methodPathTemplate); List handlers = new ArrayList<>(); - addHandlers(handlers, method, info, HandlerChainCustomizer.Phase.AFTER_MATCH); + addHandlers(handlers, clazz, method, info, HandlerChainCustomizer.Phase.AFTER_MATCH); MediaType sseElementType = null; if (method.getSseElementType() != null) { sseElementType = MediaType.valueOf(method.getSseElementType()); @@ -266,7 +266,7 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, handlers.add(instanceHandler); } - addHandlers(handlers, method, info, HandlerChainCustomizer.Phase.RESOLVE_METHOD_PARAMETERS); + addHandlers(handlers, clazz, method, info, HandlerChainCustomizer.Phase.RESOLVE_METHOD_PARAMETERS); for (int i = 0; i < parameters.length; i++) { ServerMethodParameter param = (ServerMethodParameter) parameters[i]; boolean single = param.isSingle(); @@ -297,7 +297,7 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, converter, param.parameterType, param.isObtainedAsCollection(), param.isOptional())); } - addHandlers(handlers, method, info, HandlerChainCustomizer.Phase.BEFORE_METHOD_INVOKE); + addHandlers(handlers, clazz, method, info, HandlerChainCustomizer.Phase.BEFORE_METHOD_INVOKE); EndpointInvoker invoker = method.getInvoker().get(); ServerRestHandler alternate = alternateInvoker(method, invoker); if (alternate != null) { @@ -305,7 +305,7 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, } else { handlers.add(new InvocationHandler(invoker)); } - addHandlers(handlers, method, info, HandlerChainCustomizer.Phase.AFTER_METHOD_INVOKE); + addHandlers(handlers, clazz, method, info, HandlerChainCustomizer.Phase.AFTER_METHOD_INVOKE); Type returnType = TypeSignatureParser.parse(method.getReturnType()); Type nonAsyncReturnType = getNonAsyncReturnType(returnType); @@ -393,7 +393,7 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, responseFilterHandlers = Collections.emptyList(); } else { handlers.add(new ResponseHandler()); - addHandlers(handlers, method, info, HandlerChainCustomizer.Phase.AFTER_RESPONSE_CREATED); + addHandlers(handlers, clazz, method, info, HandlerChainCustomizer.Phase.AFTER_RESPONSE_CREATED); responseFilterHandlers = new ArrayList<>(interceptorDeployment.setupResponseFilterHandler()); handlers.addAll(responseFilterHandlers); handlers.add(new ResponseWriterHandler(dynamicEntityWriter)); @@ -430,13 +430,14 @@ private boolean isSingleEffectiveWriter(List> buildTimeWrit return buildTimeWriters.get(0) instanceof ServerMessageBodyWriter.AllWriteableMessageBodyWriter; } - private void addHandlers(List handlers, ServerResourceMethod method, DeploymentInfo info, + private void addHandlers(List handlers, ResourceClass clazz, ServerResourceMethod method, + DeploymentInfo info, HandlerChainCustomizer.Phase phase) { for (int i = 0; i < info.getGlobalHandlerCustomizers().size(); i++) { - handlers.addAll(info.getGlobalHandlerCustomizers().get(i).handlers(phase)); + handlers.addAll(info.getGlobalHandlerCustomizers().get(i).handlers(phase, clazz, method)); } for (int i = 0; i < method.getHandlerChainCustomizers().size(); i++) { - handlers.addAll(method.getHandlerChainCustomizers().get(i).handlers(phase)); + handlers.addAll(method.getHandlerChainCustomizers().get(i).handlers(phase, clazz, method)); } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/FixedHandlerChainCustomizer.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/FixedHandlerChainCustomizer.java index 6c2f44df6155d..015b4962c3933 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/FixedHandlerChainCustomizer.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/FixedHandlerChainCustomizer.java @@ -2,6 +2,7 @@ import java.util.Collections; import java.util.List; +import org.jboss.resteasy.reactive.common.model.ResourceClass; import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; public class FixedHandlerChainCustomizer implements HandlerChainCustomizer { @@ -18,7 +19,8 @@ public FixedHandlerChainCustomizer() { } @Override - public List handlers(Phase phase) { + public List handlers(Phase phase, ResourceClass resourceClass, + ServerResourceMethod serverResourceMethod) { if (this.phase == phase) { return Collections.singletonList(handler); } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/HandlerChainCustomizer.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/HandlerChainCustomizer.java index 37d19a23e70ec..d12bf6d0e4434 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/HandlerChainCustomizer.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/HandlerChainCustomizer.java @@ -3,11 +3,23 @@ import java.util.Collections; import java.util.List; import java.util.function.Supplier; +import org.jboss.resteasy.reactive.common.model.ResourceClass; import org.jboss.resteasy.reactive.server.spi.EndpointInvoker; import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; public interface HandlerChainCustomizer { + /** + * + * @param phase The phase + * @param serverResourceMethod The method, will be null if this has not been matched yet + * @return + */ + default List handlers(Phase phase, ResourceClass resourceClass, ServerResourceMethod resourceMethod) { + return handlers(phase); + } + + @Deprecated default List handlers(Phase phase) { return Collections.emptyList(); } diff --git a/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/PingPongResource.java b/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/PingPongResource.java new file mode 100644 index 0000000000000..1e6318ef6f0f3 --- /dev/null +++ b/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/PingPongResource.java @@ -0,0 +1,52 @@ +package io.quarkus.it.micrometer.prometheus; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import io.smallrye.common.annotation.Blocking; +import io.smallrye.mutiny.Uni; + +@Singleton +@Path("/client") +public class PingPongResource { + @RegisterRestClient(configKey = "pingpong") + public interface PingPongRestClient { + + @Path("/client/pong/{message}") + @GET + String pingpong(@PathParam("message") String message); + + @GET + @Path("/client/pong/{message}") + Uni asyncPingpong(@PathParam("message") String message); + } + + @Inject + @RestClient + PingPongRestClient pingRestClient; + + @GET + @Path("pong/{message}") + public String pong(@PathParam("message") String message) { + return message; + } + + @GET + @Blocking + @Path("ping/{message}") + public String ping(@PathParam("message") String message) { + return pingRestClient.pingpong(message); + } + + @GET + @Path("async-ping/{message}") + public Uni asyncPing(@PathParam("message") String message) { + return pingRestClient.asyncPingpong(message); + } +} diff --git a/integration-tests/micrometer-prometheus/src/main/resources/application.properties b/integration-tests/micrometer-prometheus/src/main/resources/application.properties index 428a6ca0c2dbf..d1817bd8bb2fb 100644 --- a/integration-tests/micrometer-prometheus/src/main/resources/application.properties +++ b/integration-tests/micrometer-prometheus/src/main/resources/application.properties @@ -13,8 +13,7 @@ quarkus.hibernate-orm.statistics=true quarkus.hibernate-orm.metrics.enabled=true quarkus.datasource.db-kind=h2 -quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:test -quarkus.datasource.jdbc.max-size=8 +quarkus.datasource.devservices=true # This is the old property, should still be used quarkus.micrometer.binder.vertx.ignore-patterns=/fruit/create @@ -22,4 +21,7 @@ quarkus.micrometer.binder.vertx.ignore-patterns=/fruit/create quarkus.micrometer.binder.http-server.match-patterns=/message/match/\\\\d+/[0-9]+=/message/match/{id}/{sub},\ /message/match/.*=/message/match/{other} +pingpong/mp-rest/url=${test.url} +#pingpong/mp-rest/url=http://localhost:8080 + deployment.env=test diff --git a/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ClientRequestIT.java b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ClientRequestIT.java new file mode 100644 index 0000000000000..8f843ee563a04 --- /dev/null +++ b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ClientRequestIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.micrometer.prometheus; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +public class ClientRequestIT extends ClientRequestTest { +} diff --git a/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ClientRequestTest.java b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ClientRequestTest.java new file mode 100644 index 0000000000000..2afa3d0d3c467 --- /dev/null +++ b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ClientRequestTest.java @@ -0,0 +1,33 @@ +package io.quarkus.it.micrometer.prometheus; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.CoreMatchers.containsString; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class ClientRequestTest { + @Test + void testClientRequests() { + when().get("/client/ping/one").then().statusCode(200) + .body(containsString("one")); + when().get("/client/ping/two").then().statusCode(200) + .body(containsString("two")); + when().get("/client/async-ping/one").then().statusCode(200) + .body(containsString("one")); + when().get("/client/async-ping/two").then().statusCode(200) + .body(containsString("two")); + + when().get("/q/metrics").then().statusCode(200) + .body(containsString( + "http_client_requests_seconds_count{clientName=\"localhost\",env=\"test\",method=\"GET\",outcome=\"SUCCESS\",registry=\"prometheus\",status=\"200\",uri=\"/client/pong/{message}\"")) + .body(containsString( + "http_server_requests_seconds_count{env=\"test\",method=\"GET\",outcome=\"SUCCESS\",registry=\"prometheus\",status=\"200\",uri=\"/client/ping/{message}\"")) + .body(containsString( + "http_server_requests_seconds_count{env=\"test\",method=\"GET\",outcome=\"SUCCESS\",registry=\"prometheus\",status=\"200\",uri=\"/client/pong/{message}\"")) + .body(containsString( + "http_server_requests_seconds_count{env=\"test\",method=\"GET\",outcome=\"SUCCESS\",registry=\"prometheus\",status=\"200\",uri=\"/client/async-ping/{message}\"")); + } +} diff --git a/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ExampleResourcesIT.java b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ExampleResourcesIT.java new file mode 100644 index 0000000000000..bb31057a40f72 --- /dev/null +++ b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/ExampleResourcesIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.micrometer.prometheus; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +public class ExampleResourcesIT extends ExampleResourcesTest { +} diff --git a/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryIT.java b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryIT.java index a3542559f136b..42bd09b53b866 100644 --- a/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryIT.java +++ b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryIT.java @@ -2,12 +2,6 @@ import io.quarkus.test.junit.NativeImageTest; -/** - * tests that application.properties is read from src/main/resources when running native image tests - * - * This does not necessarily belong here, but main and test-extension have a lot of existing - * config that would need to be duplicated, so it is here out of convenience. - */ @NativeImageTest class PrometheusMetricsRegistryIT extends PrometheusMetricsRegistryTest { } diff --git a/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryTest.java b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryTest.java index 3a64d1c75226e..595ed7ed54bfa 100644 --- a/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryTest.java +++ b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryTest.java @@ -1,6 +1,6 @@ package io.quarkus.it.micrometer.prometheus; -import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.not; @@ -23,100 +23,64 @@ class PrometheusMetricsRegistryTest { @Test @Order(1) void testRegistryInjection() { - given() - .when().get("/message") - .then() - .statusCode(200) + when().get("/message").then().statusCode(200) .body(containsString("io.micrometer.core.instrument.composite.CompositeMeterRegistry")); } @Test @Order(2) void testUnknownUrl() { - given() - .when().get("/messsage/notfound") - .then() - .statusCode(404); + when().get("/messsage/notfound").then().statusCode(404); } @Test @Order(3) void testServerError() { - given() - .when().get("/message/fail") - .then() - .statusCode(500); + when().get("/message/fail").then().statusCode(500); } @Test @Order(4) void testPathParameter() { - given() - .when().get("/message/item/123") - .then() - .statusCode(200); + when().get("/message/item/123").then().statusCode(200); } @Test @Order(5) void testMultipleParameters() { - given() - .when().get("/message/match/123/1") - .then() - .statusCode(200); - - given() - .when().get("/message/match/1/123") - .then() - .statusCode(200); - - given() - .when().get("/message/match/baloney") - .then() - .statusCode(200); + when().get("/message/match/123/1").then().statusCode(200); + + when().get("/message/match/1/123").then().statusCode(200); + + when().get("/message/match/baloney").then().statusCode(200); } @Test @Order(6) void testPanacheCalls() { - given() - .when().get("/fruit/create") - .then() - .statusCode(204); - - given() - .when().get("/fruit/all") - .then() - .statusCode(204); + when().get("/fruit/create").then().statusCode(204); + + when().get("/fruit/all").then().statusCode(204); } @Test @Order(7) void testPrimeEndpointCalls() { - given() - .when().get("/prime/7") - .then() - .statusCode(200) + when().get("/prime/7").then().statusCode(200) .body(containsString("is prime")); } @Test @Order(8) void testAllTheThings() { - given() - .when().get("/all-the-things") - .then() - .statusCode(200) + when().get("/all-the-things").then().statusCode(200) .body(containsString("OK")); } @Test @Order(10) void testPrometheusScrapeEndpoint() { - given() - .when().get("/q/metrics") - .then() - .statusCode(200) + when().get("/q/metrics").then().statusCode(200) // Prometheus body has ALL THE THINGS in no particular order diff --git a/integration-tests/opentelemetry/pom.xml b/integration-tests/opentelemetry/pom.xml index 43446120d5e20..2881c8cefad66 100644 --- a/integration-tests/opentelemetry/pom.xml +++ b/integration-tests/opentelemetry/pom.xml @@ -25,6 +25,16 @@ io.quarkus quarkus-resteasy-jackson + + io.quarkus + quarkus-resteasy-mutiny + + + + + io.quarkus + quarkus-rest-client + @@ -68,6 +78,32 @@ + + io.quarkus + quarkus-rest-client-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-mutiny-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-opentelemetry-deployment diff --git a/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/PingPongResource.java b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/PingPongResource.java new file mode 100644 index 0000000000000..b875008a6b972 --- /dev/null +++ b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/PingPongResource.java @@ -0,0 +1,52 @@ +package io.quarkus.it.opentelemetry; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import io.smallrye.common.annotation.Blocking; +import io.smallrye.mutiny.Uni; + +@Singleton +@Path("/client") +public class PingPongResource { + @RegisterRestClient(configKey = "pingpong") + public interface PingPongRestClient { + + @Path("/client/pong/{message}") + @GET + String pingpong(@PathParam("message") String message); + + @GET + @Path("/client/pong/{message}") + Uni asyncPingpong(@PathParam("message") String message); + } + + @Inject + @RestClient + PingPongRestClient pingRestClient; + + @GET + @Path("pong/{message}") + public String pong(@PathParam("message") String message) { + return message; + } + + @GET + @Blocking + @Path("ping/{message}") + public String ping(@PathParam("message") String message) { + return pingRestClient.pingpong(message); + } + + @GET + @Path("async-ping/{message}") + public Uni asyncPing(@PathParam("message") String message) { + return pingRestClient.asyncPingpong(message); + } +} diff --git a/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/output/SpanDataSerializer.java b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/output/SpanDataSerializer.java index 7cfa89d8c9021..d9062d135f6b7 100644 --- a/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/output/SpanDataSerializer.java +++ b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/output/SpanDataSerializer.java @@ -23,6 +23,7 @@ public void serialize(SpanData spanData, JsonGenerator jsonGenerator, Serializer jsonGenerator.writeStartObject(); jsonGenerator.writeStringField("spanId", spanData.getSpanId()); + jsonGenerator.writeStringField("traceId", spanData.getTraceId()); jsonGenerator.writeStringField("name", spanData.getName()); jsonGenerator.writeStringField("kind", spanData.getKind().name()); jsonGenerator.writeBooleanField("ended", spanData.hasEnded()); diff --git a/integration-tests/opentelemetry/src/main/resources/application.properties b/integration-tests/opentelemetry/src/main/resources/application.properties index e72ce50b75a5d..f7645beb60e9d 100644 --- a/integration-tests/opentelemetry/src/main/resources/application.properties +++ b/integration-tests/opentelemetry/src/main/resources/application.properties @@ -1,3 +1,5 @@ # Setting these for tests explicitly. Not required in normal application quarkus.application.name=opentelemetry-integration-test -quarkus.application.version=999-SNAPSHOT \ No newline at end of file +quarkus.application.version=999-SNAPSHOT + +pingpong/mp-rest/url=${test.url} diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTestCase.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTestCase.java index dc78d7e0fb30e..0f0bddbef7da0 100644 --- a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTestCase.java +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTestCase.java @@ -2,6 +2,7 @@ import static io.restassured.RestAssured.get; import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; import java.net.URL; @@ -83,8 +84,6 @@ void testResourceTracing() { Assertions.assertEquals("/direct", spanData.get("attr_http.target")); Assertions.assertEquals(directUrl.getAuthority(), spanData.get("attr_http.host")); Assertions.assertEquals("http", spanData.get("attr_http.scheme")); - //TODO Waiting on a templatized route to be available - // Assertions.assertEquals("/direct", spanData.get("attr_http.route")); Assertions.assertEquals("200", spanData.get("attr_http.status_code")); Assertions.assertNotNull(spanData.get("attr_http.client_ip")); Assertions.assertNotNull(spanData.get("attr_http.user_agent")); @@ -122,8 +121,6 @@ void testChainedResourceTracing() { Assertions.assertEquals("/chained", spanData.get("attr_http.target")); Assertions.assertEquals(chainedUrl.getAuthority(), spanData.get("attr_http.host")); Assertions.assertEquals("http", spanData.get("attr_http.scheme")); - //TODO Waiting on a templatized route to be available - // Assertions.assertEquals("/chained", spanData.get("attr_http.route")); Assertions.assertEquals("200", spanData.get("attr_http.status_code")); Assertions.assertNotNull(spanData.get("attr_http.client_ip")); Assertions.assertNotNull(spanData.get("attr_http.user_agent")); @@ -177,8 +174,6 @@ void testTracingWithParentHeaders() { Assertions.assertEquals("/direct", spanData.get("attr_http.target")); Assertions.assertEquals(directUrl.getAuthority(), spanData.get("attr_http.host")); Assertions.assertEquals("http", spanData.get("attr_http.scheme")); - //TODO Waiting on a templatized route to be available - // Assertions.assertEquals("/direct", spanData.get("attr_http.route")); Assertions.assertEquals("200", spanData.get("attr_http.status_code")); Assertions.assertNotNull(spanData.get("attr_http.client_ip")); Assertions.assertNotNull(spanData.get("attr_http.user_agent")); @@ -216,8 +211,6 @@ void testDeepPathNaming() { Assertions.assertEquals("/deep/path", spanData.get("attr_http.target")); Assertions.assertEquals(deepPathUrl.getAuthority(), spanData.get("attr_http.host")); Assertions.assertEquals("http", spanData.get("attr_http.scheme")); - //TODO Waiting on a templatized route to be available - // Assertions.assertEquals("/deep/path", spanData.get("attr_http.route")); Assertions.assertEquals("200", spanData.get("attr_http.status_code")); Assertions.assertNotNull(spanData.get("attr_http.client_ip")); Assertions.assertNotNull(spanData.get("attr_http.user_agent")); @@ -241,8 +234,7 @@ void testPathParameter() { verifyResource(spanData); - //TODO Need templatized route to verify span name is "param/{paramId}" - Assertions.assertEquals("param/12345", spanData.get("name")); + Assertions.assertEquals("param/{paramId}", spanData.get("name")); Assertions.assertEquals(SpanKind.SERVER.toString(), spanData.get("kind")); Assertions.assertTrue((Boolean) spanData.get("ended")); @@ -256,8 +248,99 @@ void testPathParameter() { Assertions.assertEquals("/param/12345", spanData.get("attr_http.target")); Assertions.assertEquals(pathParamUrl.getAuthority(), spanData.get("attr_http.host")); Assertions.assertEquals("http", spanData.get("attr_http.scheme")); - //TODO Waiting on a templatized route to be available - // Assertions.assertEquals("/param/{paramId}", spanData.get("attr_http.route")); + Assertions.assertEquals("/param/{paramId}", spanData.get("attr_http.route")); + Assertions.assertEquals("200", spanData.get("attr_http.status_code")); + Assertions.assertNotNull(spanData.get("attr_http.client_ip")); + Assertions.assertNotNull(spanData.get("attr_http.user_agent")); + } + + @Test + void testClientTracing() { + resetExporter(); + + given() + .when().get("/client/ping/one") + .then() + .statusCode(200) + .body(containsString("one")); + + Awaitility.await().atMost(Duration.ofMinutes(2)).until(() -> getSpans().size() == 3); + + List> spans = getSpans(); + + // Server Span + Map spanData = spans.get(2); + Assertions.assertNotNull(spanData); + Assertions.assertNotNull(spanData.get("spanId")); + + String parentSpanId = (String) spanData.get("spanId"); + String parentTraceId = (String) spanData.get("traceId"); + + verifyResource(spanData); + + Assertions.assertEquals("client/ping/{message}", spanData.get("name")); + Assertions.assertEquals(SpanKind.SERVER.toString(), spanData.get("kind")); + Assertions.assertTrue((Boolean) spanData.get("ended")); + + Assertions.assertEquals(SpanId.getInvalid(), spanData.get("parent_spanId")); + Assertions.assertEquals(TraceId.getInvalid(), spanData.get("parent_traceId")); + Assertions.assertFalse((Boolean) spanData.get("parent_valid")); + Assertions.assertFalse((Boolean) spanData.get("parent_remote")); + + Assertions.assertEquals("GET", spanData.get("attr_http.method")); + Assertions.assertEquals("1.1", spanData.get("attr_http.flavor")); + Assertions.assertEquals("/client/ping/one", spanData.get("attr_http.target")); + Assertions.assertEquals(pathParamUrl.getAuthority(), spanData.get("attr_http.host")); + Assertions.assertEquals("http", spanData.get("attr_http.scheme")); + Assertions.assertEquals("/client/ping/{message}", spanData.get("attr_http.route")); + Assertions.assertEquals("200", spanData.get("attr_http.status_code")); + Assertions.assertNotNull(spanData.get("attr_http.client_ip")); + Assertions.assertNotNull(spanData.get("attr_http.user_agent")); + + // Client span + spanData = spans.get(1); + Assertions.assertNotNull(spanData); + Assertions.assertNotNull(spanData.get("spanId")); + + verifyResource(spanData); + + Assertions.assertEquals("client/pong/{message}", spanData.get("name")); + Assertions.assertEquals(SpanKind.CLIENT.toString(), spanData.get("kind")); + Assertions.assertTrue((Boolean) spanData.get("ended")); + + Assertions.assertEquals(parentSpanId, spanData.get("parent_spanId")); + Assertions.assertEquals(parentTraceId, spanData.get("parent_traceId")); + Assertions.assertTrue((Boolean) spanData.get("parent_valid")); + Assertions.assertFalse((Boolean) spanData.get("parent_remote")); + + Assertions.assertEquals("GET", spanData.get("attr_http.method")); + Assertions.assertEquals("http://localhost:8081/client/pong/one", spanData.get("attr_http.url")); + Assertions.assertEquals("200", spanData.get("attr_http.status_code")); + + parentSpanId = (String) spanData.get("spanId"); + + // Server span of client + spanData = spans.get(0); + Assertions.assertNotNull(spanData); + Assertions.assertNotNull(spanData.get("spanId")); + + verifyResource(spanData); + + Assertions.assertEquals("client/pong/{message}", spanData.get("name")); + Assertions.assertEquals(SpanKind.SERVER.toString(), spanData.get("kind")); + Assertions.assertTrue((Boolean) spanData.get("ended")); + + Assertions.assertEquals(parentSpanId, spanData.get("parent_spanId")); + Assertions.assertEquals(parentTraceId, spanData.get("parent_traceId")); + Assertions.assertTrue((Boolean) spanData.get("parent_valid")); + Assertions.assertTrue((Boolean) spanData.get("parent_remote")); + + Assertions.assertEquals("GET", spanData.get("attr_http.method")); + Assertions.assertEquals("1.1", spanData.get("attr_http.flavor")); + Assertions.assertEquals("/client/pong/one", spanData.get("attr_http.target")); + Assertions.assertEquals(pathParamUrl.getAuthority(), spanData.get("attr_http.host")); + Assertions.assertEquals("http", spanData.get("attr_http.scheme")); + Assertions.assertEquals("/client/pong/{message}", spanData.get("attr_http.route")); Assertions.assertEquals("200", spanData.get("attr_http.status_code")); Assertions.assertNotNull(spanData.get("attr_http.client_ip")); Assertions.assertNotNull(spanData.get("attr_http.user_agent"));