Since the inclusion of JFR Events stream in JDK 14 with JEP 349, Java Developers have had programmatic access to JFR Events. This can be used for influencing system behavior by reading events and executing logic based on those events, providing alerts when issues might be occurring, or providing a hook for monitoring libraries.
You may consume events programmatically by using:
jdk.jfr.consumer.EventStreamto stream events from a recording.jdk.jfr.Recordingto configure, start, stop, dump a recording.jdk.jfr.consumer.RecordingStreamfor streaming events.jdk.management.jfr.RemoteRecordingStreamto stream events from remote source.
But the JDK Flight Recorder event streaming API enables you to continuously consume JDK Flight Recorder data:
- Actively, by creating an event stream the same time a recording is created.
import jdk.jfr.Configuration;
import jdk.jfr.consumer.RecordingStream;
public class ActiveStreamEventsSample {
public static void main(String... args) throws Exception {
Configuration c = Configuration.getConfiguration("profile");
try (RecordingStream rs = new RecordingStream(c)) {
rs.onEvent("jdk.CPULoad", System.out::println);
System.out.println("Starting recording stream ...");
rs.startAsync();
}
}
}- Passively, by creating an event stream that listens for events, but what gets recorded is controlled by external means.
import java.util.concurrent.atomic.AtomicInteger;
import jdk.jfr.consumer.EventStream;
public class PassiveStreamSample {
static int NUMBER_CPULOAD_EVENTS = 3;
public static void main(String... args) throws Exception {
AtomicInteger timer = new AtomicInteger();
try (EventStream es = EventStream.openRepository()) {
es.onEvent("jdk.CPULoad", event -> {
System.out.println("CPU Load " + event.getEndTime());
if (timer.incrementAndGet() == NUMBER_CPULOAD_EVENTS) {
System.exit(0);
}
});
es.start();
}
}
}A RecordingStream has no events enabled. To enable events you will need to explicitly enable them programmatically (e.g. recordingStream.enable([eventName]);) or add a Configuration.
- From an external process, by creating an event stream from a separate Java process.
import java.nio.file.Paths;
import java.util.Optional;
import java.util.Properties;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import jdk.jfr.consumer.EventStream;
public class StreamExternalWithAttachAPI {
public static void main(String... args) throws Exception {
Optional<VirtualMachineDescriptor> vmd = VirtualMachine.list().stream()
.filter(v -> v.displayName().contains("SleepOneSecondIntervals"))
.findFirst();
if (vmd.isEmpty()) {
throw new RuntimeException("Cannot find VM for SleepOneSecondIntervals");
}
VirtualMachine vm = VirtualMachine.attach(vmd.get());
Properties props = vm.getSystemProperties();
String repository = props.getProperty("jdk.jfr.repository");
System.out.println("jdk.jfr.repository: " + repository);
try (EventStream es = EventStream.openRepository(Paths.get(repository))) {
System.out.println("Found repository ...");
es.onEvent("jdk.CPULoad", System.out::println);
es.start();
}
}
}- Remotely, monitoring using
RemoteRecordingStream.
To enable make use of and emit metrics requires configuring some Spring Boot properties. This has already been updated in spring-petclinic/src/main/resources/application.properties, but if you are interested in applying the lessons in this module to one of your own projects, it would require the following configuration:
management.endpoint.metrics.enabled=true
management.endpoint.prometheus.enabled=true
management.prometheus.metrics.export.enabled=true
management.metrics.use-global-registry=true
management.metrics.distribution.percentiles-histogram.all=trueIn this lab, you will actively subscribe to a custom JFR event and convert it into Micrometer metrics in near real-time.
The goal is to listen for javaOne.ErrorMarker events as they are emitted and publish their duration as Micrometer timers keyed by HTTP error code.
- Create the class:
org.springframework.samples.petclinic.system.ErrorMarker - In the
ErrorMarkerclass, add the fields:String route, andString errorCode, create setters for each field add a@Nameannotation like we did in the previous module and give it the valuejavaOne.ErrorMarker, add a@Categoryannotation, and give it two categoriesApplicationandHTTP, the finished code should look like this:
package org.springframework.samples.petclinic.system;
import jdk.jfr.Category;
import jdk.jfr.Event;
import jdk.jfr.Name;
@Name("javaOne.ErrorMarker")
@Category({ "Application", "HTTP" })
public class ErrorMarker extends Event {
public String route;
public String errorCode;
public void setRoute(String route){
this.route = route;
}
public void setErrorCode(String errorCode){
this.errorCode = errorCode;
}
}- Create the class
org.springframework.samples.petclinic.system.ErrorMarkerAdvice. add the class-level annotation:@ControllerAdvice. Create the fieldboolean enabledand add@Value("${errormarker.enabled:true}")to it. - Create the method
public Object handleAnyException(Exception ex, HttpServletRequest req) throws Exception, at the start of the method create an instance ofErrorMarker, on the following line callbegin(), and thensetRoute()with it taking thereq.getMethod()andreq.getRequestUri(), next seterrorCodefrom the exception type and commit the event:
package org.springframework.samples.petclinic.system;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import jakarta.servlet.http.HttpServletRequest;
@ControllerAdvice
public class ErrorMarkerAdvice {
@ExceptionHandler(Exception.class)
public Object handleAnyException(Exception ex, HttpServletRequest req) throws Exception {
ErrorMarker errorMarker = new ErrorMarker();
errorMarker.begin();
errorMarker.setRoute(req.getMethod() + " " + req.getRequestURI());
errorMarker.setErrorCode(ex.getClass().getSimpleName());
errorMarker.commit();
throw ex;
}
}-
Create the class:
org.springframework.samples.petclinic.system.CustomMetricsListener. -
Register the custom JFR event when the application starts. Add a
@PostConstructmethod and registerErrorMarker:
@PostConstruct
public void init() {
register(ErrorMarker.class);
logger.info("#### Registered");
}- Add a
RecordingStreamfield to the class so the stream can be started when the servlet context initializes and closed when the application shuts down:
private RecordingStream recordingStream;- In
contextInitialized, obtain the global Micrometer registry, create aRecordingStream, and enable the custom event:
@Override
public void contextInitialized(ServletContextEvent sce) {
CompositeMeterRegistry metricsRegistry = Metrics.globalRegistry;
ServletContextListener.super.contextInitialized(sce);
recordingStream = new RecordingStream();
recordingStream.enable("javaOne.ErrorMarker");- Add an
onEvent(...)callback forErrorMarker. In the callback, extract the event fields and convert them into a timer metric namedhttp.error.<errorCode>:
recordingStream.onEvent("javaOne.ErrorMarker", event -> {
String route = event.getString("route").substring(1);
String errorCode = event.getString("errorCode");
String name = "http.error." + errorCode;
Timer timer = metricsRegistry.find(name).timer();
Objects
.requireNonNullElseGet(timer,
() -> Timer.builder(name)
.description("Metrics for " + route + " (" + errorCode + ")")
.register(metricsRegistry))
.record(event.getDuration());
});- At the end of
contextInitialized()start the recording stream asynchronously so the application can continue to start normally:
recordingStream.startAsync();
}- Close the stream cleanly in
contextDestroyedso the background stream is shut down when the application exits:
@Override
public void contextDestroyed(ServletContextEvent sce) {
ServletContextListener.super.contextDestroyed(sce);
recordingStream.close();
try {
recordingStream.awaitTermination();
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}- The complete implementation should look like this:
package org.springframework.samples.petclinic.system;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jdk.jfr.consumer.RecordingStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Objects;
import static jdk.jfr.FlightRecorder.register;
@Component
public class CustomMetricsListener implements ServletContextListener {
private RecordingStream recordingStream;
Logger logger = LoggerFactory.getLogger(CustomMetricsListener.class);
@PostConstruct
public void init() {
register(ErrorMarker.class);
logger.info("#### Registered");
}
@Override
public void contextInitialized(ServletContextEvent sce) {
CompositeMeterRegistry metricsRegistry = Metrics.globalRegistry;
ServletContextListener.super.contextInitialized(sce);
recordingStream = new RecordingStream();
recordingStream.enable("javaOne.ErrorMarker");
recordingStream.onEvent("javaOne.ErrorMarker", event -> {
String route = event.getString("route").substring(1);
String errorCode = event.getString("errorCode");
String name = "http.error." + errorCode;
Timer timer = metricsRegistry.find(name).timer();
Objects
.requireNonNullElseGet(timer,
() -> Timer.builder(name)
.description("Metrics for " + route + " (" + errorCode + ")")
.register(metricsRegistry))
.record(event.getDuration());
});
recordingStream.startAsync();
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
ServletContextListener.super.contextDestroyed(sce);
recordingStream.close();
try {
recordingStream.awaitTermination();
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}-
Start the application and exercise an endpoint that emits
ErrorMarkerevents. -
Open the actuator metrics endpoint at http://localhost:8080/actuator/metrics and confirm you see timer metrics such as:
http.error.NoResourceFoundExceptionhttp.error.RuntimeException
In this lab, you will passively read JFR events from the repository and expose selected values as Micrometer gauges.
The goal is to use built-in JDK events, and publish useful values such as CPU load and garbage collection metrics.
-
In the
org.springframework.samples.petclinic.system.CustomMetricsListenerfrom the previous lab, go tocontextInitialized(). -
Enable built-in JDK events
jdk.CPULoadandjdk.GarbageCollection. Forjdk.CPULoad, configure its sampling period to 1 second:
recordingStream.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1));;
recordingStream.enable("jdk.GarbageCollection");- For
jdk.CPULoad, map themachineTotalfield into a MicrometerGaugeso we can monitor CPU usage over time:
recordingStream.onEvent("jdk.CPULoad", event -> {
String name = "cpu.load.machineTotal";
Gauge.builder(name, () -> event.getDouble("machineTotal"))
.description("Measures total CPU load at a given time")
.register(metricsRegistry);
String name = "cpu.load.jvmUser";
Gauge.builder(name, () -> event.getDouble("jvmUser"))
.description("Measures JVM CPU load at a given time")
.register(metricsRegistry);
});- For
jdk.GarbageCollection, map thesumOfPausesandlongestPausefields to separateGauges, and record the name and cause of the collection as tags:
recordingStream.onEvent("jdk.GarbageCollection", event -> {
List<Tag> tags = Arrays.asList(
new ImmutableTag("name", event.getString("name")),
new ImmutableTag("cause", event.getString("cause")));
Gauge.builder("jdk.GarbageCollection-sumOfPauses", event,
e -> e.getDuration("sumOfPauses").toMillis())
.tags(tags)
.description("jdk.GarbageCollection-Total pause")
.register(metricsRegistry);
Gauge.builder("jdk.GarbageCollection-longestPause", event,
e -> e.getDuration("longestPause").toMillis())
.tags(tags)
.description("jdk.GarbageCollection-Longest pause")
.register(metricsRegistry);
});As you add each event handler, make sure you select meaningful event fields and give the Micrometer gauges clear names and descriptions. This will make the metrics easier to interpret once they are exported.
- Start the application, and check http://localhost:8080/actuator/metrics to make sure the new gauges we added in this lab show up and are populating.
JDK 25 introduces experimental support for CPU-time profiling in JDK Flight Recorder (JFR) on Linux using the Linux kernel’s CPU timer. This enhances profiling accuracy by sampling threads based on actual CPU usage rather, overcoming limitations of JFR’s current execution-time sampler, which may miss threads in native code or under-represent short runs.
The new event jdk.CPUTimeSample is not enabled by default and captures stack traces of Java threads at regular CPU-time intervals, ensuring better accuracy.
Let's investigate how you can record application behavior from an already running container.
- Add the dependency for Prometheus enablement inside
spring-petclinic/pom.xml:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<scope>runtime</scope>
</dependency>- Move to the spring-petclinic directory if you are not already there and rebuild the project:
$ mvn clean verify- Update the
custom.jfcsettings file to enablejdk.CPUTimeSample(if you haven't created thecustom.jfcin an earlier module, just omit--input ../custom.jfc):
# Enable the event in your settings file
jfr configure --input ../custom.jfc \
jdk.CPUTimeSample#enabled=true \
--output ../custom.jfc-
Open a new terminal window at the root of this exercise folder (
D_consume_events), where thedocker-compose.ymlresides. -
Launch the compose file via the command
docker-compose up --build. -
Run the LoadTestUtility with the
VETSoption:
$ java ../LoadTestUtility.java VETS-
Verify that the petclinic app is emitting metrics for Prometheus by checking the: http://localhost:8080/actuator/prometheus endpoint.
-
Go to the Prometheus dashboard: http://localhost:9090/
-
Try running some of the below queries to see what they return:
# Peaks over 1 hour
max_over_time(jdk_CPULoadmachineTotal[1h])
# JVM share of total CPU (percent of machine total used by JVM)
100 * jdk_CPULoadjvmUser / jdk_CPULoadmachineTotal
# Error rate by its type
sum by (error) (rate(http_server_requests_seconds_count[5m]))
# Mean latency by error
sum by (error) (rate(http_server_requests_seconds_sum[5m]))
/
sum by (error) (rate(http_server_requests_seconds_sum[5m]))
# p95 latency by error and outcome (with histogram enabled)
histogram_quantile(0.95,sum by (error, outcome, le) ( rate(http_server_requests_seconds_bucket[5m])))
# Error rate by route
sum by (route) (rate(http_error_NoResourceFoundException_seconds_sum[5m]))Next step is to use JFR events in unit tests!