From 21a96570c1d16c56489000893cd276ae4542ced7 Mon Sep 17 00:00:00 2001
From: Georgios Andrianakis <geoand@gmail.com>
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<CustomContainerRequestFilterBuildItem> filterBeans,
-            BuildProducer<AdditionalBeanBuildItem> additionalBeans) {
+            BuildProducer<AdditionalBeanBuildItem> additionalBeans,
+            BuildProducer<GlobalHandlerCustomizerBuildItem> 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<ServerRestHandler> 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<ResumeOn404BuildItem> resumeOn404Items)
+            List<ResumeOn404BuildItem> resumeOn404Items,
+            List<GlobalHandlerCustomizerBuildItem> 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<RecordedEvent> 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));
+    }
+
 }