diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 71506d6a43ca3..f1d751b651232 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -6331,6 +6331,18 @@ + + + io.quarkus + quarkus-jfr + ${project.version} + + + io.quarkus + quarkus-jfr-deployment + ${project.version} + + io.quarkus diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml index c7e2908451a7f..078055e35d884 100644 --- a/devtools/bom-descriptor-json/pom.xml +++ b/devtools/bom-descriptor-json/pom.xml @@ -1110,6 +1110,19 @@ + + io.quarkus + quarkus-jfr + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-jsonb diff --git a/docs/pom.xml b/docs/pom.xml index bec1fb8c038d4..423fa5c37ddde 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -1126,6 +1126,19 @@ + + io.quarkus + quarkus-jfr-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-jsonb-deployment diff --git a/docs/src/main/asciidoc/images/jfr-edit-thread-activity-lanes.png b/docs/src/main/asciidoc/images/jfr-edit-thread-activity-lanes.png new file mode 100644 index 0000000000000..3dd185c779e16 Binary files /dev/null and b/docs/src/main/asciidoc/images/jfr-edit-thread-activity-lanes.png differ diff --git a/docs/src/main/asciidoc/images/jfr-event-browser.png b/docs/src/main/asciidoc/images/jfr-event-browser.png new file mode 100644 index 0000000000000..081312999833c Binary files /dev/null and b/docs/src/main/asciidoc/images/jfr-event-browser.png differ diff --git a/docs/src/main/asciidoc/images/jfr-java-ap-thread.png b/docs/src/main/asciidoc/images/jfr-java-ap-thread.png new file mode 100644 index 0000000000000..a950e9830594f Binary files /dev/null and b/docs/src/main/asciidoc/images/jfr-java-ap-thread.png differ diff --git a/docs/src/main/asciidoc/images/jfr-thread.png b/docs/src/main/asciidoc/images/jfr-thread.png new file mode 100644 index 0000000000000..0ab0db144e823 Binary files /dev/null and b/docs/src/main/asciidoc/images/jfr-thread.png differ diff --git a/docs/src/main/asciidoc/jfr.adoc b/docs/src/main/asciidoc/jfr.adoc new file mode 100644 index 0000000000000..c5ada66c7025a --- /dev/null +++ b/docs/src/main/asciidoc/jfr.adoc @@ -0,0 +1,364 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// += Using JDK Flight Recorder +include::_attributes.adoc[] +:categories: observability +:summary: This guide explains how JDK Flight Recorder can be extended to provide insight into your Quarkus application. +:topics: observability,jfr +:extensions: io.quarkus:quarkus-jfr + +This guide explains how https://openjdk.org/jeps/328[JDK Flight Recorder] (JFR) can be extended to provide insight into your Quarkus application. +insight into itself. +JFR records various information from the Java standard API and JVM as events. +By adding this extension, you can add custom Quarkus events to JFR. This will help you solve problems in your application. + +JFR can be preconfigured to dump a file, and when the application exits, JFR will output the file. +The file will contain the contents of the JFR event stream to which Quarkus custom events have also been added. +You can, of course, get this file at any time you want, even if your application exits unexpectedly. + +== Prerequisites + +:prerequisites: +include::{includes}/prerequisites.adoc[] + +== Architecture + +In this guide, we create a straightforward REST application to demonstrate JFR. + +== Creating the Maven project + +First, we need a new project. Create a new project with the following command: + +:create-app-artifact-id: jfr-quickstart +:create-app-extensions: quarkus-rest,quarkus-jfr +include::{includes}/devtools/create-app.adoc[] + +This command generates the Maven project and imports the `quarkus-jfr` extension, +which includes the default JFR support. + +If you already have your Quarkus project configured, you can add the `quarkus-jfr` extension +to your project by running the following command in your project base directory: + +:add-extension-extensions: quarkus-jfr +include::{includes}/devtools/extension-add.adoc[] + +This will add the following to your build file: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-jfr + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.quarkus:quarkus-jfr") +---- + +=== Examine the Jakarta REST resource + +Create a `src/main/java/org/acme/jfr/JfrResource.java` file with the following content: + +[source,java] +---- +package org.acme.jfr; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +public class JfrResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "hello"; + } +} +---- + +Notice that there is no JFR specific code included in the application. By default, requests sent to this +endpoint will be recorded into JFR without any required code changes. + +=== Running Quarkus applications and JFR + +Now we are ready to run our application. +We can launch the application with JFR configured to be enabled from the startup of the Java Virtual Machine. + +:dev-additional-parameters: -Djvm.args="-XX:StartFlightRecording=name=quarkus,dumponexit=true,filename=myrecording.jfr" +include::{includes}/devtools/dev.adoc[] + +With the JDK Flight Recorder and the application running, we can make a request to the provided endpoint: + +[source,shell] +---- +$ curl http://localhost:8080/hello +hello +---- + +This is all that was needed to write the information to the JFR. + +=== Save the JFR to a file +As mentioned above, the Quarkus application was configured to also start JFR at startup and dump it to a `myrecording.jfr` when it terminates. +So we can get the file when we hit `CTRL+C` or type `q` to stop the application. + +Or, we can also dump with the jcmd command. + +---- +jcmd JFR.dump name=quarkus filename=myrecording.jfr +---- + +[NOTE] +==== +Running jcmd command give us a list of running Java processes and the PID of each process. +==== + +== Open JFR dump file + +We can open a JFR dump using two tools: Jfr CLI and JDK Mission Control (JMC). +It is also possible to read them using JFR APIs, but we won't go into that here. + +=== jfr CLI + +The jfr CLI is a tool included in OpenJDK. The executable file is `$JAVA_HOME/bin/jfr`. +We can use the jfr CLI to see a list of events limited to those related to Quarkus in the dump file by doing the following. + +---- +jfr print --categories quarkus myrecording.jfr +---- + +=== JDK Mission Control + +JMC is essentially a GUI viewer for JFR. +Some distributions include JMC in OpenJDK binary, but if not, we need to download it manually. +To see a list of events using the JMC, first we load the JFR file in the JMC as follows. + +---- +jmc -open myrecording.jfr +---- + +After opening the JFR file, we have two options. +One is to view the events as a tabular list, and the other is to view the events on the threads in which they occurred, in chronological order. + +To view Quarkus events in tabular style, select the Event Browser on the left side of the JMC, then open the Quarkus event type tree on the right side of the JMC. + +image::jfr-event-browser.png[alt=JDK Mission Control Event Browser view,role="center"] + +To see Quarkus events in chronological order on a thread, select the `Java application` and `Threads` on the left side of the JMC. + +image::jfr-java-ap-thread.png[alt=JDK Mission Control thread view,role="center"] + +The standard configuration does not show Quarkus events. +We have to do three tasks to see the Quarkus events. + +1. Right-click and select `Edit Thread Activity Lanes...`. +2. Select the plus button to add a new lane on the left side, then check to display that lane. +3. Select Quarkus as the event type that lane will display and press OK. + +image::jfr-edit-thread-activity-lanes.png[alt=JDK Mission Control Edit Thread Activity Lanes,role="center"] + +Now we can see the Quarkus events per thread. + +image::jfr-thread.png[alt=JDK Mission Control thread view,role="center"] + +[NOTE] +==== +Non-blocking is where multiple processes are processed apparently simultaneously in a single thread. +Therefore, this extension records multiple JFR events concurrently, and a number of events might overlap on the JMC. +This could make it difficult for you to see the events you want to see. +To avoid this, we recommend to use xref:#identifying-requests[Request ID] to filter events so that you only see the information about the requests you want to see. +==== + +== Events + +=== Identifying Requests +This extension works with the OpenTelemetry extension. +The events recorded by this extension have a trace ID and a span ID. These are recorded with the OpenTelemetry IDs respectively. + +This means that after we identify the trace and span IDs of interest from the UI provided by the OpenTelemetry implementation, we can immediately jump to the details in JFR using those IDs. + +If we have not enabled the OpenTelemetry extension, this extension creates an ID for each request and links it to JFR events as a traceId. +In this case, the span ID will be null. + +For now, Quarkus only has REST events, but we plan to use this ID to link each event to each other as we add more events in the future. + +=== Event Implementation Policy +When JFR starts recording an event, the event does not record to JFR yet, but when it commits that event, the event is recorded. +Therefore, events that have started recording at dump time but have not yet been committed are not dumped. +This is unavoidable due to the design of JFR. +This means that events are not recorded forever if there are prolonged processing. +Therefore, you will not be aware of prolonged processing. + +To solve this problem, Quarkus can also record start and end events at the beginning and end of processing. +These events are disabled by default. +However, we can enable these events on JFR.(described below) + +=== REST API Event +This event is recorded when either `quarkus-rest` or `resteasy-classic` extension is enabled. +The following three JFR events are recorded as soon as REST server processing is complete. + +- REST +- REST Start +- REST End + +REST Event records the time period from the start of the REST process to the end of the REST server process. + +REST Start Event records the start of the REST server process. + +REST End Event records the end of the REST server process. + +These events have the following information. + +- HTTP Method +- URI +- Resource Class +- Resource Method +- Client + +HTTP Method records the HTTP Method accessed by the client. +This will record a string of HTTP methods such as GET, POST, DELETE, and other general HTTP methods. + +URI records the URI path accessed by the client. +This does not include host names or port numbers. + +Resource Class records the class that was executed. +We can check whether the Resource Class was executed as expected by the HTTP Method and URI. + +Resource Method records the method that was executed. +We can check if the Resource Method was executed as expected by the HTTP Method and URI. + +Client records information about the accessing client. + +=== Native Image +Native image supports JDK Flight Recorder. +This extension also supports native images. +To enable JFR on Native image, it is usually built with `--enable-monitoring`. +However, we can enable JFR in Quarkus Native images by adding `jfr` to the configuration `quarkus.native.monitoring`. +There are two ways to set up this configuration: by including it in `application.properties` or by specifying it at build time. + +The first method is to first configure settings in `application.properties`. + +application.properties +``` +quarkus.native.monitoring=jfr +``` +Next, simply build as `./mvnw package -Dnative`. + +The second way is to give `-Dquarkus.native.monitoring=jfr` at build time and build as `./mvnw package -Dnative -Dquarkus.native.monitoring=jfr`. + +Once we have finished building the Native image, we can run the native application with JFR as follows + +``` +target/your-application-runner -XX:StartFlightRecording=name=quarkus,dumponexit=true,filename=myrecording.jfr +``` + +[NOTE] +==== +Note that at this time, GraalVM is not possible to record JFR on Windows native images. +==== + +== JFR configuration + +We can use the JFR CLI to configure the events that JFR will record. +The configuration file, JFC file, is in XML format, so we can modify with a text editor. +However, we should use `jfr configure`, which is included in OpenJDK by default. + +Here we create a configuration file in which RestStart and RestEnd events are recorded, which are not recorded by default. +---- +jfr configure --input default.jfc +quarkus.RestStart#enabled=true +quarkus.RestEnd#enabled=true --output custom-rest.jfc +---- +This creates `custom-rest.jfc` as a configuration file with RestStart and RestEnd enabled. + +Now we are ready to run our application with new settings. We launch the application with JFR configured to be enabled from the startup of the Java Virtual Machine. + +:dev-additional-parameters: -Djvm.args="-XX:StartFlightRecording=name=quarkus,settings=./custom-rest.jfc,dumponexit=true,filename=myrecording.jfr" +include::{includes}/devtools/dev.adoc[] + + +== Developing new events into quarkus-jfr extension. + +This section is for those who would like to add new events with this extension. + +We recommend that new events be associated with existing events. +Associations are useful when looking at the details of a process that is taking a long time. +For example, a general REST application retrieves the data needed for processing from a data store. +If REST events are not associated with datastore events, it is impossible to know which datastore events were processed in each REST request. +When the two events are associated, we know immediately which datastore event was processed in each REST request. + +[NOTE] +==== +Data store events are not implemented yet. +==== + +The quarkus-jfr extension provides a Request ID for event association. +See Identifying Requests for more information on Request IDs. + +In specific code, the following two steps are required. +First, implement `traceId` and `spanId` on the new event as follows +The `@TraceIdRelational` and `@SpanIdRelational` attached to these events will provide the association. + +[source,java] +---- +import io.quarkus.jfr.runtime.SpanIdRelational; +import io.quarkus.jfr.runtime.TraceIdRelational; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Label; + +public class NewEvent extends Event { + + @Label("Trace ID") + @Description("Trace ID to identify the request") + @TraceIdRelational + protected String traceId; + + @Label("Span ID") + @Description("Span ID to identify the request if necessary") + @SpanIdRelational + protected String spanId; + + // other properties which you want to add + // setters and getters +} +---- + +Then you get the information to store in them from the `IdProducer` object's `getTraceId()` and `getSpanId()`. + +[source,java] +---- +import io.quarkus.jfr.runtime.IdProducer; + +public class NewInterceptor { + + @Inject + IdProducer idProducer; + + private void recordedInvoke() { + NewEvent event = new NewEvent(); + event.begin(); + + // The process you want to measure + + event.end(); + if (endEvent.shouldCommit()) { + event.setTraceId(idProducer.getTraceId()); + event.setSpanId(idProducer.getSpanId()); + // call other setters which you want to add + endEvent.commit(); + } + } +} +---- + +== quarkus-jfr Configuration Reference + +include::{generated-dir}/config/quarkus-jfr.adoc[leveloffset=+1, opts=optional] \ No newline at end of file diff --git a/extensions/jfr/deployment/pom.xml b/extensions/jfr/deployment/pom.xml new file mode 100644 index 0000000000000..d224bcc3c8311 --- /dev/null +++ b/extensions/jfr/deployment/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + io.quarkus + quarkus-jfr-parent + 999-SNAPSHOT + + quarkus-jfr-deployment + Quarkus - Jfr - Deployment + + + io.quarkus + quarkus-jfr + ${project.version} + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-resteasy-common-spi + + + io.quarkus + quarkus-rest-server-spi-deployment + + + + io.quarkus + quarkus-junit5-internal + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + 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 new file mode 100644 index 0000000000000..14936fd93c4cb --- /dev/null +++ b/extensions/jfr/deployment/src/main/java/io/quarkus/jfr/deployment/JfrProcessor.java @@ -0,0 +1,89 @@ +package io.quarkus.jfr.deployment; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; +import io.quarkus.deployment.annotations.*; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; +import io.quarkus.jfr.runtime.JfrRecorder; +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.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; +import io.quarkus.resteasy.reactive.spi.CustomContainerRequestFilterBuildItem; + +@BuildSteps +public class JfrProcessor { + + private static final String FEATURE = "jfr"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + void registerRequestIdProducer(Capabilities capabilities, + BuildProducer additionalBeans, + BuildProducer runtimeInitializedClassBuildItem) { + + if (capabilities.isPresent(Capability.OPENTELEMETRY_TRACER)) { + + additionalBeans.produce(AdditionalBeanBuildItem.builder().setUnremovable() + .addBeanClasses(OTelIdProducer.class) + .build()); + + } else { + + additionalBeans.produce(AdditionalBeanBuildItem.builder().setUnremovable() + .addBeanClasses(QuarkusIdProducer.class) + .build()); + + runtimeInitializedClassBuildItem + .produce(new RuntimeInitializedClassBuildItem(QuarkusIdProducer.class.getCanonicalName())); + } + } + + @BuildStep + void registerReactiveResteasyIntegration(Capabilities capabilities, + BuildProducer filterBeans, + BuildProducer additionalBeans) { + + if (capabilities.isPresent(Capability.RESTEASY_REACTIVE)) { + + additionalBeans.produce(AdditionalBeanBuildItem.builder().setUnremovable() + .addBeanClasses(ReactiveServerRecorderProducer.class) + .build()); + + filterBeans + .produce(new CustomContainerRequestFilterBuildItem(JfrReactiveServerFilter.class.getName())); + } + } + + @BuildStep + void registerResteasyClassicIntegration(Capabilities capabilities, + BuildProducer resteasyJaxrsProviderBuildItemBuildProducer, + BuildProducer additionalBeans) { + if (capabilities.isPresent(Capability.RESTEASY)) { + + additionalBeans.produce(AdditionalBeanBuildItem.builder().setUnremovable() + .addBeanClasses(ClassicServerRecorderProducer.class) + .build()); + + resteasyJaxrsProviderBuildItemBuildProducer + .produce(new ResteasyJaxrsProviderBuildItem(JfrClassicServerFilter.class.getName())); + } + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public void runtimeInit(JfrRecorder recorder, JfrRuntimeConfig runtimeConfig) { + recorder.runtimeInit(runtimeConfig); + } +} diff --git a/extensions/jfr/deployment/src/test/java/io/quarkus/jfr/test/JfrConfigurationTest.java b/extensions/jfr/deployment/src/test/java/io/quarkus/jfr/test/JfrConfigurationTest.java new file mode 100644 index 0000000000000..55816bfc7b871 --- /dev/null +++ b/extensions/jfr/deployment/src/test/java/io/quarkus/jfr/test/JfrConfigurationTest.java @@ -0,0 +1,28 @@ +package io.quarkus.jfr.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.jfr.runtime.config.JfrRuntimeConfig; +import io.quarkus.test.QuarkusUnitTest; + +public class JfrConfigurationTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .overrideConfigKey("quarkus.jfr.enabled", "false") + .overrideConfigKey("quarkus.jfr.rest.enabled", "false"); + + @Inject + JfrRuntimeConfig runtimeConfig; + + @Test + void config() { + assertFalse(runtimeConfig.enabled()); + assertFalse(runtimeConfig.restEnabled()); + } +} diff --git a/extensions/jfr/deployment/src/test/java/io/quarkus/jfr/test/JfrDevModeTest.java b/extensions/jfr/deployment/src/test/java/io/quarkus/jfr/test/JfrDevModeTest.java new file mode 100644 index 0000000000000..6ddb8fb5d4db7 --- /dev/null +++ b/extensions/jfr/deployment/src/test/java/io/quarkus/jfr/test/JfrDevModeTest.java @@ -0,0 +1,23 @@ +package io.quarkus.jfr.test; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; + +public class JfrDevModeTest { + + // Start hot reload (DevMode) test with your extension loaded + @RegisterExtension + static final QuarkusDevModeTest devModeTest = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Test + public void writeYourOwnDevModeTest() { + // Write your dev mode tests here - see the testing extension guide https://quarkus.io/guides/writing-extensions#testing-hot-reload for more information + Assertions.assertTrue(true, "Add dev mode assertions to " + getClass().getName()); + } +} diff --git a/extensions/jfr/deployment/src/test/java/io/quarkus/jfr/test/JfrTest.java b/extensions/jfr/deployment/src/test/java/io/quarkus/jfr/test/JfrTest.java new file mode 100644 index 0000000000000..48bbba92e23eb --- /dev/null +++ b/extensions/jfr/deployment/src/test/java/io/quarkus/jfr/test/JfrTest.java @@ -0,0 +1,23 @@ +package io.quarkus.jfr.test; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class JfrTest { + + // Start unit test with your extension loaded + @RegisterExtension + static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Test + public void writeYourOwnUnitTest() { + // Write your unit tests here - see the testing extension guide https://quarkus.io/guides/writing-extensions#testing-extensions for more information + Assertions.assertTrue(true, "Add some assertions to " + getClass().getName()); + } +} diff --git a/extensions/jfr/pom.xml b/extensions/jfr/pom.xml new file mode 100644 index 0000000000000..60f24088b364e --- /dev/null +++ b/extensions/jfr/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + io.quarkus + quarkus-extensions-parent + 999-SNAPSHOT + ../pom.xml + + quarkus-jfr-parent + pom + Quarkus - Jfr - Parent + + deployment + runtime + + + + + io.quarkus + quarkus-bom + ${project.version} + pom + import + + + + diff --git a/extensions/jfr/runtime/pom.xml b/extensions/jfr/runtime/pom.xml new file mode 100644 index 0000000000000..78c99cab07e42 --- /dev/null +++ b/extensions/jfr/runtime/pom.xml @@ -0,0 +1,72 @@ + + + 4.0.0 + + io.quarkus + quarkus-jfr-parent + 999-SNAPSHOT + + quarkus-jfr + Quarkus - Jfr - Runtime + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-rest + provided + + + + + io.quarkus + quarkus-opentelemetry + provided + true + + + + + jakarta.ws.rs + jakarta.ws.rs-api + 3.1.0 + + + + + + io.quarkus + quarkus-extension-maven-plugin + ${project.version} + + + compile + + extension-descriptor + + + ${project.groupId}:${project.artifactId}-deployment:${project.version} + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/IdProducer.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/IdProducer.java new file mode 100644 index 0000000000000..48f47d644e5e9 --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/IdProducer.java @@ -0,0 +1,8 @@ +package io.quarkus.jfr.runtime; + +public interface IdProducer { + + String getTraceId(); + + String getSpanId(); +} diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/JfrRecorder.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/JfrRecorder.java new file mode 100644 index 0000000000000..95f1e217bd7f3 --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/JfrRecorder.java @@ -0,0 +1,39 @@ +package io.quarkus.jfr.runtime; + +import org.jboss.logging.Logger; + +import io.quarkus.jfr.runtime.config.JfrRuntimeConfig; +import io.quarkus.jfr.runtime.http.rest.RestEndEvent; +import io.quarkus.jfr.runtime.http.rest.RestPeriodEvent; +import io.quarkus.jfr.runtime.http.rest.RestStartEvent; +import io.quarkus.runtime.annotations.Recorder; +import jdk.jfr.FlightRecorder; + +@Recorder +public class JfrRecorder { + + public void runtimeInit(JfrRuntimeConfig runtimeConfig) { + + Logger logger = Logger.getLogger(JfrRecorder.class); + + if (!runtimeConfig.enabled()) { + logger.info("quarkus-jfr is disabled at runtime"); + this.disabledQuarkusJfr(); + } else { + if (!runtimeConfig.restEnabled()) { + logger.info("quarkus-jfr for REST server is disabled at runtime"); + this.disabledRestJfr(); + } + } + } + + public void disabledRestJfr() { + FlightRecorder.unregister(RestStartEvent.class); + FlightRecorder.unregister(RestEndEvent.class); + FlightRecorder.unregister(RestPeriodEvent.class); + } + + public void disabledQuarkusJfr() { + this.disabledRestJfr(); + } +} diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/OTelIdProducer.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/OTelIdProducer.java new file mode 100644 index 0000000000000..a806915c8f2a9 --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/OTelIdProducer.java @@ -0,0 +1,23 @@ +package io.quarkus.jfr.runtime; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; + +import io.opentelemetry.api.trace.Span; + +@RequestScoped +public class OTelIdProducer implements IdProducer { + + @Inject + Span span; + + @Override + public String getTraceId() { + return span.getSpanContext().getTraceId(); + } + + @Override + public String getSpanId() { + return span.getSpanContext().getSpanId(); + } +} diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/QuarkusIdProducer.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/QuarkusIdProducer.java new file mode 100644 index 0000000000000..e6f37f6b13ccc --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/QuarkusIdProducer.java @@ -0,0 +1,29 @@ +package io.quarkus.jfr.runtime; + +import java.security.SecureRandom; +import java.util.HexFormat; + +import jakarta.enterprise.context.RequestScoped; + +@RequestScoped +public class QuarkusIdProducer implements IdProducer { + + private static final SecureRandom random = new SecureRandom(); + private final String traceId; + + public QuarkusIdProducer() { + final byte[] bytes = new byte[16]; + random.nextBytes(bytes); + traceId = HexFormat.of().formatHex(bytes); + } + + @Override + public String getTraceId() { + return traceId; + } + + @Override + public String getSpanId() { + return null; + } +} diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/SpanIdRelational.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/SpanIdRelational.java new file mode 100644 index 0000000000000..ad3aa4662e5af --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/SpanIdRelational.java @@ -0,0 +1,26 @@ +package io.quarkus.jfr.runtime; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jdk.jfr.Description; +import jdk.jfr.Label; +import jdk.jfr.MetadataDefinition; +import jdk.jfr.Name; +import jdk.jfr.Relational; + +/** + * This is an annotation that associates multiple events in JFR by SpanId. + * Fields that can be annotated with this annotation must be in the String class. + */ +@Relational +@MetadataDefinition +@Name("io.quarkus.SpanId") +@Label("Span ID") +@Description("Links spans with the same ID together") +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SpanIdRelational { +} diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/TraceIdRelational.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/TraceIdRelational.java new file mode 100644 index 0000000000000..2d9b0c7406453 --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/TraceIdRelational.java @@ -0,0 +1,26 @@ +package io.quarkus.jfr.runtime; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jdk.jfr.Description; +import jdk.jfr.Label; +import jdk.jfr.MetadataDefinition; +import jdk.jfr.Name; +import jdk.jfr.Relational; + +/** + * This is an annotation that associates multiple events in JFR by TraceId. + * Fields that can be annotated with this annotation must be in the String class. + */ +@Relational +@MetadataDefinition +@Name("io.quarkus.TraceId") +@Label("Trace ID") +@Description("Links traces with the same ID together") +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface TraceIdRelational { +} diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/config/JfrRuntimeConfig.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/config/JfrRuntimeConfig.java new file mode 100644 index 0000000000000..c2293206b50e0 --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/config/JfrRuntimeConfig.java @@ -0,0 +1,30 @@ +package io.quarkus.jfr.runtime.config; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; +import io.smallrye.config.WithName; + +@ConfigMapping(prefix = "quarkus.jfr") +@ConfigRoot(phase = ConfigPhase.RUN_TIME) +public interface JfrRuntimeConfig { + + /** + * If false, only quarkus-jfr events are not recorded even if JFR is enabled. + * In this case, Java standard API and virtual machine information will be recorded according to the setting. + * Default value is true + */ + @WithDefault("true") + boolean enabled(); + + /** + * If false, only REST events in quarkus-jfr are not recorded even if JFR is enabled. + * In this case, other quarkus-jfr, Java standard API and virtual machine information will be recorded according to the + * setting. + * Default value is true + */ + @WithName("rest.enabled") + @WithDefault("true") + boolean restEnabled(); +} diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/AbstractHttpEvent.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/AbstractHttpEvent.java new file mode 100644 index 0000000000000..7df4a69470d95 --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/AbstractHttpEvent.java @@ -0,0 +1,68 @@ +package io.quarkus.jfr.runtime.http; + +import io.quarkus.jfr.runtime.SpanIdRelational; +import io.quarkus.jfr.runtime.TraceIdRelational; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Label; + +public abstract class AbstractHttpEvent extends Event { + + @Label("Trace ID") + @Description("Trace ID to identify the request") + @TraceIdRelational + protected String traceId; + + @Label("Span ID") + @Description("Span ID to identify the request if necessary") + @SpanIdRelational + protected String spanId; + + @Label("HTTP Method") + @Description("HTTP Method accessed by the client") + protected String httpMethod; + + @Label("URI") + @Description("URI accessed by the client") + protected String uri; + + @Label("Resource Class") + @Description("Class name executed by Quarkus") + protected String resourceClass; + + @Label("Resource Method") + @Description("Method name executed by Quarkus") + protected String resourceMethod; + + @Label("Client") + @Description("Client accessed") + protected String client; + + public void setTraceId(String traceId) { + this.traceId = traceId; + } + + public void setSpanId(String spanId) { + this.spanId = spanId; + } + + public void setHttpMethod(String httpMethod) { + this.httpMethod = httpMethod; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public void setResourceClass(String resourceClass) { + this.resourceClass = resourceClass; + } + + public void setResourceMethod(String resourceMethod) { + this.resourceMethod = resourceMethod; + } + + public void setClient(String client) { + this.client = client; + } +} 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/ClassicServerRecorderProducer.java new file mode 100644 index 0000000000000..9ee161e302ade --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/ClassicServerRecorderProducer.java @@ -0,0 +1,39 @@ +package io.quarkus.jfr.runtime.http.rest; + +import java.lang.reflect.Method; + +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import jakarta.ws.rs.container.ResourceInfo; + +import io.quarkus.jfr.runtime.IdProducer; +import io.vertx.core.http.HttpServerRequest; + +@Dependent +public class ClassicServerRecorderProducer { + + @Inject + HttpServerRequest vertxRequest; + + @Inject + ResourceInfo 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(); + Method resourceMethod = resourceInfo.getResourceMethod(); + String resourceMethodName = (resourceMethod == null) ? null : resourceMethod.getName(); + 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/JfrClassicServerFilter.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/JfrClassicServerFilter.java new file mode 100644 index 0000000000000..e019c5dfb6710 --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/JfrClassicServerFilter.java @@ -0,0 +1,52 @@ +package io.quarkus.jfr.runtime.http.rest; + +import java.io.IOException; + +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Provider; + +import org.jboss.logging.Logger; + +import io.quarkus.arc.Arc; + +@Provider +public class JfrClassicServerFilter implements ContainerRequestFilter, ContainerResponseFilter { + + private static final Logger LOG = Logger.getLogger(JfrClassicServerFilter.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(); + recorder.recordStartEvent(); + recorder.startPeriodEvent(); + } + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + if (LOG.isDebugEnabled()) { + LOG.debug("Enter Jfr Classic Response Filter"); + } + + if (isRecordable(responseContext)) { + Recorder recorder = Arc.container().instance(Recorder.class).get(); + 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/JfrReactiveServerFilter.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/JfrReactiveServerFilter.java new file mode 100644 index 0000000000000..46f14bdead66e --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/JfrReactiveServerFilter.java @@ -0,0 +1,45 @@ +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 new file mode 100644 index 0000000000000..393e50c84848e --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/ReactiveServerRecorderProducer.java @@ -0,0 +1,38 @@ +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 new file mode 100644 index 0000000000000..fc8535b773d67 --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/Recorder.java @@ -0,0 +1,12 @@ +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/RestEndEvent.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/RestEndEvent.java new file mode 100644 index 0000000000000..d020413fb8aea --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/RestEndEvent.java @@ -0,0 +1,18 @@ +package io.quarkus.jfr.runtime.http.rest; + +import io.quarkus.jfr.runtime.http.AbstractHttpEvent; +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Enabled; +import jdk.jfr.Label; +import jdk.jfr.Name; +import jdk.jfr.StackTrace; + +@Label("REST End") +@Category({ "Quarkus", "HTTP" }) +@Name("quarkus.RestEnd") +@Description("REST Server processing has completed") +@StackTrace(false) +@Enabled(false) +public class RestEndEvent extends AbstractHttpEvent { +} diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/RestPeriodEvent.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/RestPeriodEvent.java new file mode 100644 index 0000000000000..871d4d9e35aca --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/RestPeriodEvent.java @@ -0,0 +1,16 @@ +package io.quarkus.jfr.runtime.http.rest; + +import io.quarkus.jfr.runtime.http.AbstractHttpEvent; +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Label; +import jdk.jfr.Name; +import jdk.jfr.StackTrace; + +@Label("REST") +@Category({ "Quarkus", "HTTP" }) +@Name("quarkus.Rest") +@Description("REST Server has been processing during this period") +@StackTrace(false) +public class RestPeriodEvent extends AbstractHttpEvent { +} diff --git a/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/RestStartEvent.java b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/RestStartEvent.java new file mode 100644 index 0000000000000..ce94258121ea4 --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/RestStartEvent.java @@ -0,0 +1,18 @@ +package io.quarkus.jfr.runtime.http.rest; + +import io.quarkus.jfr.runtime.http.AbstractHttpEvent; +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Enabled; +import jdk.jfr.Label; +import jdk.jfr.Name; +import jdk.jfr.StackTrace; + +@Label("REST Start") +@Category({ "Quarkus", "HTTP" }) +@Name("quarkus.RestStart") +@Description("REST Server processing has started") +@StackTrace(false) +@Enabled(false) +public class RestStartEvent extends AbstractHttpEvent { +} 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/ServerRecorder.java new file mode 100644 index 0000000000000..3f2dc0428fdc6 --- /dev/null +++ b/extensions/jfr/runtime/src/main/java/io/quarkus/jfr/runtime/http/rest/ServerRecorder.java @@ -0,0 +1,74 @@ +package io.quarkus.jfr.runtime.http.rest; + +import io.quarkus.jfr.runtime.IdProducer; +import io.quarkus.jfr.runtime.http.AbstractHttpEvent; + +public class ServerRecorder implements Recorder { + + private final String httpMethod; + private final String uri; + private final String resourceClass; + private final String resourceMethod; + private final String client; + private final IdProducer idProducer; + private RestPeriodEvent durationEvent; + + public ServerRecorder(String httpMethod, String uri, String resourceClass, String resourceMethod, String client, + IdProducer idProducer) { + this.httpMethod = httpMethod; + this.uri = uri; + this.resourceClass = resourceClass; + this.resourceMethod = resourceMethod; + this.client = client; + this.idProducer = idProducer; + } + + @Override + public void recordStartEvent() { + + RestStartEvent startEvent = new RestStartEvent(); + + if (startEvent.shouldCommit()) { + this.setHttpInfo(startEvent); + startEvent.commit(); + } + } + + @Override + public void recordEndEvent() { + + RestEndEvent endEvent = new RestEndEvent(); + + if (endEvent.shouldCommit()) { + this.setHttpInfo(endEvent); + endEvent.commit(); + } + } + + @Override + public void startPeriodEvent() { + durationEvent = new RestPeriodEvent(); + durationEvent.begin(); + } + + @Override + public void endPeriodEvent() { + + durationEvent.end(); + + if (durationEvent.shouldCommit()) { + this.setHttpInfo(durationEvent); + durationEvent.commit(); + } + } + + private void setHttpInfo(AbstractHttpEvent event) { + event.setTraceId(idProducer.getTraceId()); + event.setSpanId(idProducer.getSpanId()); + event.setHttpMethod(httpMethod); + event.setUri(uri); + event.setResourceClass(resourceClass); + event.setResourceMethod(resourceMethod); + event.setClient(client); + } +} diff --git a/extensions/jfr/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/jfr/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..e56bb4fb031d5 --- /dev/null +++ b/extensions/jfr/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,9 @@ +name: Jfr +#description: Do something useful. +metadata: +# keywords: +# - jfr +# guide: ... # To create and publish this guide, see https://github.com/quarkiverse/quarkiverse/wiki#documenting-your-extension +# categories: +# - "miscellaneous" +# status: "preview" diff --git a/extensions/pom.xml b/extensions/pom.xml index 254514c22fcdb..60ff2a2cff316 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -53,6 +53,7 @@ opentelemetry info observability-devservices + jfr resteasy-classic diff --git a/integration-tests/jfr-blocking/pom.xml b/integration-tests/jfr-blocking/pom.xml new file mode 100644 index 0000000000000..b032c6feaf2a9 --- /dev/null +++ b/integration-tests/jfr-blocking/pom.xml @@ -0,0 +1,141 @@ + + + 4.0.0 + + io.quarkus + quarkus-integration-tests-parent + 999-SNAPSHOT + + jfr-blocking-integration-tests + Quarkus - Integration Tests - Jfr Blocking + + true + + + + io.quarkus + quarkus-rest + + + io.quarkus + quarkus-jfr + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-rest-jackson + + + + io.quarkus + quarkus-jfr-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + native-image + + + native + + + + + true + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + ${native.surefire.skip} + + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + + + + + diff --git a/integration-tests/jfr-blocking/src/main/java/io/quarkus/jfr/it/AppResource.java b/integration-tests/jfr-blocking/src/main/java/io/quarkus/jfr/it/AppResource.java new file mode 100644 index 0000000000000..1e98a26a1309e --- /dev/null +++ b/integration-tests/jfr-blocking/src/main/java/io/quarkus/jfr/it/AppResource.java @@ -0,0 +1,28 @@ +package io.quarkus.jfr.it; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.quarkus.jfr.runtime.IdProducer; + +@Path("/app") +@ApplicationScoped +public class AppResource { + + @Inject + IdProducer idProducer; + + @GET + @Path("blocking") + public IdResponse blocking() { + return new IdResponse(idProducer.getTraceId(), idProducer.getSpanId()); + } + + @GET + @Path("error") + public void error() { + throw new JfrTestException(idProducer.getTraceId()); + } +} diff --git a/integration-tests/jfr-blocking/src/main/java/io/quarkus/jfr/it/IdResponse.java b/integration-tests/jfr-blocking/src/main/java/io/quarkus/jfr/it/IdResponse.java new file mode 100644 index 0000000000000..16d30ad99aec7 --- /dev/null +++ b/integration-tests/jfr-blocking/src/main/java/io/quarkus/jfr/it/IdResponse.java @@ -0,0 +1,22 @@ +package io.quarkus.jfr.it; + +public class IdResponse { + public String traceId; + public String spanId; + + public IdResponse() { + } + + public IdResponse(String traceId, String spanId) { + this.traceId = traceId; + this.spanId = spanId; + } + + @Override + public String toString() { + return "IdResponse{" + + "traceId='" + traceId + '\'' + + ", spanId='" + spanId + '\'' + + '}'; + } +} diff --git a/integration-tests/jfr-blocking/src/main/java/io/quarkus/jfr/it/JfrResource.java b/integration-tests/jfr-blocking/src/main/java/io/quarkus/jfr/it/JfrResource.java new file mode 100644 index 0000000000000..4749eb3e42ce9 --- /dev/null +++ b/integration-tests/jfr-blocking/src/main/java/io/quarkus/jfr/it/JfrResource.java @@ -0,0 +1,157 @@ +package io.quarkus.jfr.it; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.text.ParseException; +import java.util.List; +import java.util.Optional; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.jfr.runtime.http.rest.RestEndEvent; +import io.quarkus.jfr.runtime.http.rest.RestPeriodEvent; +import io.quarkus.jfr.runtime.http.rest.RestStartEvent; +import io.quarkus.logging.Log; +import jdk.jfr.Configuration; +import jdk.jfr.FlightRecorder; +import jdk.jfr.Name; +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordingFile; + +@Path("/jfr") +@ApplicationScoped +public class JfrResource { + + final Configuration c = Configuration.create(Paths.get("src/main/resources/quarkus-jfr.jfc")); + + public JfrResource() throws IOException, ParseException { + } + + @GET + @Path("/start/{name}") + public void startJfr(@PathParam("name") String name) { + Recording recording = new Recording(c); + recording.setName(name); + recording.start(); + } + + @GET + @Path("/stop/{name}") + public void stopJfr(@PathParam("name") String name) throws IOException { + Recording recording = getRecording(name); + recording.stop(); + } + + @GET + @Path("check/{name}/{traceId}") + @Produces(MediaType.APPLICATION_JSON) + public JfrRestEventResponse check(@PathParam("name") String name, @PathParam("traceId") String traceId) throws IOException { + java.nio.file.Path dumpFile = Files.createTempFile("dump", "jfr"); + Recording recording = getRecording(name); + recording.dump(dumpFile); + recording.close(); + + List recordedEvents = RecordingFile.readAllEvents(dumpFile); + if (Log.isDebugEnabled()) { + Log.debug(recordedEvents.size() + " events were recorded"); + } + + RecordedEvent periodEvent = null; + RecordedEvent startEvent = null; + RecordedEvent endEvent = null; + + for (RecordedEvent e : recordedEvents) { + if (Log.isDebugEnabled()) { + if (e.getEventType().getCategoryNames().contains("Quarkus")) { + Log.debug(e); + } + } + if (e.hasField("traceId") && e.getString("traceId").equals(traceId)) { + 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())) { + startEvent = e; + } else if (RestEndEvent.class.getAnnotation(Name.class).value().equals(e.getEventType().getName())) { + endEvent = e; + } + } + } + + return new JfrRestEventResponse(createRestEvent(periodEvent), createRestEvent(startEvent), + createRestEvent(endEvent)); + } + + @GET + @Path("count/{name}") + @Produces(MediaType.APPLICATION_JSON) + public long count(@PathParam("name") String name) throws IOException { + java.nio.file.Path dumpFile = Files.createTempFile("dump", "jfr"); + Recording recording = getRecording(name); + recording.dump(dumpFile); + recording.close(); + + List recordedEvents = RecordingFile.readAllEvents(dumpFile); + return recordedEvents.stream().filter(r -> r.getEventType().getCategoryNames().contains("quarkus")).count(); + } + + private Recording getRecording(String name) { + List recordings = FlightRecorder.getFlightRecorder().getRecordings(); + Optional recording = recordings.stream().filter(r -> r.getName().equals(name)).findFirst(); + return recording.get(); + } + + private RestEvent createRestEvent(RecordedEvent event) { + if (event == null) { + return null; + } + RestEvent restEvent = new RestEvent(); + setHttpInfo(restEvent, event); + + return restEvent; + } + + private void setHttpInfo(RestEvent response, RecordedEvent event) { + response.traceId = event.getString("traceId"); + response.spanId = event.getString("spanId"); + response.httpMethod = event.getString("httpMethod"); + response.uri = event.getString("uri"); + response.resourceClass = event.getString("resourceClass"); + response.resourceMethod = event.getString("resourceMethod"); + response.client = event.getString("client"); + } + + class JfrRestEventResponse { + + public RestEvent period; + public RestEvent start; + public RestEvent end; + + public JfrRestEventResponse() { + } + + public JfrRestEventResponse(RestEvent period, RestEvent start, + RestEvent end) { + this.period = period; + this.start = start; + this.end = end; + } + } + + class RestEvent { + + public String traceId; + public String spanId; + public String httpMethod; + public String uri; + public String resourceClass; + public String resourceMethod; + public String client; + } +} diff --git a/integration-tests/jfr-blocking/src/main/java/io/quarkus/jfr/it/JfrTestException.java b/integration-tests/jfr-blocking/src/main/java/io/quarkus/jfr/it/JfrTestException.java new file mode 100644 index 0000000000000..97b0babe50603 --- /dev/null +++ b/integration-tests/jfr-blocking/src/main/java/io/quarkus/jfr/it/JfrTestException.java @@ -0,0 +1,7 @@ +package io.quarkus.jfr.it; + +public class JfrTestException extends RuntimeException { + public JfrTestException(String message) { + super(message); + } +} diff --git a/integration-tests/jfr-blocking/src/main/java/io/quarkus/jfr/it/RequestIdResource.java b/integration-tests/jfr-blocking/src/main/java/io/quarkus/jfr/it/RequestIdResource.java new file mode 100644 index 0000000000000..fcd2b4060c263 --- /dev/null +++ b/integration-tests/jfr-blocking/src/main/java/io/quarkus/jfr/it/RequestIdResource.java @@ -0,0 +1,25 @@ +package io.quarkus.jfr.it; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.jfr.runtime.IdProducer; + +@Path("") +@ApplicationScoped +public class RequestIdResource { + + @Inject + IdProducer idProducer; + + @Path("/requestId") + @Produces(MediaType.APPLICATION_JSON) + @GET + public IdResponse hello() { + return new IdResponse(idProducer.getTraceId(), idProducer.getSpanId()); + } +} diff --git a/integration-tests/jfr-blocking/src/main/java/io/quarkus/jfr/it/TraceIdExceptionMapper.java b/integration-tests/jfr-blocking/src/main/java/io/quarkus/jfr/it/TraceIdExceptionMapper.java new file mode 100644 index 0000000000000..a9cd3f25322f4 --- /dev/null +++ b/integration-tests/jfr-blocking/src/main/java/io/quarkus/jfr/it/TraceIdExceptionMapper.java @@ -0,0 +1,13 @@ +package io.quarkus.jfr.it; + +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +@Provider +public class TraceIdExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(JfrTestException e) { + return Response.serverError().entity(e.getMessage()).build(); + } +} diff --git a/integration-tests/jfr-blocking/src/main/resources/application.properties b/integration-tests/jfr-blocking/src/main/resources/application.properties new file mode 100644 index 0000000000000..82aef9fed0a3b --- /dev/null +++ b/integration-tests/jfr-blocking/src/main/resources/application.properties @@ -0,0 +1 @@ +quarkus.native.monitoring=jfr \ No newline at end of file diff --git a/integration-tests/jfr-blocking/src/main/resources/quarkus-jfr.jfc b/integration-tests/jfr-blocking/src/main/resources/quarkus-jfr.jfc new file mode 100644 index 0000000000000..6896ce0bc8509 --- /dev/null +++ b/integration-tests/jfr-blocking/src/main/resources/quarkus-jfr.jfc @@ -0,0 +1,12 @@ + + + + true + + + true + + + true + + diff --git a/integration-tests/jfr-blocking/src/test/java/io/quarkus/jfr/it/JfrTest.java b/integration-tests/jfr-blocking/src/test/java/io/quarkus/jfr/it/JfrTest.java new file mode 100644 index 0000000000000..531e6886a9596 --- /dev/null +++ b/integration-tests/jfr-blocking/src/test/java/io/quarkus/jfr/it/JfrTest.java @@ -0,0 +1,162 @@ +package io.quarkus.jfr.it; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.matchesRegex; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.response.ValidatableResponse; + +@QuarkusTest +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 + public void blockingTest() { + String jfrName = "blockingTest"; + + final String url = "/app/blocking"; + + given() + .when().get("/jfr/start/" + jfrName) + .then() + .statusCode(204); + + IdResponse response = given() + .when() + .get(url) + .then() + .statusCode(200) + .extract() + .as(IdResponse.class); + + given() + .when().get("/jfr/stop/" + jfrName) + .then() + .statusCode(204); + + final String resourceMethod = "blocking"; + + ValidatableResponse validatableResponse = given() + .when().get("/jfr/check/" + jfrName + "/" + 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.resourceClass", is(RESOURCE_CLASS)) + .body("start.resourceMethod", is(resourceMethod)) + .body("start.client", matchesRegex(CLIENT)) + .body("end", notNullValue()) + .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.resourceClass", is(RESOURCE_CLASS)) + .body("end.resourceMethod", is(resourceMethod)) + .body("end.client", matchesRegex(CLIENT)) + .body("period", notNullValue()) + .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.resourceClass", is(RESOURCE_CLASS)) + .body("period.resourceMethod", is(resourceMethod)) + .body("period.client", matchesRegex(CLIENT)); + } + + @Test + public void errorTest() { + String jfrName = "errorTest"; + + final String url = "/app/error"; + + given() + .when().get("/jfr/start/" + jfrName) + .then() + .statusCode(204); + + String traceId = given() + .when() + .get(url) + .then() + .statusCode(500) + .extract().asString(); + + given() + .when().get("/jfr/stop/" + jfrName) + .then() + .statusCode(204); + + final String resourceMethod = "error"; + + ValidatableResponse validatableResponse = given() + .when().get("/jfr/check/" + jfrName + "/" + 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.resourceClass", is(RESOURCE_CLASS)) + .body("start.resourceMethod", is(resourceMethod)) + .body("start.client", matchesRegex(CLIENT)) + .body("end", notNullValue()) + .body("end.uri", is(url)) + .body("end.traceId", is(traceId)) + .body("end.spanId", is(nullValue())) + .body("end.httpMethod", is(HTTP_METHOD)) + .body("end.resourceClass", is(RESOURCE_CLASS)) + .body("end.resourceMethod", is(resourceMethod)) + .body("end.client", matchesRegex(CLIENT)) + .body("period", notNullValue()) + .body("period.uri", is(url)) + .body("period.traceId", is(traceId)) + .body("period.spanId", is(nullValue())) + .body("period.httpMethod", is(HTTP_METHOD)) + .body("period.resourceClass", is(RESOURCE_CLASS)) + .body("period.resourceMethod", is(resourceMethod)) + .body("period.client", matchesRegex(CLIENT)); + } + + @Test + public void nonExistURL() { + String jfrName = "nonExistURL"; + + final String url = "/nonExistURL"; + + given() + .when().get("/jfr/start/" + jfrName) + .then() + .statusCode(204); + + given() + .when() + .get(url) + .then() + .statusCode(404); + + given() + .when().get("/jfr/stop/" + jfrName) + .then() + .statusCode(204); + + Long count = given() + .when().get("/jfr/count/" + jfrName).then() + .statusCode(200) + .extract().as(Long.class); + + Assertions.assertEquals(0, count); + } +} diff --git a/integration-tests/jfr-blocking/src/test/java/io/quarkus/jfr/it/RequestIdTest.java b/integration-tests/jfr-blocking/src/test/java/io/quarkus/jfr/it/RequestIdTest.java new file mode 100644 index 0000000000000..59672b4895f11 --- /dev/null +++ b/integration-tests/jfr-blocking/src/test/java/io/quarkus/jfr/it/RequestIdTest.java @@ -0,0 +1,23 @@ +package io.quarkus.jfr.it; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.matchesRegex; +import static org.hamcrest.Matchers.nullValue; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class RequestIdTest { + + @Test + public void testRequestWithoutRequestId() { + given() + .when().get("/requestId") + .then() + .statusCode(200) + .body("traceId", matchesRegex("[0-9a-f]{32}")) + .body("spanId", nullValue()); + } +} diff --git a/integration-tests/jfr-opentelemetry/pom.xml b/integration-tests/jfr-opentelemetry/pom.xml new file mode 100644 index 0000000000000..c319f162d875b --- /dev/null +++ b/integration-tests/jfr-opentelemetry/pom.xml @@ -0,0 +1,157 @@ + + + 4.0.0 + + io.quarkus + quarkus-integration-tests-parent + 999-SNAPSHOT + + jf-opentelemetry-integration-tests + Quarkus - Integration Tests - Jfr OpenTelemetry + + true + + + + io.quarkus + quarkus-rest + + + io.quarkus + quarkus-jfr + + + io.quarkus + quarkus-opentelemetry + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-rest-jackson + + + + io.quarkus + quarkus-jfr-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-opentelemetry-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + native-image + + + native + + + + + true + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + ${native.surefire.skip} + + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + + + + + diff --git a/integration-tests/jfr-opentelemetry/src/main/java/io/quarkus/jfr/it/IdResponse.java b/integration-tests/jfr-opentelemetry/src/main/java/io/quarkus/jfr/it/IdResponse.java new file mode 100644 index 0000000000000..16d30ad99aec7 --- /dev/null +++ b/integration-tests/jfr-opentelemetry/src/main/java/io/quarkus/jfr/it/IdResponse.java @@ -0,0 +1,22 @@ +package io.quarkus.jfr.it; + +public class IdResponse { + public String traceId; + public String spanId; + + public IdResponse() { + } + + public IdResponse(String traceId, String spanId) { + this.traceId = traceId; + this.spanId = spanId; + } + + @Override + public String toString() { + return "IdResponse{" + + "traceId='" + traceId + '\'' + + ", spanId='" + spanId + '\'' + + '}'; + } +} diff --git a/integration-tests/jfr-opentelemetry/src/main/java/io/quarkus/jfr/it/RequestIdResource.java b/integration-tests/jfr-opentelemetry/src/main/java/io/quarkus/jfr/it/RequestIdResource.java new file mode 100644 index 0000000000000..fcd2b4060c263 --- /dev/null +++ b/integration-tests/jfr-opentelemetry/src/main/java/io/quarkus/jfr/it/RequestIdResource.java @@ -0,0 +1,25 @@ +package io.quarkus.jfr.it; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.jfr.runtime.IdProducer; + +@Path("") +@ApplicationScoped +public class RequestIdResource { + + @Inject + IdProducer idProducer; + + @Path("/requestId") + @Produces(MediaType.APPLICATION_JSON) + @GET + public IdResponse hello() { + return new IdResponse(idProducer.getTraceId(), idProducer.getSpanId()); + } +} diff --git a/integration-tests/jfr-opentelemetry/src/main/resources/application.properties b/integration-tests/jfr-opentelemetry/src/main/resources/application.properties new file mode 100644 index 0000000000000..82aef9fed0a3b --- /dev/null +++ b/integration-tests/jfr-opentelemetry/src/main/resources/application.properties @@ -0,0 +1 @@ +quarkus.native.monitoring=jfr \ No newline at end of file diff --git a/integration-tests/jfr-opentelemetry/src/main/resources/quarkus-jfr.jfc b/integration-tests/jfr-opentelemetry/src/main/resources/quarkus-jfr.jfc new file mode 100644 index 0000000000000..4ef1158cb8556 --- /dev/null +++ b/integration-tests/jfr-opentelemetry/src/main/resources/quarkus-jfr.jfc @@ -0,0 +1,12 @@ + + + + true + + + true + + + true + + diff --git a/integration-tests/jfr-opentelemetry/src/test/java/io/quarkus/jfr/it/RequestIdTest.java b/integration-tests/jfr-opentelemetry/src/test/java/io/quarkus/jfr/it/RequestIdTest.java new file mode 100644 index 0000000000000..00bf880594502 --- /dev/null +++ b/integration-tests/jfr-opentelemetry/src/test/java/io/quarkus/jfr/it/RequestIdTest.java @@ -0,0 +1,22 @@ +package io.quarkus.jfr.it; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.matchesRegex; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class RequestIdTest { + + @Test + public void testRequestWithoutRequestId() { + given() + .when().get("/requestId") + .then() + .statusCode(200) + .body("traceId", matchesRegex("[0-9a-f]{32}")) + .body("spanId", matchesRegex("[0-9a-f]{16}")); + } +} diff --git a/integration-tests/jfr-reactive/pom.xml b/integration-tests/jfr-reactive/pom.xml new file mode 100644 index 0000000000000..a8b55e033febb --- /dev/null +++ b/integration-tests/jfr-reactive/pom.xml @@ -0,0 +1,141 @@ + + + 4.0.0 + + io.quarkus + quarkus-integration-tests-parent + 999-SNAPSHOT + + jfr-reactive-integration-tests + Quarkus - Integration Tests - Jfr Reactive + + true + + + + io.quarkus + quarkus-rest + + + io.quarkus + quarkus-jfr + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-rest-jackson + + + + io.quarkus + quarkus-jfr-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + native-image + + + native + + + + + true + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + ${native.surefire.skip} + + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + + + + + 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 new file mode 100644 index 0000000000000..0d69b1780272c --- /dev/null +++ b/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/AppResource.java @@ -0,0 +1,39 @@ +package io.quarkus.jfr.it; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.quarkus.jfr.runtime.IdProducer; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +@Path("/app") +@ApplicationScoped +public class AppResource { + + @Inject + IdProducer idProducer; + + @Inject + RoutingContext routingContext; + + @GET + @Path("/reactive") + public Uni reactive() { + return Uni.createFrom().item(new IdResponse(idProducer.getTraceId(), idProducer.getSpanId())); + } + + @GET + @Path("blocking") + public IdResponse blocking() { + return new IdResponse(idProducer.getTraceId(), idProducer.getSpanId()); + } + + @GET + @Path("error") + public void error() { + throw new JfrTestException(idProducer.getTraceId()); + } +} diff --git a/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/IdResponse.java b/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/IdResponse.java new file mode 100644 index 0000000000000..16d30ad99aec7 --- /dev/null +++ b/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/IdResponse.java @@ -0,0 +1,22 @@ +package io.quarkus.jfr.it; + +public class IdResponse { + public String traceId; + public String spanId; + + public IdResponse() { + } + + public IdResponse(String traceId, String spanId) { + this.traceId = traceId; + this.spanId = spanId; + } + + @Override + public String toString() { + return "IdResponse{" + + "traceId='" + traceId + '\'' + + ", spanId='" + spanId + '\'' + + '}'; + } +} 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 new file mode 100644 index 0000000000000..4749eb3e42ce9 --- /dev/null +++ b/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/JfrResource.java @@ -0,0 +1,157 @@ +package io.quarkus.jfr.it; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.text.ParseException; +import java.util.List; +import java.util.Optional; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.jfr.runtime.http.rest.RestEndEvent; +import io.quarkus.jfr.runtime.http.rest.RestPeriodEvent; +import io.quarkus.jfr.runtime.http.rest.RestStartEvent; +import io.quarkus.logging.Log; +import jdk.jfr.Configuration; +import jdk.jfr.FlightRecorder; +import jdk.jfr.Name; +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordingFile; + +@Path("/jfr") +@ApplicationScoped +public class JfrResource { + + final Configuration c = Configuration.create(Paths.get("src/main/resources/quarkus-jfr.jfc")); + + public JfrResource() throws IOException, ParseException { + } + + @GET + @Path("/start/{name}") + public void startJfr(@PathParam("name") String name) { + Recording recording = new Recording(c); + recording.setName(name); + recording.start(); + } + + @GET + @Path("/stop/{name}") + public void stopJfr(@PathParam("name") String name) throws IOException { + Recording recording = getRecording(name); + recording.stop(); + } + + @GET + @Path("check/{name}/{traceId}") + @Produces(MediaType.APPLICATION_JSON) + public JfrRestEventResponse check(@PathParam("name") String name, @PathParam("traceId") String traceId) throws IOException { + java.nio.file.Path dumpFile = Files.createTempFile("dump", "jfr"); + Recording recording = getRecording(name); + recording.dump(dumpFile); + recording.close(); + + List recordedEvents = RecordingFile.readAllEvents(dumpFile); + if (Log.isDebugEnabled()) { + Log.debug(recordedEvents.size() + " events were recorded"); + } + + RecordedEvent periodEvent = null; + RecordedEvent startEvent = null; + RecordedEvent endEvent = null; + + for (RecordedEvent e : recordedEvents) { + if (Log.isDebugEnabled()) { + if (e.getEventType().getCategoryNames().contains("Quarkus")) { + Log.debug(e); + } + } + if (e.hasField("traceId") && e.getString("traceId").equals(traceId)) { + 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())) { + startEvent = e; + } else if (RestEndEvent.class.getAnnotation(Name.class).value().equals(e.getEventType().getName())) { + endEvent = e; + } + } + } + + return new JfrRestEventResponse(createRestEvent(periodEvent), createRestEvent(startEvent), + createRestEvent(endEvent)); + } + + @GET + @Path("count/{name}") + @Produces(MediaType.APPLICATION_JSON) + public long count(@PathParam("name") String name) throws IOException { + java.nio.file.Path dumpFile = Files.createTempFile("dump", "jfr"); + Recording recording = getRecording(name); + recording.dump(dumpFile); + recording.close(); + + List recordedEvents = RecordingFile.readAllEvents(dumpFile); + return recordedEvents.stream().filter(r -> r.getEventType().getCategoryNames().contains("quarkus")).count(); + } + + private Recording getRecording(String name) { + List recordings = FlightRecorder.getFlightRecorder().getRecordings(); + Optional recording = recordings.stream().filter(r -> r.getName().equals(name)).findFirst(); + return recording.get(); + } + + private RestEvent createRestEvent(RecordedEvent event) { + if (event == null) { + return null; + } + RestEvent restEvent = new RestEvent(); + setHttpInfo(restEvent, event); + + return restEvent; + } + + private void setHttpInfo(RestEvent response, RecordedEvent event) { + response.traceId = event.getString("traceId"); + response.spanId = event.getString("spanId"); + response.httpMethod = event.getString("httpMethod"); + response.uri = event.getString("uri"); + response.resourceClass = event.getString("resourceClass"); + response.resourceMethod = event.getString("resourceMethod"); + response.client = event.getString("client"); + } + + class JfrRestEventResponse { + + public RestEvent period; + public RestEvent start; + public RestEvent end; + + public JfrRestEventResponse() { + } + + public JfrRestEventResponse(RestEvent period, RestEvent start, + RestEvent end) { + this.period = period; + this.start = start; + this.end = end; + } + } + + class RestEvent { + + public String traceId; + public String spanId; + public String httpMethod; + public String uri; + public String resourceClass; + public String resourceMethod; + public String client; + } +} diff --git a/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/JfrTestException.java b/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/JfrTestException.java new file mode 100644 index 0000000000000..97b0babe50603 --- /dev/null +++ b/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/JfrTestException.java @@ -0,0 +1,7 @@ +package io.quarkus.jfr.it; + +public class JfrTestException extends RuntimeException { + public JfrTestException(String message) { + super(message); + } +} diff --git a/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/RequestIdResource.java b/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/RequestIdResource.java new file mode 100644 index 0000000000000..fcd2b4060c263 --- /dev/null +++ b/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/RequestIdResource.java @@ -0,0 +1,25 @@ +package io.quarkus.jfr.it; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.jfr.runtime.IdProducer; + +@Path("") +@ApplicationScoped +public class RequestIdResource { + + @Inject + IdProducer idProducer; + + @Path("/requestId") + @Produces(MediaType.APPLICATION_JSON) + @GET + public IdResponse hello() { + return new IdResponse(idProducer.getTraceId(), idProducer.getSpanId()); + } +} diff --git a/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/TraceIdExceptionMapper.java b/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/TraceIdExceptionMapper.java new file mode 100644 index 0000000000000..a9cd3f25322f4 --- /dev/null +++ b/integration-tests/jfr-reactive/src/main/java/io/quarkus/jfr/it/TraceIdExceptionMapper.java @@ -0,0 +1,13 @@ +package io.quarkus.jfr.it; + +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +@Provider +public class TraceIdExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(JfrTestException e) { + return Response.serverError().entity(e.getMessage()).build(); + } +} diff --git a/integration-tests/jfr-reactive/src/main/resources/application.properties b/integration-tests/jfr-reactive/src/main/resources/application.properties new file mode 100644 index 0000000000000..82aef9fed0a3b --- /dev/null +++ b/integration-tests/jfr-reactive/src/main/resources/application.properties @@ -0,0 +1 @@ +quarkus.native.monitoring=jfr \ No newline at end of file diff --git a/integration-tests/jfr-reactive/src/main/resources/quarkus-jfr.jfc b/integration-tests/jfr-reactive/src/main/resources/quarkus-jfr.jfc new file mode 100644 index 0000000000000..6896ce0bc8509 --- /dev/null +++ b/integration-tests/jfr-reactive/src/main/resources/quarkus-jfr.jfc @@ -0,0 +1,12 @@ + + + + true + + + true + + + true + + 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 new file mode 100644 index 0000000000000..81d7bb23b6d23 --- /dev/null +++ b/integration-tests/jfr-reactive/src/test/java/io/quarkus/jfr/it/JfrTest.java @@ -0,0 +1,217 @@ +package io.quarkus.jfr.it; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.matchesRegex; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.response.ValidatableResponse; + +@QuarkusTest +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 + public void blockingTest() { + String jfrName = "blockingTest"; + + final String url = "/app/blocking"; + + given() + .when().get("/jfr/start/" + jfrName) + .then() + .statusCode(204); + + IdResponse response = given() + .when() + .get(url) + .then() + .statusCode(200) + .extract() + .as(IdResponse.class); + + given() + .when().get("/jfr/stop/" + jfrName) + .then() + .statusCode(204); + + final String resourceMethod = "blocking"; + + ValidatableResponse validatableResponse = given() + .when().get("/jfr/check/" + jfrName + "/" + 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.resourceClass", is(RESOURCE_CLASS)) + .body("start.resourceMethod", is(resourceMethod)) + .body("start.client", matchesRegex(CLIENT)) + .body("end", notNullValue()) + .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.resourceClass", is(RESOURCE_CLASS)) + .body("end.resourceMethod", is(resourceMethod)) + .body("end.client", matchesRegex(CLIENT)) + .body("period", notNullValue()) + .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.resourceClass", is(RESOURCE_CLASS)) + .body("period.resourceMethod", is(resourceMethod)) + .body("period.client", matchesRegex(CLIENT)); + } + + @Test + public void reactiveTest() { + String jfrName = "reactiveTest"; + + final String url = "/app/reactive"; + + given() + .when().get("/jfr/start/" + jfrName) + .then() + .statusCode(204); + + IdResponse response = given() + .when() + .get(url) + .then() + .statusCode(200) + .extract().as(IdResponse.class); + + given() + .when().get("/jfr/stop/" + jfrName) + .then() + .statusCode(204); + + final String resourceMethod = "reactive"; + + ValidatableResponse validatableResponse = given() + .when().get("/jfr/check/" + jfrName + "/" + 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.resourceClass", is(RESOURCE_CLASS)) + .body("start.resourceMethod", is(resourceMethod)) + .body("start.client", matchesRegex(CLIENT)) + .body("end", notNullValue()) + .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.resourceClass", is(RESOURCE_CLASS)) + .body("end.resourceMethod", is(resourceMethod)) + .body("end.client", matchesRegex(CLIENT)) + .body("period", notNullValue()) + .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.resourceClass", is(RESOURCE_CLASS)) + .body("period.resourceMethod", is(resourceMethod)) + .body("period.client", matchesRegex(CLIENT)); + } + + @Test + public void errorTest() { + String jfrName = "errorTest"; + + final String url = "/app/error"; + + given() + .when().get("/jfr/start/" + jfrName) + .then() + .statusCode(204); + + String traceId = given() + .when() + .get(url) + .then() + .statusCode(500) + .extract().asString(); + + given() + .when().get("/jfr/stop/" + jfrName) + .then() + .statusCode(204); + + final String resourceMethod = "error"; + + ValidatableResponse validatableResponse = given() + .when().get("/jfr/check/" + jfrName + "/" + 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.resourceClass", is(RESOURCE_CLASS)) + .body("start.resourceMethod", is(resourceMethod)) + .body("start.client", matchesRegex(CLIENT)) + .body("end", notNullValue()) + .body("end.uri", is(url)) + .body("end.traceId", is(traceId)) + .body("end.spanId", is(nullValue())) + .body("end.httpMethod", is(HTTP_METHOD)) + .body("end.resourceClass", is(RESOURCE_CLASS)) + .body("end.resourceMethod", is(resourceMethod)) + .body("end.client", matchesRegex(CLIENT)) + .body("period", notNullValue()) + .body("period.uri", is(url)) + .body("period.traceId", is(traceId)) + .body("period.spanId", is(nullValue())) + .body("period.httpMethod", is(HTTP_METHOD)) + .body("period.resourceClass", is(RESOURCE_CLASS)) + .body("period.resourceMethod", is(resourceMethod)) + .body("period.client", matchesRegex(CLIENT)); + } + + @Test + public void nonExistURL() { + String jfrName = "nonExistURL"; + + final String url = "/nonExistURL"; + + given() + .when().get("/jfr/start/" + jfrName) + .then() + .statusCode(204); + + given() + .when() + .get(url) + .then() + .statusCode(404); + + given() + .when().get("/jfr/stop/" + jfrName) + .then() + .statusCode(204); + + Long count = given() + .when().get("/jfr/count/" + jfrName).then() + .statusCode(200) + .extract().as(Long.class); + + Assertions.assertEquals(0, count); + } +} diff --git a/integration-tests/jfr-reactive/src/test/java/io/quarkus/jfr/it/RequestIdTest.java b/integration-tests/jfr-reactive/src/test/java/io/quarkus/jfr/it/RequestIdTest.java new file mode 100644 index 0000000000000..59672b4895f11 --- /dev/null +++ b/integration-tests/jfr-reactive/src/test/java/io/quarkus/jfr/it/RequestIdTest.java @@ -0,0 +1,23 @@ +package io.quarkus.jfr.it; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.matchesRegex; +import static org.hamcrest.Matchers.nullValue; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class RequestIdTest { + + @Test + public void testRequestWithoutRequestId() { + given() + .when().get("/requestId") + .then() + .statusCode(200) + .body("traceId", matchesRegex("[0-9a-f]{32}")) + .body("spanId", nullValue()); + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 0a63ea27ed39a..e5ce0e0a1076d 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -371,6 +371,9 @@ logging-panache-kotlin locales redis-devservices + jfr-reactive + jfr-blocking + jfr-opentelemetry grpc-descriptor-sets grpc-inprocess