From 21a96570c1d16c56489000893cd276ae4542ced7 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 10 Dec 2024 18:47:58 +0200 Subject: [PATCH] Improve JFR integration in Quarkus REST We are now able to capture events for all HTTP requests that are meant to be handled by Quarkus REST, whether they were successfully handled or not. Fixes: #44976 --- .../quarkus/jfr/deployment/JfrProcessor.java | 20 ++- .../http/rest/JfrReactiveServerFilter.java | 45 ------ .../rest/ReactiveServerRecorderProducer.java | 38 ----- .../jfr/runtime/http/rest/Recorder.java | 12 -- .../ClassicServerFilter.java} | 10 +- .../ClassicServerRecorder.java} | 14 +- .../ClassicServerRecorderProducer.java | 6 +- .../rest/reactive/ReactiveServerFilters.java | 50 +++++++ .../rest/reactive/ReactiveServerRecorder.java | 98 +++++++++++++ .../ReactiveServerRecorderProducer.java | 18 +++ .../http/rest/reactive/RequestInfo.java | 4 + .../http/rest/reactive/ResourceInfo.java | 4 + .../reactive/ServerStartRecordingHandler.java | 48 +++++++ .../deployment/ResteasyReactiveProcessor.java | 12 +- .../spi/GlobalHandlerCustomizerBuildItem.java | 22 +++ .../java/io/quarkus/jfr/it/AppResource.java | 10 ++ .../java/io/quarkus/jfr/it/JfrResource.java | 21 ++- .../test/java/io/quarkus/jfr/it/JfrTest.java | 132 ++++++++++++++++-- 18 files changed, 429 insertions(+), 135 deletions(-) delete mode 100644 extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/JfrReactiveServerFilter.java delete mode 100644 extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/ReactiveServerRecorderProducer.java delete mode 100644 extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/Recorder.java rename extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/{JfrClassicServerFilter.java => classic/ClassicServerFilter.java} (76%) rename extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/{ServerRecorder.java => classic/ClassicServerRecorder.java} (81%) rename extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/{ => classic}/ClassicServerRecorderProducer.java (83%) create mode 100644 extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ReactiveServerFilters.java create mode 100644 extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ReactiveServerRecorder.java create mode 100644 extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ReactiveServerRecorderProducer.java create mode 100644 extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/RequestInfo.java create mode 100644 extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ResourceInfo.java create mode 100644 extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ServerStartRecordingHandler.java create mode 100644 extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/GlobalHandlerCustomizerBuildItem.java diff --git a/extensions/jfr/deployment/src/main/java/io/quarkus/jfr/deployment/JfrProcessor.java b/extensions/jfr/deployment/src/main/java/io/quarkus/jfr/deployment/JfrProcessor.java index d5494245b1874..caad3d89959e1 100644 --- a/extensions/jfr/deployment/src/main/java/io/quarkus/jfr/deployment/JfrProcessor.java +++ b/extensions/jfr/deployment/src/main/java/io/quarkus/jfr/deployment/JfrProcessor.java @@ -12,11 +12,13 @@ import io.quarkus.jfr.runtime.OTelIdProducer; import io.quarkus.jfr.runtime.QuarkusIdProducer; import io.quarkus.jfr.runtime.config.JfrRuntimeConfig; -import io.quarkus.jfr.runtime.http.rest.ClassicServerRecorderProducer; -import io.quarkus.jfr.runtime.http.rest.JfrClassicServerFilter; -import io.quarkus.jfr.runtime.http.rest.JfrReactiveServerFilter; -import io.quarkus.jfr.runtime.http.rest.ReactiveServerRecorderProducer; +import io.quarkus.jfr.runtime.http.rest.classic.ClassicServerFilter; +import io.quarkus.jfr.runtime.http.rest.classic.ClassicServerRecorderProducer; +import io.quarkus.jfr.runtime.http.rest.reactive.ReactiveServerFilters; +import io.quarkus.jfr.runtime.http.rest.reactive.ReactiveServerRecorderProducer; +import io.quarkus.jfr.runtime.http.rest.reactive.ServerStartRecordingHandler; import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; +import io.quarkus.resteasy.reactive.server.spi.GlobalHandlerCustomizerBuildItem; import io.quarkus.resteasy.reactive.spi.CustomContainerRequestFilterBuildItem; @BuildSteps @@ -52,7 +54,8 @@ void registerRequestIdProducer(Capabilities capabilities, @BuildStep void registerRestIntegration(Capabilities capabilities, BuildProducer filterBeans, - BuildProducer additionalBeans) { + BuildProducer additionalBeans, + BuildProducer globalHandlerCustomizerProducer) { if (capabilities.isPresent(Capability.RESTEASY_REACTIVE)) { @@ -61,7 +64,10 @@ void registerRestIntegration(Capabilities capabilities, .build()); filterBeans - .produce(new CustomContainerRequestFilterBuildItem(JfrReactiveServerFilter.class.getName())); + .produce(new CustomContainerRequestFilterBuildItem(ReactiveServerFilters.class.getName())); + + globalHandlerCustomizerProducer + .produce(new GlobalHandlerCustomizerBuildItem(new ServerStartRecordingHandler.Customizer())); } } @@ -76,7 +82,7 @@ void registerResteasyClassicIntegration(Capabilities capabilities, .build()); resteasyJaxrsProviderBuildItemBuildProducer - .produce(new ResteasyJaxrsProviderBuildItem(JfrClassicServerFilter.class.getName())); + .produce(new ResteasyJaxrsProviderBuildItem(ClassicServerFilter.class.getName())); } } diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/JfrReactiveServerFilter.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/JfrReactiveServerFilter.java deleted file mode 100644 index 46f14bdead66e..0000000000000 --- a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/JfrReactiveServerFilter.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.quarkus.jfr.runtime.http.rest; - -import jakarta.inject.Inject; -import jakarta.ws.rs.container.ContainerResponseContext; -import jakarta.ws.rs.core.Response; - -import org.jboss.logging.Logger; -import org.jboss.resteasy.reactive.server.ServerRequestFilter; -import org.jboss.resteasy.reactive.server.ServerResponseFilter; - -public class JfrReactiveServerFilter { - - private static final Logger LOG = Logger.getLogger(JfrReactiveServerFilter.class); - - @Inject - Recorder recorder; - - @ServerRequestFilter - public void requestFilter() { - if (LOG.isDebugEnabled()) { - LOG.debug("Enter Jfr Reactive Request Filter"); - } - recorder.recordStartEvent(); - recorder.startPeriodEvent(); - } - - @ServerResponseFilter - public void responseFilter(ContainerResponseContext responseContext) { - if (LOG.isDebugEnabled()) { - LOG.debug("Enter Jfr Reactive Response Filter"); - } - if (isRecordable(responseContext)) { - recorder.endPeriodEvent(); - recorder.recordEndEvent(); - } else { - if (LOG.isDebugEnabled()) { - LOG.debug("Recording REST event was skipped"); - } - } - } - - private boolean isRecordable(ContainerResponseContext responseContext) { - return responseContext.getStatus() != Response.Status.NOT_FOUND.getStatusCode(); - } -} diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/ReactiveServerRecorderProducer.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/ReactiveServerRecorderProducer.java deleted file mode 100644 index 393e50c84848e..0000000000000 --- a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/ReactiveServerRecorderProducer.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.quarkus.jfr.runtime.http.rest; - -import jakarta.enterprise.context.Dependent; -import jakarta.enterprise.context.RequestScoped; -import jakarta.enterprise.inject.Produces; -import jakarta.inject.Inject; -import jakarta.ws.rs.core.Context; - -import org.jboss.resteasy.reactive.server.SimpleResourceInfo; - -import io.quarkus.jfr.runtime.IdProducer; -import io.vertx.core.http.HttpServerRequest; - -@Dependent -public class ReactiveServerRecorderProducer { - - @Context - HttpServerRequest vertxRequest; - - @Context - SimpleResourceInfo resourceInfo; - - @Inject - IdProducer idProducer; - - @Produces - @RequestScoped - public Recorder create() { - String httpMethod = vertxRequest.method().name(); - String uri = vertxRequest.path(); - Class resourceClass = resourceInfo.getResourceClass(); - String resourceClassName = (resourceClass == null) ? null : resourceClass.getName(); - String resourceMethodName = resourceInfo.getMethodName(); - String client = vertxRequest.remoteAddress().toString(); - - return new ServerRecorder(httpMethod, uri, resourceClassName, resourceMethodName, client, idProducer); - } -} diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/Recorder.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/Recorder.java deleted file mode 100644 index fc8535b773d67..0000000000000 --- a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/Recorder.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.quarkus.jfr.runtime.http.rest; - -public interface Recorder { - - void recordStartEvent(); - - void recordEndEvent(); - - void startPeriodEvent(); - - void endPeriodEvent(); -} diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/JfrClassicServerFilter.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/classic/ClassicServerFilter.java similarity index 76% rename from extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/JfrClassicServerFilter.java rename to extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/classic/ClassicServerFilter.java index e019c5dfb6710..2e67e40201571 100644 --- a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/JfrClassicServerFilter.java +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/classic/ClassicServerFilter.java @@ -1,4 +1,4 @@ -package io.quarkus.jfr.runtime.http.rest; +package io.quarkus.jfr.runtime.http.rest.classic; import java.io.IOException; @@ -14,16 +14,16 @@ import io.quarkus.arc.Arc; @Provider -public class JfrClassicServerFilter implements ContainerRequestFilter, ContainerResponseFilter { +public class ClassicServerFilter implements ContainerRequestFilter, ContainerResponseFilter { - private static final Logger LOG = Logger.getLogger(JfrClassicServerFilter.class); + private static final Logger LOG = Logger.getLogger(ClassicServerFilter.class); @Override public void filter(ContainerRequestContext requestContext) throws IOException { if (LOG.isDebugEnabled()) { LOG.debug("Enter Jfr Classic Request Filter"); } - Recorder recorder = Arc.container().instance(Recorder.class).get(); + ClassicServerRecorder recorder = Arc.container().instance(ClassicServerRecorder.class).get(); recorder.recordStartEvent(); recorder.startPeriodEvent(); } @@ -36,7 +36,7 @@ public void filter(ContainerRequestContext requestContext, ContainerResponseCont } if (isRecordable(responseContext)) { - Recorder recorder = Arc.container().instance(Recorder.class).get(); + ClassicServerRecorder recorder = Arc.container().instance(ClassicServerRecorder.class).get(); recorder.endPeriodEvent(); recorder.recordEndEvent(); } else { diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/ServerRecorder.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/classic/ClassicServerRecorder.java similarity index 81% rename from extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/ServerRecorder.java rename to extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/classic/ClassicServerRecorder.java index 3f2dc0428fdc6..fed42a45de64e 100644 --- a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/ServerRecorder.java +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/classic/ClassicServerRecorder.java @@ -1,9 +1,12 @@ -package io.quarkus.jfr.runtime.http.rest; +package io.quarkus.jfr.runtime.http.rest.classic; import io.quarkus.jfr.runtime.IdProducer; import io.quarkus.jfr.runtime.http.AbstractHttpEvent; +import io.quarkus.jfr.runtime.http.rest.RestEndEvent; +import io.quarkus.jfr.runtime.http.rest.RestPeriodEvent; +import io.quarkus.jfr.runtime.http.rest.RestStartEvent; -public class ServerRecorder implements Recorder { +public class ClassicServerRecorder { private final String httpMethod; private final String uri; @@ -13,7 +16,8 @@ public class ServerRecorder implements Recorder { private final IdProducer idProducer; private RestPeriodEvent durationEvent; - public ServerRecorder(String httpMethod, String uri, String resourceClass, String resourceMethod, String client, + public ClassicServerRecorder(String httpMethod, String uri, String resourceClass, String resourceMethod, + String client, IdProducer idProducer) { this.httpMethod = httpMethod; this.uri = uri; @@ -23,7 +27,6 @@ public ServerRecorder(String httpMethod, String uri, String resourceClass, Strin this.idProducer = idProducer; } - @Override public void recordStartEvent() { RestStartEvent startEvent = new RestStartEvent(); @@ -34,7 +37,6 @@ public void recordStartEvent() { } } - @Override public void recordEndEvent() { RestEndEvent endEvent = new RestEndEvent(); @@ -45,13 +47,11 @@ public void recordEndEvent() { } } - @Override public void startPeriodEvent() { durationEvent = new RestPeriodEvent(); durationEvent.begin(); } - @Override public void endPeriodEvent() { durationEvent.end(); diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/ClassicServerRecorderProducer.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/classic/ClassicServerRecorderProducer.java similarity index 83% rename from extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/ClassicServerRecorderProducer.java rename to extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/classic/ClassicServerRecorderProducer.java index 9ee161e302ade..355df43b5571e 100644 --- a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/ClassicServerRecorderProducer.java +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/classic/ClassicServerRecorderProducer.java @@ -1,4 +1,4 @@ -package io.quarkus.jfr.runtime.http.rest; +package io.quarkus.jfr.runtime.http.rest.classic; import java.lang.reflect.Method; @@ -25,7 +25,7 @@ public class ClassicServerRecorderProducer { @Produces @RequestScoped - public Recorder create() { + public ClassicServerRecorder create() { String httpMethod = vertxRequest.method().name(); String uri = vertxRequest.path(); Class resourceClass = resourceInfo.getResourceClass(); @@ -34,6 +34,6 @@ public Recorder create() { String resourceMethodName = (resourceMethod == null) ? null : resourceMethod.getName(); String client = vertxRequest.remoteAddress().toString(); - return new ServerRecorder(httpMethod, uri, resourceClassName, resourceMethodName, client, idProducer); + return new ClassicServerRecorder(httpMethod, uri, resourceClassName, resourceMethodName, client, idProducer); } } diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ReactiveServerFilters.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ReactiveServerFilters.java new file mode 100644 index 0000000000000..b7d1566dfb9be --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ReactiveServerFilters.java @@ -0,0 +1,50 @@ +package io.quarkus.jfr.runtime.http.rest.reactive; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.server.ServerRequestFilter; +import org.jboss.resteasy.reactive.server.ServerResponseFilter; +import org.jboss.resteasy.reactive.server.SimpleResourceInfo; + +public class ReactiveServerFilters { + + private static final Logger LOG = Logger.getLogger(ReactiveServerFilters.class); + + private final ReactiveServerRecorder recorder; + + public ReactiveServerFilters(ReactiveServerRecorder recorder) { + this.recorder = recorder; + } + + /** + * Executed if request processing proceeded correctly. + * We now have to update the start event with the resource class and method data and also commit the event. + */ + @ServerRequestFilter + public void requestFilter(SimpleResourceInfo resourceInfo) { + Class resourceClass = resourceInfo.getResourceClass(); + if (resourceClass != null) { // should always be the case + String resourceClassName = resourceClass.getName(); + String resourceMethodName = resourceInfo.getMethodName(); + recorder + .updateResourceInfo(new ResourceInfo(resourceClassName, resourceMethodName)) + .commitStartEventIfNecessary(); + } + + } + + /** + * This will execute regardless of a processing failure or not. + * If there was a failure, we need to check if the start event was not commited + * (which happens when request was not matched to any resource method) and if so, commit it. + */ + @ServerResponseFilter + public void responseFilter() { + if (LOG.isDebugEnabled()) { + LOG.debug("Enter Jfr Reactive Response Filter"); + } + recorder + .recordEndEvent() + .endPeriodEvent(); + } + +} diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ReactiveServerRecorder.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ReactiveServerRecorder.java new file mode 100644 index 0000000000000..151736f520d0d --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ReactiveServerRecorder.java @@ -0,0 +1,98 @@ +package io.quarkus.jfr.runtime.http.rest.reactive; + +import io.quarkus.jfr.runtime.IdProducer; +import io.quarkus.jfr.runtime.http.AbstractHttpEvent; +import io.quarkus.jfr.runtime.http.rest.RestEndEvent; +import io.quarkus.jfr.runtime.http.rest.RestPeriodEvent; +import io.quarkus.jfr.runtime.http.rest.RestStartEvent; + +class ReactiveServerRecorder { + + private final RequestInfo requestInfo; + private final IdProducer idProducer; + + private volatile ResourceInfo resourceInfo; + + private volatile RestStartEvent startEvent; + // TODO: we can perhaps get rid of this volatile if access patterns to this and startEvent allow it + private volatile boolean startEventHandled; + + private volatile RestPeriodEvent durationEvent; + + public ReactiveServerRecorder(RequestInfo requestInfo, IdProducer idProducer) { + this.requestInfo = requestInfo; + this.idProducer = idProducer; + } + + public ReactiveServerRecorder createStartEvent() { + startEvent = new RestStartEvent(); + return this; + } + + public ReactiveServerRecorder createAndStartPeriodEvent() { + durationEvent = new RestPeriodEvent(); + durationEvent.begin(); + return this; + } + + public ReactiveServerRecorder updateResourceInfo(ResourceInfo resourceInfo) { + this.resourceInfo = resourceInfo; + return this; + } + + public ReactiveServerRecorder commitStartEventIfNecessary() { + startEventHandled = true; + var se = startEvent; + if (se.shouldCommit()) { + setHttpInfo(startEvent); + se.commit(); + } + return this; + } + + /** + * Because this can be called when a start event has not been completely handled + * (this happens when request processing failed because a Resource method could not be identified), + * we need to handle that event as well. + */ + public ReactiveServerRecorder recordEndEvent() { + if (!startEventHandled) { + commitStartEventIfNecessary(); + } + + RestEndEvent endEvent = new RestEndEvent(); + if (endEvent.shouldCommit()) { + setHttpInfo(endEvent); + endEvent.commit(); + } + + return this; + } + + public ReactiveServerRecorder endPeriodEvent() { + if (durationEvent != null) { + durationEvent.end(); + if (durationEvent.shouldCommit()) { + setHttpInfo(durationEvent); + durationEvent.commit(); + } + } else { + // this shouldn't happen, but if it does due to an error on our side, the request processing shouldn't be botched because of it + } + + return this; + } + + private void setHttpInfo(AbstractHttpEvent event) { + event.setTraceId(idProducer.getTraceId()); + event.setSpanId(idProducer.getSpanId()); + event.setHttpMethod(requestInfo.httpMethod()); + event.setUri(requestInfo.uri()); + event.setClient(requestInfo.remoteAddress()); + var ri = resourceInfo; + if (resourceInfo != null) { + event.setResourceClass(ri.resourceClass()); + event.setResourceMethod(ri.resourceMethod()); + } + } +} diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ReactiveServerRecorderProducer.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ReactiveServerRecorderProducer.java new file mode 100644 index 0000000000000..62cd81ce11bc5 --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ReactiveServerRecorderProducer.java @@ -0,0 +1,18 @@ +package io.quarkus.jfr.runtime.http.rest.reactive; + +import jakarta.enterprise.context.RequestScoped; + +import io.quarkus.jfr.runtime.IdProducer; +import io.vertx.core.http.HttpServerRequest; + +public class ReactiveServerRecorderProducer { + + @RequestScoped + public ReactiveServerRecorder create(IdProducer idProducer, HttpServerRequest vertxRequest) { + String httpMethod = vertxRequest.method().name(); + String uri = vertxRequest.path(); + String client = vertxRequest.remoteAddress().toString(); + + return new ReactiveServerRecorder(new RequestInfo(httpMethod, uri, client), idProducer); + } +} diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/RequestInfo.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/RequestInfo.java new file mode 100644 index 0000000000000..41a9baa19add4 --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/RequestInfo.java @@ -0,0 +1,4 @@ +package io.quarkus.jfr.runtime.http.rest.reactive; + +record RequestInfo(String httpMethod, String uri, String remoteAddress) { +} diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ResourceInfo.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ResourceInfo.java new file mode 100644 index 0000000000000..219b842e679c7 --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ResourceInfo.java @@ -0,0 +1,4 @@ +package io.quarkus.jfr.runtime.http.rest.reactive; + +record ResourceInfo(String resourceClass, String resourceMethod) { +} diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ServerStartRecordingHandler.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ServerStartRecordingHandler.java new file mode 100644 index 0000000000000..74ec6fd7f61c0 --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/reactive/ServerStartRecordingHandler.java @@ -0,0 +1,48 @@ +package io.quarkus.jfr.runtime.http.rest.reactive; + +import java.util.Collections; +import java.util.List; + +import org.jboss.logging.Logger; +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; +import io.quarkus.jfr.runtime.http.rest.RestStartEvent; + +/** + * Kicks off the creation of a {@link RestStartEvent}. + * This is done very early as to be able to capture events such as 405, 406, etc. + */ +public class ServerStartRecordingHandler implements ServerRestHandler { + + private static final ServerStartRecordingHandler INSTANCE = new ServerStartRecordingHandler(); + + private static final Logger LOG = Logger.getLogger(ServerStartRecordingHandler.class); + + @Override + public void handle(ResteasyReactiveRequestContext requestContext) { + if (LOG.isDebugEnabled()) { + LOG.debug("Enter Jfr Reactive Request Filter"); + } + requestContext.requireCDIRequestScope(); + ReactiveServerRecorder recorder = Arc.container().instance(ReactiveServerRecorder.class).get(); + recorder + .createStartEvent() + .createAndStartPeriodEvent(); + } + + public static class Customizer implements HandlerChainCustomizer { + @Override + public List handlers(Phase phase, ResourceClass resourceClass, + ServerResourceMethod serverResourceMethod) { + if (phase == Phase.AFTER_PRE_MATCH) { + return Collections.singletonList(INSTANCE); + } + return Collections.emptyList(); + } + } +} diff --git a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index 35ce4ab24bdde..88419520a3ea5 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -191,6 +191,7 @@ import io.quarkus.resteasy.reactive.server.spi.AllowNotRestParametersBuildItem; import io.quarkus.resteasy.reactive.server.spi.AnnotationsTransformerBuildItem; import io.quarkus.resteasy.reactive.server.spi.ContextTypeBuildItem; +import io.quarkus.resteasy.reactive.server.spi.GlobalHandlerCustomizerBuildItem; import io.quarkus.resteasy.reactive.server.spi.HandlerConfigurationProviderBuildItem; import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem; import io.quarkus.resteasy.reactive.server.spi.NonBlockingReturnTypeBuildItem; @@ -1232,6 +1233,11 @@ public void additionalReflection(BeanArchiveIndexBuildItem beanArchiveIndexBuild } } + @BuildStep + public GlobalHandlerCustomizerBuildItem securityContextOverrideHandler() { + return new GlobalHandlerCustomizerBuildItem(new SecurityContextOverrideHandler.Customizer()); + } + @BuildStep @Record(value = ExecutionTime.STATIC_INIT, useIdentityComparisonForParameters = false) public void setupDeployment(BeanContainerBuildItem beanContainerBuildItem, @@ -1260,7 +1266,8 @@ public void setupDeployment(BeanContainerBuildItem beanContainerBuildItem, ContextResolversBuildItem contextResolversBuildItem, ResteasyReactiveServerConfig serverConfig, LaunchModeBuildItem launchModeBuildItem, - List resumeOn404Items) + List resumeOn404Items, + List globalHandlerCustomizers) throws NoSuchMethodException { if (!resourceScanningResultBuildItem.isPresent()) { @@ -1361,7 +1368,8 @@ public void setupDeployment(BeanContainerBuildItem beanContainerBuildItem, .setSerialisers(serialisers) .setPreExceptionMapperHandler(determinePreExceptionMapperHandler(preExceptionMapperHandlerBuildItems)) .setApplicationPath(applicationPath) - .setGlobalHandlerCustomizers(Collections.singletonList(new SecurityContextOverrideHandler.Customizer())) //TODO: should be pluggable + .setGlobalHandlerCustomizers(globalHandlerCustomizers.stream().map( + GlobalHandlerCustomizerBuildItem::getCustomizer).toList()) .setResourceClasses(resourceClasses) .setDevelopmentMode(launchModeBuildItem.getLaunchMode() == LaunchMode.DEVELOPMENT) .setLocatableResourceClasses(subResourceClasses) diff --git a/extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/GlobalHandlerCustomizerBuildItem.java b/extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/GlobalHandlerCustomizerBuildItem.java new file mode 100644 index 0000000000000..851a390b1987c --- /dev/null +++ b/extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/GlobalHandlerCustomizerBuildItem.java @@ -0,0 +1,22 @@ +package io.quarkus.resteasy.reactive.server.spi; + +import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Allows for extension to register global handler customizers. + * These are useful for adding handlers that run before and after pre matching + */ +public final class GlobalHandlerCustomizerBuildItem extends MultiBuildItem { + + private final HandlerChainCustomizer customizer; + + public GlobalHandlerCustomizerBuildItem(HandlerChainCustomizer customizer) { + this.customizer = customizer; + } + + public HandlerChainCustomizer getCustomizer() { + return customizer; + } +} diff --git a/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/AppResource.java b/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/AppResource.java index 0d69b1780272c..3045bbc4dd113 100644 --- a/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/AppResource.java +++ b/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/AppResource.java @@ -2,8 +2,11 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.MediaType; import io.quarkus.jfr.runtime.IdProducer; import io.smallrye.mutiny.Uni; @@ -31,6 +34,13 @@ public IdResponse blocking() { return new IdResponse(idProducer.getTraceId(), idProducer.getSpanId()); } + @POST + @Path("consuming") + @Consumes(MediaType.APPLICATION_JSON) + public IdResponse consuming(IdResponse idResponse) { + return new IdResponse(idProducer.getTraceId(), idProducer.getSpanId()); + } + @GET @Path("error") public void error() { diff --git a/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/JfrResource.java b/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/JfrResource.java index 4749eb3e42ce9..76c3a9c949310 100644 --- a/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/JfrResource.java +++ b/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/JfrResource.java @@ -6,9 +6,11 @@ import java.text.ParseException; import java.util.List; import java.util.Optional; +import java.util.function.Predicate; import jakarta.enterprise.context.ApplicationScoped; import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; @@ -50,9 +52,22 @@ public void stopJfr(@PathParam("name") String name) throws IOException { } @GET - @Path("check/{name}/{traceId}") + @Path("check/{name}/traceId/{traceId}") @Produces(MediaType.APPLICATION_JSON) - public JfrRestEventResponse check(@PathParam("name") String name, @PathParam("traceId") String traceId) throws IOException { + public JfrRestEventResponse checkForTraceId(@PathParam("name") String name, @PathParam("traceId") String traceId) + throws IOException { + return doCheck(name, (e) -> e.hasField("traceId") && e.getString("traceId").equals(traceId)); + } + + @GET + @Path("check/{name}/uri") + @Produces(MediaType.APPLICATION_JSON) + public JfrRestEventResponse checkForPath(@PathParam("name") String name, @HeaderParam("uri") String uri) + throws IOException { + return doCheck(name, (e) -> e.hasField("uri") && e.getString("uri").equals(uri)); + } + + private JfrRestEventResponse doCheck(String name, Predicate predicate) throws IOException { java.nio.file.Path dumpFile = Files.createTempFile("dump", "jfr"); Recording recording = getRecording(name); recording.dump(dumpFile); @@ -73,7 +88,7 @@ public JfrRestEventResponse check(@PathParam("name") String name, @PathParam("tr Log.debug(e); } } - if (e.hasField("traceId") && e.getString("traceId").equals(traceId)) { + if (predicate.test(e)) { if (RestPeriodEvent.class.getAnnotation(Name.class).value().equals(e.getEventType().getName())) { periodEvent = e; } else if (RestStartEvent.class.getAnnotation(Name.class).value().equals(e.getEventType().getName())) { diff --git a/integration-tests/jfr-reactive/src/test/java/io/quarkus/jfr/it/JfrTest.java b/integration-tests/jfr-reactive/src/test/java/io/quarkus/jfr/it/JfrTest.java index 81d7bb23b6d23..1b2999974dd94 100644 --- a/integration-tests/jfr-reactive/src/test/java/io/quarkus/jfr/it/JfrTest.java +++ b/integration-tests/jfr-reactive/src/test/java/io/quarkus/jfr/it/JfrTest.java @@ -16,7 +16,6 @@ public class JfrTest { private static final String CLIENT = "127.0.0.1:\\d{1,5}"; - private static final String HTTP_METHOD = "GET"; private static final String RESOURCE_CLASS = "io.quarkus.jfr.it.AppResource"; @Test @@ -46,14 +45,14 @@ public void blockingTest() { final String resourceMethod = "blocking"; ValidatableResponse validatableResponse = given() - .when().get("/jfr/check/" + jfrName + "/" + response.traceId) + .when().get("/jfr/check/" + jfrName + "/traceId/" + response.traceId) .then() .statusCode(200) .body("start", notNullValue()) .body("start.uri", is(url)) .body("start.traceId", is(response.traceId)) .body("start.spanId", is(response.spanId)) - .body("start.httpMethod", is(HTTP_METHOD)) + .body("start.httpMethod", is("GET")) .body("start.resourceClass", is(RESOURCE_CLASS)) .body("start.resourceMethod", is(resourceMethod)) .body("start.client", matchesRegex(CLIENT)) @@ -61,7 +60,7 @@ public void blockingTest() { .body("end.uri", is(url)) .body("end.traceId", is(response.traceId)) .body("end.spanId", is(response.spanId)) - .body("end.httpMethod", is(HTTP_METHOD)) + .body("end.httpMethod", is("GET")) .body("end.resourceClass", is(RESOURCE_CLASS)) .body("end.resourceMethod", is(resourceMethod)) .body("end.client", matchesRegex(CLIENT)) @@ -69,7 +68,7 @@ public void blockingTest() { .body("period.uri", is(url)) .body("period.traceId", is(response.traceId)) .body("period.spanId", is(response.spanId)) - .body("period.httpMethod", is(HTTP_METHOD)) + .body("period.httpMethod", is("GET")) .body("period.resourceClass", is(RESOURCE_CLASS)) .body("period.resourceMethod", is(resourceMethod)) .body("period.client", matchesRegex(CLIENT)); @@ -101,14 +100,14 @@ public void reactiveTest() { final String resourceMethod = "reactive"; ValidatableResponse validatableResponse = given() - .when().get("/jfr/check/" + jfrName + "/" + response.traceId) + .when().get("/jfr/check/" + jfrName + "/traceId/" + response.traceId) .then() .statusCode(200) .body("start", notNullValue()) .body("start.uri", is(url)) .body("start.traceId", is(response.traceId)) .body("start.spanId", is(response.spanId)) - .body("start.httpMethod", is(HTTP_METHOD)) + .body("start.httpMethod", is("GET")) .body("start.resourceClass", is(RESOURCE_CLASS)) .body("start.resourceMethod", is(resourceMethod)) .body("start.client", matchesRegex(CLIENT)) @@ -116,7 +115,7 @@ public void reactiveTest() { .body("end.uri", is(url)) .body("end.traceId", is(response.traceId)) .body("end.spanId", is(response.spanId)) - .body("end.httpMethod", is(HTTP_METHOD)) + .body("end.httpMethod", is("GET")) .body("end.resourceClass", is(RESOURCE_CLASS)) .body("end.resourceMethod", is(resourceMethod)) .body("end.client", matchesRegex(CLIENT)) @@ -124,7 +123,7 @@ public void reactiveTest() { .body("period.uri", is(url)) .body("period.traceId", is(response.traceId)) .body("period.spanId", is(response.spanId)) - .body("period.httpMethod", is(HTTP_METHOD)) + .body("period.httpMethod", is("GET")) .body("period.resourceClass", is(RESOURCE_CLASS)) .body("period.resourceMethod", is(resourceMethod)) .body("period.client", matchesRegex(CLIENT)); @@ -156,14 +155,14 @@ public void errorTest() { final String resourceMethod = "error"; ValidatableResponse validatableResponse = given() - .when().get("/jfr/check/" + jfrName + "/" + traceId) + .when().get("/jfr/check/" + jfrName + "/traceId/" + traceId) .then() .statusCode(200) .body("start", notNullValue()) .body("start.uri", is(url)) .body("start.traceId", is(traceId)) .body("start.spanId", nullValue()) - .body("start.httpMethod", is(HTTP_METHOD)) + .body("start.httpMethod", is("GET")) .body("start.resourceClass", is(RESOURCE_CLASS)) .body("start.resourceMethod", is(resourceMethod)) .body("start.client", matchesRegex(CLIENT)) @@ -171,7 +170,7 @@ public void errorTest() { .body("end.uri", is(url)) .body("end.traceId", is(traceId)) .body("end.spanId", is(nullValue())) - .body("end.httpMethod", is(HTTP_METHOD)) + .body("end.httpMethod", is("GET")) .body("end.resourceClass", is(RESOURCE_CLASS)) .body("end.resourceMethod", is(resourceMethod)) .body("end.client", matchesRegex(CLIENT)) @@ -179,7 +178,7 @@ public void errorTest() { .body("period.uri", is(url)) .body("period.traceId", is(traceId)) .body("period.spanId", is(nullValue())) - .body("period.httpMethod", is(HTTP_METHOD)) + .body("period.httpMethod", is("GET")) .body("period.resourceClass", is(RESOURCE_CLASS)) .body("period.resourceMethod", is(resourceMethod)) .body("period.client", matchesRegex(CLIENT)); @@ -214,4 +213,111 @@ public void nonExistURL() { Assertions.assertEquals(0, count); } + + @Test + public void invalidHttpMethod() { + String jfrName = "invalidHttpMethodTest"; + + given() + .when().get("/jfr/start/" + jfrName) + .then() + .statusCode(204); + + String url = "/app/blocking"; + given() + .when() + .post(url) + .then() + .statusCode(405); + + given() + .when().get("/jfr/stop/" + jfrName) + .then() + .statusCode(204); + + given() + .header("uri", url) + .when().get("/jfr/check/" + jfrName + "/uri") + .then() + .statusCode(200) + .body("start", notNullValue()) + .body("start.uri", is(url)) + .body("start.traceId", notNullValue()) + .body("start.spanId", nullValue()) + .body("start.httpMethod", is("POST")) + .body("start.resourceClass", nullValue()) + .body("start.resourceMethod", nullValue()) + .body("start.client", matchesRegex(CLIENT)) + .body("end", notNullValue()) + .body("end.uri", is(url)) + .body("end.traceId", notNullValue()) + .body("end.spanId", nullValue()) + .body("end.httpMethod", is("POST")) + .body("end.resourceClass", nullValue()) + .body("end.resourceMethod", nullValue()) + .body("end.client", matchesRegex(CLIENT)) + .body("period", notNullValue()) + .body("period.uri", is(url)) + .body("period.traceId", notNullValue()) + .body("period.spanId", nullValue()) + .body("period.httpMethod", is("POST")) + .body("period.resourceClass", nullValue()) + .body("period.resourceMethod", nullValue()) + .body("period.client", matchesRegex(CLIENT)); + } + + @Test + public void unhandledContentType() { + String jfrName = "unhandledContentType"; + + given() + .when().get("/jfr/start/" + jfrName) + .then() + .statusCode(204); + + String url = "/app/consuming"; + given() + .contentType("text/plain") + .body("whatever") + .when() + .post(url) + .then() + .statusCode(415); + + given() + .when().get("/jfr/stop/" + jfrName) + .then() + .statusCode(204); + + given() + .header("uri", url) + .when().get("/jfr/check/" + jfrName + "/uri") + .then() + .statusCode(200) + .body("start", notNullValue()) + .body("start.uri", is(url)) + .body("start.traceId", notNullValue()) + .body("start.spanId", nullValue()) + .body("start.httpMethod", is("POST")) + .body("start.resourceClass", nullValue()) + .body("start.resourceMethod", nullValue()) + .body("start.client", matchesRegex(CLIENT)) + .body("end", notNullValue()) + .body("end.uri", is(url)) + .body("end.traceId", notNullValue()) + .body("end.spanId", nullValue()) + .body("end.httpMethod", is("POST")) + .body("end.resourceClass", nullValue()) + .body("end.resourceMethod", nullValue()) + .body("end.client", matchesRegex(CLIENT)) + .body("period", notNullValue()) + .body("period.uri", is(url)) + .body("period.traceId", notNullValue()) + .body("period.spanId", nullValue()) + .body("period.httpMethod", is("POST")) + .body("period.resourceClass", nullValue()) + .body("period.resourceMethod", nullValue()) + .body("period.client", matchesRegex(CLIENT)); + } + }