Monitoring Quarkus JVM Mode With Cryostat

Cryostat is a profiling and monitoring tool that leverages the JDK Flight Recorder (JFR) framework already present in your Java applications running on the HotSpot JVM. Cryostat provides an in-cluster collection hub for easy and secure access to your JDK Flight Recorder data from outside the cluster. Cryostat is a cloud-native application primarily targeting deployment on OpenShift, so this guide will assume you are also deploying your Quarkus applications to OpenShift for simplicity.

In this article we will explore how to configure Quarkus applications to allow Cryostat to connect to them, allowing Cryostat to communicate with Quarkus and provide its JDK Flight Recorder collection, storage, and analysis tooling.

Automated Analysis Report in the Cryostat web-client UI
Figure 1. Cryostat Automated Analysis Report
Grafana dashboard displaying metrics from Cryostat
Figure 2. Cryostat Grafana Dashboard

Caveats for Cryostat with Quarkus

Quarkus famously has the ability to be built in standard JVM mode (the build produces .JAR files to be loaded and run by the JVM at runtime) or native mode (the build produces a native binary to be directly executed). Cryostat relies upon both JDK Flight Recorder (JFR), which is only partially supported in Quarkus native mode, and JDK Management Extensions (JMX), which is not supported in native mode at the time of writing. This means that, unfortunately, only JVM-mode Quarkus applications can be configured for use with Cryostat.

Getting Started with Cryostat

Getting started with Cryostat on OpenShift is quick and easy - just install it from OperatorHub:

Cryostat Operator in the OpenShift OperatorHub
Figure 3. Cryostat Installation

Then create a Cryostat CR resource instance to let the Operator know you’d like to deploy a Cryostat instance. At this stage you can also choose some configuration options, but we’ll assume the defaults for now.

A created Cryostat CR in the OpenShift Console
Figure 4. Cryostat Creation

Cryostat Communication

Cryostat uses JMX to talk to applications running on the HotSpot JVM, including but not limited to Java and Scala applications. JMX is a standard Java technology that allows tools to connect to applications and perform actions or retrieve data across a variety of underlying transport protocols. If you build and deploy your Quarkus application in JVM mode then JMX support is already built-in and ready out of the box. There are two ways to enable JMX on your application.

Method 1: Enable it at runtime

JMX can be enabled by setting JVM system properties:${RJMX_PORT}${RJMX_PORT}

Where ${RJMX_PORT} is replaced with your chosen port number for the remote JMX network connection over RMI. In this example we disable JMX authentication and JMX SSL - in practice these should both be enabled for security reasons, but configuring these particular options is out of scope for this guide. Please see this document for further information.

Quarkus uses a feature-rich application startup script, which allows us to add JVM system properties at runtime by simply setting an environment variable:

JAVA_OPTS_APPEND="${RJMX_PORT}${RJMX_PORT} -Djava.rmi.server.hostname="

If you are running your Quarkus application in OpenShift or Kubernetes then try setting this environment variable in your Deployment or DeploymentConfig, nested within the Spec for the application’s Container. This method of enabling JMX does not require the Quarkus application to be rebuilt, only to be redeployed.

Method 2: Enable it at build time

The same JVM system properties as before can be added to containerized Quarkus applications by editing the Dockerfile.jvm under src/main/docker/. Below is a sample Dockerfile.jvm from a basic Quarkus project - pay attention to the ENV JAVA_OPTS_APPEND line that has been added to append the JVM properties and expose the port number:



# We make four distinct layers so if there are application changes the library layers can be re-used
COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/
COPY --chown=185 target/quarkus-app/*.jar /deployments/
COPY --chown=185 target/quarkus-app/app/ /deployments/app/
COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/

USER 185
ENV JAVA_OPTS=" -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_OPTS_APPEND="${RJMX_PORT}${RJMX_PORT} -Djava.rmi.server.hostname="
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"

Again, be sure to replace ${RJMX_PORT} on all lines with a port number in this file.

This method is less flexible because it requires the remote JMX port number to be known at build time, and it requires the application to be rebuilt and redeployed.

Cryostat Discovery

Now that we have enabled JMX on our Quarkus application and redeployed it, it is ready to talk to Cryostat. Before this can happen, however, Cryostat needs to know how and where to find the application. There are two ways to achieve this: automatic discovery and custom targets. Automatic discovery is the preferred method and is enabled by default. Custom targets can be used to fill in gaps for deployed application targets that do not meet the criteria for automatic discovery, but which are known to be reachable over the network.

Automatic Discovery

Cryostat can discover target applications automatically and use their discovered Remote JMX service URLs to connect. Automatic discovery depends on the deployment platform - at the time of writing, Cryostat will use the OpenShift/Kubernetes API to discover Endpoints objects when deployed on OpenShift/Kubernetes. If no OpenShift/Kubernetes API server is available then Cryostat falls back to using the Java Discovery Protocol (JDP). In any case the automatically discovered targets are merged with the custom targets. If OpenShift/Kubernetes Endpoints discovery is used then the target application should have an associated Service object that exposes a port for cluster-internal JMX traffic. This port must either use the port number 9091 or have the name jfr-jmx for Cryostat to pick it up as an intended target application.

Custom Targets

This is a way for Cryostat clients (end users or other automated tooling) to tell Cryostat how and where to find a single target application instance. A Custom Target definition is at its core simply a network-reachable remote connection JMX URL and a human-readable alias. These can be created in the Cryostat web client by clicking the + (Plus) icon on the target selection dropdown or by using your preferred HTTP client:

curl \
  -X POST \
  -F alias=myapp \
  -F connectUrl=service:jmx:rmi:///jndi/rmi:// \

Once a custom target definition is added the connectUrl can be used as a targetId URL parameter anywhere one is expected in the Cryostat HTTP API.

Please see the following links for further information on Cryostat: