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.
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:
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.
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:
-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=${RJMX_PORT} -Dcom.sun.management.jmxremote.rmi.port=${RJMX_PORT} -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.local.only=false
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="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=${RJMX_PORT} -Dcom.sun.management.jmxremote.rmi.port=${RJMX_PORT} -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.local.only=false"
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:
FROM registry.access.redhat.com/ubi8/openjdk-11:1.11 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' # 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/ EXPOSE 8080 ${RJMX_PORT} USER 185 ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" ENV JAVA_OPTS_APPEND="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=${RJMX_PORT} -Dcom.sun.management.jmxremote.rmi.port=${RJMX_PORT} -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.local.only=false" 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://myapp.my-openshift-cluster.example.com:1234/jmxrmi \ https://cryostat.my-openshift-cluster.example.com/api/v2/targets
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: