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 be1d52d4dabd4..413492800c160 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; @@ -1236,6 +1237,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, @@ -1264,7 +1270,8 @@ public void setupDeployment(BeanContainerBuildItem beanContainerBuildItem, ContextResolversBuildItem contextResolversBuildItem, ResteasyReactiveServerConfig serverConfig, LaunchModeBuildItem launchModeBuildItem, - List resumeOn404Items) + List resumeOn404Items, + List globalHandlerCustomizers) throws NoSuchMethodException { if (!resourceScanningResultBuildItem.isPresent()) { @@ -1365,7 +1372,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)); + } + }