This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 1cbab2188bd7 CAMEL-24328: Show JFR runtime data in TUI JFR tab
1cbab2188bd7 is described below
commit 1cbab2188bd72163951baad8b23493f94553ebca
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Aug 3 22:52:46 2026 +0200
CAMEL-24328: Show JFR runtime data in TUI JFR tab
Add snapshot command to CamelJfrDevConsole that captures a point-in-time
JFR recording and aggregates Camel runtime events into per-route,
per-processor, and per-endpoint duration statistics, plus recent failures
and redeliveries. Show the data in the TUI JFR tab with five navigable
views (Routes, Processors, Endpoints, Failures, Redeliveries), split
layout with processor drill-down, sorting, auto-snapshot on tab open,
and performance bars with timing-aware styling. Expose data via
getTableDataAsJson() for MCP/AI agents. Move JFR tab to Observability
group. Fix CamelContextAware injection for
FlightRecorderStartupStepRecorder.
Closes #25312
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../org/apache/camel/catalog/dev-consoles/jfr.json | 32 +-
.../org/apache/camel/catalog/docs/jfr.adoc | 57 +-
.../META-INF/org/apache/camel/dev-console/jfr.json | 32 +-
components/camel-jfr/src/main/docs/jfr.adoc | 57 +-
.../camel/runtime/jfr/CamelJfrDevConsole.java | 251 ++++-
.../jfr/FlightRecorderStartupStepRecorder.java | 4 +
.../camel/runtime/jfr/CamelJfrDevConsoleTest.java | 76 ++
.../camel/impl/engine/AbstractCamelContext.java | 1 +
.../modules/ROOT/pages/camel-jbang-tui.adoc | 32 +-
.../camel/cli/connector/LocalCliConnector.java | 8 +
.../camel/dsl/jbang/core/commands/tui/JfrTab.java | 1112 +++++++++++++++++---
.../dsl/jbang/core/commands/tui/TabRegistry.java | 6 +-
.../jbang/core/commands/tui/JfrTabRenderTest.java | 187 +++-
13 files changed, 1696 insertions(+), 159 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/jfr.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/jfr.json
index 0886083d6657..1d7b21b30794 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/jfr.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/jfr.json
@@ -25,7 +25,8 @@
"status",
"enable",
"disable",
- "jfc"
+ "jfc",
+ "snapshot"
],
"deprecated": false,
"autowired": false,
@@ -70,6 +71,35 @@
"secret": false,
"defaultValue": "all",
"description": "The runtime event to enable or disable, or all for every
event"
+ },
+ "limit": {
+ "index": 3,
+ "kind": "option",
+ "displayName": "Limit",
+ "group": "query",
+ "label": "query",
+ "required": false,
+ "type": "integer",
+ "javaType": "int",
+ "deprecated": false,
+ "autowired": false,
+ "secret": false,
+ "defaultValue": 50,
+ "description": "Maximum number of failure and redelivery entries to
return in a snapshot"
+ },
+ "routeId": {
+ "index": 4,
+ "kind": "option",
+ "displayName": "Route Id",
+ "group": "query",
+ "label": "query",
+ "required": false,
+ "type": "string",
+ "javaType": "java.lang.String",
+ "deprecated": false,
+ "autowired": false,
+ "secret": false,
+ "description": "Filter snapshot results to the given route id"
}
}
}
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jfr.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jfr.adoc
index 91f09529eecc..fff95280def1 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jfr.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jfr.adoc
@@ -119,7 +119,7 @@ JVM-wide recorder settings such as the repository path and
stack depth.
=== The `jfr` dev console
-When `camel-jfr` is on the classpath, a `jfr` dev console is available with
four commands
+When `camel-jfr` is on the classpath, a `jfr` dev console is available with
five commands
(pass as the `command` option):
[cols="1,3",options="header"]
@@ -138,6 +138,11 @@ When `camel-jfr` is on the classpath, a `jfr` dev console
is available with four
| `jfc`
| Generates a `.jfc` overlay for the six events, honoring an optional
comma-separated
`disable` option, and a ready-to-copy `jcmd JFR.start settings=...` line.
+
+| `snapshot`
+| Takes a point-in-time snapshot of the active JFR recording and aggregates
Camel
+ runtime events into per-route, per-processor, and per-endpoint duration
statistics,
+ plus recent failures and redeliveries. Accepts optional `routeId` and
`limit` options.
|===
The console is reachable wherever dev consoles are, for example over HTTP when
the
@@ -150,3 +155,53 @@ curl "http://localhost:8080/q/dev/jfr?command=status"
It is also available as a *JFR* tab in the CLI terminal UI (`camel tui`),
which renders
the status and binds `E` / `D` / `J` to enable all, disable all, and generate
a `.jfc`.
+
+=== Snapshot
+
+The `snapshot` command captures the current state of a running JFR recording
without
+stopping it, and returns aggregated statistics for Camel runtime events. This
gives you
+a lightweight alternative to exporting a `.jfr` file and opening it in Java
Mission Control.
+
+A recording must be active for the snapshot to contain data. If no recording
is running,
+the command returns an error message.
+
+==== Options
+
+[cols="1,1,3",options="header"]
+|===
+| Option | Default | Description
+| `routeId` | _(all)_ | Filter results to a single route. Applies to routes,
processors, failures, and redeliveries.
+| `limit` | `50` | Maximum number of failure and redelivery entries to return.
+|===
+
+==== Example
+
+[source,bash]
+----
+curl "http://localhost:8080/q/dev/jfr?command=snapshot"
+curl
"http://localhost:8080/q/dev/jfr?command=snapshot&routeId=order-in&limit=20"
+----
+
+==== Response
+
+The JSON response contains five sections:
+
+[cols="1,3",options="header"]
+|===
+| Section | Description
+| `routes` | Per-route totals, failure counts, and min/mean/max duration in
milliseconds. Sorted by total descending.
+| `processors` | Per-processor statistics with processor type and owning
route. Sorted by mean duration descending (slowest first).
+| `endpoints` | Per-endpoint send statistics with duration. Sorted by total
descending.
+| `failures` | Recent exchange failures with exception type and message.
Newest first, capped at `limit`.
+| `redeliveries` | Recent redelivery attempts with attempt number and max
attempts. Newest first, capped at `limit`.
+|===
+
+The top-level `eventCount` field reports the total number of Camel events
found in the
+snapshot.
+
+==== TUI integration
+
+In the CLI terminal UI (`camel tui`), the *JFR* tab shows the snapshot data in
five
+navigable table views. Press `F5` to take a snapshot, and use keys `1`–`5` to
switch
+between Routes, Processors, Endpoints, Failures, and Redeliveries. Press
`Enter` on
+a route to drill down into its processors, and `Esc` to go back.
diff --git
a/components/camel-jfr/src/generated/resources/META-INF/org/apache/camel/dev-console/jfr.json
b/components/camel-jfr/src/generated/resources/META-INF/org/apache/camel/dev-console/jfr.json
index 0886083d6657..1d7b21b30794 100644
---
a/components/camel-jfr/src/generated/resources/META-INF/org/apache/camel/dev-console/jfr.json
+++
b/components/camel-jfr/src/generated/resources/META-INF/org/apache/camel/dev-console/jfr.json
@@ -25,7 +25,8 @@
"status",
"enable",
"disable",
- "jfc"
+ "jfc",
+ "snapshot"
],
"deprecated": false,
"autowired": false,
@@ -70,6 +71,35 @@
"secret": false,
"defaultValue": "all",
"description": "The runtime event to enable or disable, or all for every
event"
+ },
+ "limit": {
+ "index": 3,
+ "kind": "option",
+ "displayName": "Limit",
+ "group": "query",
+ "label": "query",
+ "required": false,
+ "type": "integer",
+ "javaType": "int",
+ "deprecated": false,
+ "autowired": false,
+ "secret": false,
+ "defaultValue": 50,
+ "description": "Maximum number of failure and redelivery entries to
return in a snapshot"
+ },
+ "routeId": {
+ "index": 4,
+ "kind": "option",
+ "displayName": "Route Id",
+ "group": "query",
+ "label": "query",
+ "required": false,
+ "type": "string",
+ "javaType": "java.lang.String",
+ "deprecated": false,
+ "autowired": false,
+ "secret": false,
+ "description": "Filter snapshot results to the given route id"
}
}
}
diff --git a/components/camel-jfr/src/main/docs/jfr.adoc
b/components/camel-jfr/src/main/docs/jfr.adoc
index 91f09529eecc..fff95280def1 100644
--- a/components/camel-jfr/src/main/docs/jfr.adoc
+++ b/components/camel-jfr/src/main/docs/jfr.adoc
@@ -119,7 +119,7 @@ JVM-wide recorder settings such as the repository path and
stack depth.
=== The `jfr` dev console
-When `camel-jfr` is on the classpath, a `jfr` dev console is available with
four commands
+When `camel-jfr` is on the classpath, a `jfr` dev console is available with
five commands
(pass as the `command` option):
[cols="1,3",options="header"]
@@ -138,6 +138,11 @@ When `camel-jfr` is on the classpath, a `jfr` dev console
is available with four
| `jfc`
| Generates a `.jfc` overlay for the six events, honoring an optional
comma-separated
`disable` option, and a ready-to-copy `jcmd JFR.start settings=...` line.
+
+| `snapshot`
+| Takes a point-in-time snapshot of the active JFR recording and aggregates
Camel
+ runtime events into per-route, per-processor, and per-endpoint duration
statistics,
+ plus recent failures and redeliveries. Accepts optional `routeId` and
`limit` options.
|===
The console is reachable wherever dev consoles are, for example over HTTP when
the
@@ -150,3 +155,53 @@ curl "http://localhost:8080/q/dev/jfr?command=status"
It is also available as a *JFR* tab in the CLI terminal UI (`camel tui`),
which renders
the status and binds `E` / `D` / `J` to enable all, disable all, and generate
a `.jfc`.
+
+=== Snapshot
+
+The `snapshot` command captures the current state of a running JFR recording
without
+stopping it, and returns aggregated statistics for Camel runtime events. This
gives you
+a lightweight alternative to exporting a `.jfr` file and opening it in Java
Mission Control.
+
+A recording must be active for the snapshot to contain data. If no recording
is running,
+the command returns an error message.
+
+==== Options
+
+[cols="1,1,3",options="header"]
+|===
+| Option | Default | Description
+| `routeId` | _(all)_ | Filter results to a single route. Applies to routes,
processors, failures, and redeliveries.
+| `limit` | `50` | Maximum number of failure and redelivery entries to return.
+|===
+
+==== Example
+
+[source,bash]
+----
+curl "http://localhost:8080/q/dev/jfr?command=snapshot"
+curl
"http://localhost:8080/q/dev/jfr?command=snapshot&routeId=order-in&limit=20"
+----
+
+==== Response
+
+The JSON response contains five sections:
+
+[cols="1,3",options="header"]
+|===
+| Section | Description
+| `routes` | Per-route totals, failure counts, and min/mean/max duration in
milliseconds. Sorted by total descending.
+| `processors` | Per-processor statistics with processor type and owning
route. Sorted by mean duration descending (slowest first).
+| `endpoints` | Per-endpoint send statistics with duration. Sorted by total
descending.
+| `failures` | Recent exchange failures with exception type and message.
Newest first, capped at `limit`.
+| `redeliveries` | Recent redelivery attempts with attempt number and max
attempts. Newest first, capped at `limit`.
+|===
+
+The top-level `eventCount` field reports the total number of Camel events
found in the
+snapshot.
+
+==== TUI integration
+
+In the CLI terminal UI (`camel tui`), the *JFR* tab shows the snapshot data in
five
+navigable table views. Press `F5` to take a snapshot, and use keys `1`–`5` to
switch
+between Routes, Processors, Endpoints, Failures, and Redeliveries. Press
`Enter` on
+a route to drill down into its processors, and `Esc` to go back.
diff --git
a/components/camel-jfr/src/main/java/org/apache/camel/runtime/jfr/CamelJfrDevConsole.java
b/components/camel-jfr/src/main/java/org/apache/camel/runtime/jfr/CamelJfrDevConsole.java
index 0f19b4b25ec8..3aeff394ee6c 100644
---
a/components/camel-jfr/src/main/java/org/apache/camel/runtime/jfr/CamelJfrDevConsole.java
+++
b/components/camel-jfr/src/main/java/org/apache/camel/runtime/jfr/CamelJfrDevConsole.java
@@ -16,8 +16,14 @@
*/
package org.apache.camel.runtime.jfr;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
import java.util.ArrayList;
+import java.util.Comparator;
import java.util.EnumSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -25,11 +31,14 @@ import java.util.Set;
import jdk.jfr.FlightRecorder;
import jdk.jfr.Recording;
import jdk.jfr.RecordingState;
+import jdk.jfr.consumer.RecordedEvent;
+import jdk.jfr.consumer.RecordingFile;
import org.apache.camel.CamelContext;
import org.apache.camel.spi.LifecycleStrategy;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.annotations.DevConsole;
import org.apache.camel.support.console.AbstractDevConsole;
+import org.apache.camel.util.StringHelper;
import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
@@ -44,7 +53,7 @@ import org.apache.camel.util.json.JsonObject;
public class CamelJfrDevConsole extends AbstractDevConsole {
@Metadata(label = "query", description = "Command to perform", javaType =
"java.lang.String",
- defaultValue = "status", enums = "status,enable,disable,jfc")
+ defaultValue = "status", enums =
"status,enable,disable,jfc,snapshot")
public static final String COMMAND = "command";
@Metadata(label = "query", description = "The runtime event to enable or
disable, or all for every event",
@@ -57,7 +66,17 @@ public class CamelJfrDevConsole extends AbstractDevConsole {
javaType = "java.lang.String")
public static final String DISABLE = "disable";
+ @Metadata(label = "query", description = "Filter snapshot results to the
given route id",
+ javaType = "java.lang.String")
+ public static final String ROUTE_ID = "routeId";
+
+ @Metadata(label = "query",
+ description = "Maximum number of failure and redelivery entries
to return in a snapshot",
+ javaType = "int", defaultValue = "50")
+ public static final String LIMIT = "limit";
+
private static final String ALL = "all";
+ private static final int DEFAULT_LIMIT = 50;
public CamelJfrDevConsole() {
super("camel", "jfr", "JFR Runtime Instrumentation",
@@ -94,7 +113,7 @@ public class CamelJfrDevConsole extends AbstractDevConsole {
List<Recording> recordings =
FlightRecorder.getFlightRecorder().getRecordings();
StringBuilder sb = new StringBuilder();
- sb.append("registered:
").append(isInstrumentationRegistered()).append('\n');
+ sb.append("runtimeEvents:
").append(isInstrumentationRegistered()).append('\n');
if (recordings.isEmpty()) {
sb.append("recordings: none active\n");
} else {
@@ -199,6 +218,224 @@ public class CamelJfrDevConsole extends
AbstractDevConsole {
+ "\n(replace 'default' with whatever base profile your
recording already uses)";
}
+ // ---- snapshot ----
+
+ private static class DurationStats {
+ long count;
+ long failed;
+ long minNanos = Long.MAX_VALUE;
+ long maxNanos;
+ long totalNanos;
+
+ void record(long nanos, boolean isFailed) {
+ count++;
+ if (isFailed) {
+ failed++;
+ }
+ totalNanos += nanos;
+ if (nanos < minNanos) {
+ minNanos = nanos;
+ }
+ if (nanos > maxNanos) {
+ maxNanos = nanos;
+ }
+ }
+
+ double minMs() {
+ return count == 0 ? 0 : minNanos / 1_000_000.0;
+ }
+
+ double meanMs() {
+ return count == 0 ? 0 : (totalNanos / (double) count) /
1_000_000.0;
+ }
+
+ double maxMs() {
+ return maxNanos / 1_000_000.0;
+ }
+
+ JsonObject toJson() {
+ JsonObject jo = new JsonObject();
+ jo.put("total", count);
+ jo.put("failed", failed);
+ jo.put("minMs", round3(minMs()));
+ jo.put("meanMs", round3(meanMs()));
+ jo.put("maxMs", round3(maxMs()));
+ return jo;
+ }
+ }
+
+ private static double round3(double value) {
+ return Math.round(value * 1000.0) / 1000.0;
+ }
+
+ private JsonObject doSnapshot(Map<String, Object> options) {
+ String routeIdFilter = optionString(options, ROUTE_ID);
+ int limit = DEFAULT_LIMIT;
+ String limitStr = optionString(options, LIMIT);
+ if (limitStr != null) {
+ try {
+ limit = Integer.parseInt(limitStr);
+ } catch (NumberFormatException e) {
+ // keep default
+ }
+ }
+
+ JsonObject result = new JsonObject();
+ result.put("snapshot", true);
+ result.put("snapshotTimestamp", System.currentTimeMillis());
+
+ Path tempFile = null;
+ try (Recording snapshot =
FlightRecorder.getFlightRecorder().takeSnapshot()) {
+ if (snapshot.getSize() == 0) {
+ result.put("error", "no JFR data available: ensure a recording
is active");
+ return result;
+ }
+ tempFile = Files.createTempFile("camel-jfr-snapshot-", ".jfr");
+ snapshot.dump(tempFile);
+
+ Map<String, DurationStats> routeStats = new LinkedHashMap<>();
+ Map<String, DurationStats> processorStats = new LinkedHashMap<>();
+ Map<String, String> processorTypes = new LinkedHashMap<>();
+ Map<String, String> processorRoutes = new LinkedHashMap<>();
+ Map<String, DurationStats> endpointStats = new LinkedHashMap<>();
+ List<JsonObject> failures = new ArrayList<>();
+ List<JsonObject> redeliveries = new ArrayList<>();
+ int eventCount = 0;
+
+ String routeEventName = CamelJfrEvents.ROUTE.getEventName();
+ String processorEventName =
CamelJfrEvents.PROCESSOR.getEventName();
+ String sendEventName = CamelJfrEvents.SEND.getEventName();
+ String failedEventName = CamelJfrEvents.FAILED.getEventName();
+ String redeliveryEventName =
CamelJfrEvents.REDELIVERY.getEventName();
+
+ try (RecordingFile rf = new RecordingFile(tempFile)) {
+ while (rf.hasMoreEvents()) {
+ RecordedEvent event = rf.readEvent();
+ String eventName = event.getEventType().getName();
+
+ if (routeEventName.equals(eventName)) {
+ String routeId = event.getString("routeId");
+ if (routeIdFilter == null ||
routeIdFilter.equals(routeId)) {
+ long nanos = durationNanos(event);
+ boolean failed = event.getBoolean("failed");
+ routeStats.computeIfAbsent(routeId, k -> new
DurationStats()).record(nanos, failed);
+ eventCount++;
+ }
+ } else if (processorEventName.equals(eventName)) {
+ String routeId = event.getString("routeId");
+ if (routeIdFilter == null ||
routeIdFilter.equals(routeId)) {
+ String processorId =
event.getString("processorId");
+ long nanos = durationNanos(event);
+ boolean failed = event.getBoolean("failed");
+ processorStats.computeIfAbsent(processorId, k ->
new DurationStats()).record(nanos, failed);
+ processorTypes.putIfAbsent(processorId,
event.getString("processorType"));
+ processorRoutes.putIfAbsent(processorId, routeId);
+ eventCount++;
+ }
+ } else if (sendEventName.equals(eventName)) {
+ String endpointUri = event.getString("endpointUri");
+ long nanos = durationNanos(event);
+ boolean failed = event.getBoolean("failed");
+ endpointStats.computeIfAbsent(endpointUri, k -> new
DurationStats()).record(nanos, failed);
+ eventCount++;
+ } else if (failedEventName.equals(eventName)) {
+ String routeId = event.getString("routeId");
+ if (routeIdFilter == null ||
routeIdFilter.equals(routeId)) {
+ JsonObject fo = new JsonObject();
+ fo.put("timestamp",
event.getStartTime().toString());
+ fo.put("exchangeId",
event.getString("exchangeId"));
+ fo.put("routeId", routeId);
+ fo.put("exceptionType",
event.getString("exceptionType"));
+ fo.put("exceptionMessage",
event.getString("exceptionMessage"));
+ failures.add(fo);
+ eventCount++;
+ }
+ } else if (redeliveryEventName.equals(eventName)) {
+ String routeId = event.getString("routeId");
+ if (routeIdFilter == null ||
routeIdFilter.equals(routeId)) {
+ JsonObject ro = new JsonObject();
+ ro.put("timestamp",
event.getStartTime().toString());
+ ro.put("exchangeId",
event.getString("exchangeId"));
+ ro.put("routeId", routeId);
+ ro.put("attempt", event.getInt("attempt"));
+ ro.put("maxAttempts", event.getInt("maxAttempts"));
+ redeliveries.add(ro);
+ eventCount++;
+ }
+ }
+ }
+ }
+
+ result.put("eventCount", eventCount);
+
+ // routes — sorted by total descending
+ JsonArray routesJson = new JsonArray();
+ routeStats.entrySet().stream()
+ .sorted(Comparator.<Map.Entry<String, DurationStats>>
comparingLong(e -> e.getValue().count).reversed())
+ .forEach(e -> {
+ JsonObject jo = e.getValue().toJson();
+ jo.put("routeId", e.getKey());
+ routesJson.add(jo);
+ });
+ result.put("routes", routesJson);
+
+ // processors — sorted by mean duration descending (slowest first)
+ JsonArray processorsJson = new JsonArray();
+ processorStats.entrySet().stream()
+ .sorted(Comparator.<Map.Entry<String, DurationStats>>
comparingDouble(
+ e -> e.getValue().meanMs()).reversed())
+ .forEach(e -> {
+ JsonObject jo = e.getValue().toJson();
+ jo.put("processorId", e.getKey());
+ jo.put("processorType",
processorTypes.get(e.getKey()));
+ jo.put("routeId", processorRoutes.get(e.getKey()));
+ processorsJson.add(jo);
+ });
+ result.put("processors", processorsJson);
+
+ // endpoints — sorted by total descending
+ JsonArray endpointsJson = new JsonArray();
+ endpointStats.entrySet().stream()
+ .sorted(Comparator.<Map.Entry<String, DurationStats>>
comparingLong(
+ e -> e.getValue().count).reversed())
+ .forEach(e -> {
+ JsonObject jo = e.getValue().toJson();
+ jo.put("endpointUri", e.getKey());
+ endpointsJson.add(jo);
+ });
+ result.put("endpoints", endpointsJson);
+
+ // failures — newest first, capped at limit
+ failures.sort(Comparator.comparing((JsonObject o) ->
o.getString("timestamp")).reversed());
+ JsonArray failuresJson = new JsonArray();
+ failures.stream().limit(limit).forEach(failuresJson::add);
+ result.put("failures", failuresJson);
+
+ // redeliveries — newest first, capped at limit
+ redeliveries.sort(Comparator.comparing((JsonObject o) ->
o.getString("timestamp")).reversed());
+ JsonArray redeliveriesJson = new JsonArray();
+ redeliveries.stream().limit(limit).forEach(redeliveriesJson::add);
+ result.put("redeliveries", redeliveriesJson);
+
+ } catch (IOException e) {
+ result.put("error", "failed to read JFR snapshot: " +
e.getMessage());
+ } finally {
+ if (tempFile != null) {
+ try {
+ Files.deleteIfExists(tempFile);
+ } catch (IOException ignored) {
+ }
+ }
+ }
+
+ return result;
+ }
+
+ private static long durationNanos(RecordedEvent event) {
+ Duration d = event.getDuration();
+ return d != null ? d.toNanos() : 0;
+ }
+
@Override
protected String doCallText(Map<String, Object> options) {
String command = optionString(options, COMMAND);
@@ -213,6 +450,7 @@ public class CamelJfrDevConsole extends AbstractDevConsole {
yield e.getMessage();
}
}
+ case "snapshot" -> doSnapshot(options).toJson();
default -> unknownCommand(command);
};
}
@@ -223,12 +461,12 @@ public class CamelJfrDevConsole extends
AbstractDevConsole {
JsonObject root = new JsonObject();
switch (command != null ? command : "status") {
case "status" -> {
- root.put("registered", isInstrumentationRegistered());
+ root.put("runtimeEvents", isInstrumentationRegistered());
JsonArray recordingsJson = new JsonArray();
for (Recording recording :
FlightRecorder.getFlightRecorder().getRecordings()) {
JsonObject rec = new JsonObject();
rec.put("name", recording.getName());
- rec.put("state", recording.getState().toString());
+ rec.put("state",
StringHelper.capitalize(recording.getState().toString().toLowerCase(java.util.Locale.US)));
rec.put("destination",
recording.getDestination() != null ?
recording.getDestination().toString() : null);
recordingsJson.add(rec);
@@ -252,12 +490,15 @@ public class CamelJfrDevConsole extends
AbstractDevConsole {
root.put("error", e.getMessage());
}
}
+ case "snapshot" -> {
+ return doSnapshot(options);
+ }
default -> root.put("error", unknownCommand(command));
}
return root;
}
private static String unknownCommand(String command) {
- return "unknown command: " + command + ". Valid values: status,
enable, disable, jfc";
+ return "unknown command: " + command + ". Valid values: status,
enable, disable, jfc, snapshot";
}
}
diff --git
a/components/camel-jfr/src/main/java/org/apache/camel/startup/jfr/FlightRecorderStartupStepRecorder.java
b/components/camel-jfr/src/main/java/org/apache/camel/startup/jfr/FlightRecorderStartupStepRecorder.java
index 33c2ba76a731..494a5a2a5630 100644
---
a/components/camel-jfr/src/main/java/org/apache/camel/startup/jfr/FlightRecorderStartupStepRecorder.java
+++
b/components/camel-jfr/src/main/java/org/apache/camel/startup/jfr/FlightRecorderStartupStepRecorder.java
@@ -73,6 +73,10 @@ public class FlightRecorderStartupStepRecorder extends
DefaultStartupStepRecorde
@Override
public void setRuntimeEnabled(boolean runtimeEnabled) {
this.runtimeEnabled = runtimeEnabled;
+ if (runtimeEnabled && camelContext != null && instrumentation == null)
{
+ instrumentation = new CamelJfrRuntimeInstrumentation();
+ camelContext.addLifecycleStrategy(instrumentation);
+ }
}
@Override
diff --git
a/components/camel-jfr/src/test/java/org/apache/camel/runtime/jfr/CamelJfrDevConsoleTest.java
b/components/camel-jfr/src/test/java/org/apache/camel/runtime/jfr/CamelJfrDevConsoleTest.java
index 0965a4c03890..eb9e105904aa 100644
---
a/components/camel-jfr/src/test/java/org/apache/camel/runtime/jfr/CamelJfrDevConsoleTest.java
+++
b/components/camel-jfr/src/test/java/org/apache/camel/runtime/jfr/CamelJfrDevConsoleTest.java
@@ -26,6 +26,7 @@ import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.startup.jfr.FlightRecorderStartupStepRecorder;
import org.apache.camel.support.PluginHelper;
import org.apache.camel.test.junit6.CamelTestSupport;
+import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
import org.junit.jupiter.api.Test;
@@ -256,4 +257,79 @@ class CamelJfrDevConsoleTest extends CamelTestSupport {
assertThat(json.getString("error")).contains("unknown event: bogus");
assertThat(json.get("jfc")).isNull();
}
+
+ @Test
+ void snapshotReturnsAggregatedDataFromActiveRecording() throws Exception {
+ try (DefaultCamelContext ctx = new DefaultCamelContext(false)) {
+ FlightRecorderStartupStepRecorder recorder = new
FlightRecorderStartupStepRecorder();
+ recorder.setRuntimeEnabled(true);
+ recorder.setCamelContext(ctx);
+ ctx.getCamelContextExtension().setStartupStepRecorder(recorder);
+ ctx.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("direct:snap").routeId("snap-route").to("mock:snap-result");
+ }
+ });
+ ctx.build();
+ ctx.start();
+
+ try (Recording recording = startRecordingWithAllEvents()) {
+ ctx.createProducerTemplate().sendBody("direct:snap", "hello");
+ ctx.createProducerTemplate().sendBody("direct:snap", "world");
+
+ CamelJfrDevConsole console = resolveConsole(ctx);
+ JsonObject json = (JsonObject)
console.call(DevConsole.MediaType.JSON, Map.of("command", "snapshot"));
+
+ assertThat(json.getBoolean("snapshot")).isTrue();
+ assertThat(json.getInteger("eventCount")).isGreaterThan(0);
+
+ JsonArray routes = json.getJsonArray("routes");
+ assertThat(routes).isNotNull().isNotEmpty();
+
+ JsonArray processors = json.getJsonArray("processors");
+ assertThat(processors).isNotNull();
+
+ JsonArray endpoints = json.getJsonArray("endpoints");
+ assertThat(endpoints).isNotNull();
+ }
+ }
+ }
+
+ @Test
+ void snapshotTextFormatContainsRouteData() throws Exception {
+ try (DefaultCamelContext ctx = new DefaultCamelContext(false)) {
+ FlightRecorderStartupStepRecorder recorder = new
FlightRecorderStartupStepRecorder();
+ recorder.setRuntimeEnabled(true);
+ recorder.setCamelContext(ctx);
+ ctx.getCamelContextExtension().setStartupStepRecorder(recorder);
+ ctx.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("direct:txt").routeId("txt-route").to("mock:txt-result");
+ }
+ });
+ ctx.build();
+ ctx.start();
+
+ try (Recording recording = startRecordingWithAllEvents()) {
+ ctx.createProducerTemplate().sendBody("direct:txt", "test");
+
+ CamelJfrDevConsole console = resolveConsole(ctx);
+ String text = (String) console.call(DevConsole.MediaType.TEXT,
Map.of("command", "snapshot"));
+
+ assertThat(text).contains("snapshot").contains("txt-route");
+ }
+ }
+ }
+
+ @Test
+ void snapshotWithNoRecordingReturnsErrorMessage() {
+ CamelJfrDevConsole console = resolveConsole(context);
+
+ JsonObject json = (JsonObject) console.call(DevConsole.MediaType.JSON,
Map.of("command", "snapshot"));
+
+ assertThat(json.getBoolean("snapshot")).isTrue();
+ assertThat(json.getString("error")).contains("no JFR data available");
+ }
}
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
index 0875b7af7d7c..0bc06ccd0635 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
@@ -2447,6 +2447,7 @@ public abstract class AbstractCamelContext extends
BaseService
}
}
+ CamelContextAware.trySetCamelContext(startupStepRecorder, this);
startupStepRecorder.start();
StartupStep step = startupStepRecorder.beginStep(CamelContext.class,
null, "Build CamelContext");
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index 6cd09de2a64c..5ce703da222e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -124,9 +124,9 @@ to any tab, or use *Tab* / *Shift+Tab* to cycle.
The *More* menu (key *0*) opens a popup with tabs organized into groups:
* *Routing* -- Browse Endpoints, Consumers, HTTP, Inflight, Producers, Route
Controller
-* *Observability* -- Circuit Breaker, Health, Metrics, Network Services,
Exchange Events, Recovery Tasks, OpenTelemetry Spans
+* *Observability* -- Circuit Breaker, Health, JFR, Metrics, Network Services,
Exchange Events, Recovery Tasks, OpenTelemetry Spans
* *Data* -- JDBC DataSource, Kafka, SQL Query, SQL Trace
-* *JVM* -- Classpath, Heap Memory Histogram, JFR, Memory Usage, Memory Leak,
Process, Startup, Threads
+* *JVM* -- Classpath, Heap Memory Histogram, Memory Usage, Memory Leak,
Process, Startup, Threads
* *Project* -- Beans, Catalog, Configuration, CVE Audit, Maven Dependencies,
Type Converters, Data Type Transformers
Tabs appear dynamically based on what the integration uses. For example, the
Kafka tab appears
@@ -608,6 +608,34 @@ in your running integration. It runs two sequential
recordings and compares obje
classifying each class as growing, stable, shrinking, new, or gone. This is
lightweight and safe
for production use.
+=== JFR Runtime Profiling
+
+The JFR tab (under More > Observability) gives you a lightweight profiling
view of your running integration
+using Java Flight Recorder. JFR is built into the JVM with very low overhead,
making it suitable
+for near-production use cases where you need timing data without impacting
throughput.
+
+Start your integration with JFR runtime instrumentation enabled:
+
+[source,bash]
+----
+camel run my-route.yaml --jfr
+----
+
+The JFR tab shows the recording status and event configuration. Press *F5* to
take a snapshot
+of the active recording -- the TUI aggregates all Camel events and presents
them in five views:
+
+* *Routes* (key *1*) -- per-route totals, failure counts, and min/mean/max
duration
+* *Processors* (key *2*) -- per-processor statistics with type and owning
route, sorted by slowest first
+* *Endpoints* (key *3*) -- per-endpoint send statistics with duration
+* *Failures* (key *4*) -- recent exchange failures with exception type and
message
+* *Redeliveries* (key *5*) -- recent redelivery attempts with attempt and max
counts
+
+Press *Enter* on a route to drill down into its processors filtered by that
route.
+Press *s* to cycle sort columns, and *Esc* to go back.
+
+Each snapshot is a point-in-time read -- the recording keeps running, so you
can press *F5*
+again at any time to see updated statistics as more messages flow through the
system.
+
=== Catalog
The Catalog tab (under More > Project) lets you browse the full Camel
component catalog from within
diff --git
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index b1e4a9507fea..c5859350123f 100644
---
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -1012,6 +1012,14 @@ public class LocalCliConnector extends ServiceSupport
implements CliConnector, C
if (disable != null) {
params.put("disable", disable);
}
+ String routeId = root.getString("routeId");
+ if (routeId != null) {
+ params.put("routeId", routeId);
+ }
+ String limit = root.getString("limit");
+ if (limit != null) {
+ params.put("limit", limit);
+ }
JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON,
params);
LOG.trace("Updating output file: {}", outputFile);
IOHelper.writeText(json.toJson(), outputFile);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java
index ba0b46104c5f..6fa628aeed13 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java
@@ -17,12 +17,14 @@
package org.apache.camel.dsl.jbang.core.commands.tui;
import java.util.ArrayList;
-import java.util.LinkedHashMap;
+import java.util.Comparator;
import java.util.List;
-import java.util.Map;
+import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
import dev.tamboui.layout.Rect;
import dev.tamboui.style.Style;
import dev.tamboui.terminal.Frame;
@@ -31,10 +33,16 @@ import dev.tamboui.text.Span;
import dev.tamboui.text.Text;
import dev.tamboui.tui.event.KeyCode;
import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.MouseEvent;
import dev.tamboui.widgets.block.Block;
import dev.tamboui.widgets.block.BorderType;
import dev.tamboui.widgets.block.Borders;
import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.scrollbar.ScrollbarState;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import dev.tamboui.widgets.table.TableState;
import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
@@ -43,17 +51,80 @@ import static
org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
class JfrTab extends AbstractTab {
private static final Style LABEL = Theme.muted();
- private static final Style OK = Style.EMPTY.fg(Theme.baseFg()).bold();
+ private static final Style VALUE = Style.EMPTY.fg(Theme.baseFg()).bold();
+
+ enum View {
+ ROUTES("Routes"),
+ PROCESSORS("Processors"),
+ ENDPOINTS("Endpoints"),
+ FAILURES("Failures"),
+ REDELIVERIES("Redeliveries");
+
+ final String label;
+
+ View(String label) {
+ this.label = label;
+ }
+ }
private final AtomicBoolean loading = new AtomicBoolean(false);
private final Consumer<Runnable> renderThreadExecutor;
+ // status state
private boolean registered;
private List<String> recordings = List.of();
- private Map<String, Boolean> events = new LinkedHashMap<>();
- private String message;
private String errorMessage;
- private boolean dataLoaded;
+ private boolean statusLoaded;
+
+ // snapshot data
+ private List<RouteStats> routeData = List.of();
+ private List<ProcessorStats> processorData = List.of();
+ private List<EndpointStats> endpointData = List.of();
+ private List<FailureEntry> failureData = List.of();
+ private List<RedeliveryEntry> redeliveryData = List.of();
+ private int snapshotEventCount;
+ private long snapshotTime;
+ private boolean snapshotLoaded;
+
+ // view state
+ private View activeView = View.ROUTES;
+ private final TableState tableState = new TableState();
+ private final ScrollbarState scrollState = new ScrollbarState();
+ private final TableState processorTableState = new TableState();
+ private final ScrollbarState processorScrollState = new ScrollbarState();
+ private Rect lastTableArea;
+ private int topPanelHeight = -1;
+
+ // sort state per view
+ private static final String[] ROUTE_SORT_COLUMNS = { "route", "total",
"failed", "min", "mean", "max" };
+ private static final String[] PROCESSOR_SORT_COLUMNS = { "processor",
"total", "failed", "min", "mean", "max" };
+ private static final String[] ENDPOINT_SORT_COLUMNS = { "endpoint",
"total", "failed", "min", "mean", "max" };
+ private String routeSort = "route";
+ private int routeSortIndex;
+ private boolean routeSortReversed;
+ private String processorSort = "processor";
+ private int processorSortIndex;
+ private boolean processorSortReversed;
+ private String endpointSort = "endpoint";
+ private int endpointSortIndex;
+ private boolean endpointSortReversed;
+
+ record RouteStats(String routeId, long total, long failed, double minMs,
double meanMs, double maxMs) {
+ }
+
+ record ProcessorStats(String processorId, String processorType, String
routeId,
+ long total, long failed, double minMs, double meanMs, double
maxMs) {
+ }
+
+ record EndpointStats(String endpointUri, long total, long failed, double
minMs, double meanMs, double maxMs) {
+ }
+
+ record FailureEntry(String timestamp, String exchangeId, String routeId,
+ String exceptionType, String exceptionMessage) {
+ }
+
+ record RedeliveryEntry(String timestamp, String exchangeId, String
routeId, int attempt, int maxAttempts) {
+ }
JfrTab(MonitorContext ctx) {
this(ctx, action -> {
@@ -70,7 +141,7 @@ class JfrTab extends AbstractTab {
@Override
public void onTabSelected() {
- if (!dataLoaded) {
+ if (!statusLoaded) {
refreshStatus();
}
}
@@ -79,33 +150,110 @@ class JfrTab extends AbstractTab {
public void onIntegrationChanged() {
registered = false;
recordings = List.of();
- events = new LinkedHashMap<>();
- message = null;
errorMessage = null;
- dataLoaded = false;
+ statusLoaded = false;
+ clearSnapshotData();
+ }
+
+ private void clearSnapshotData() {
+ routeData = List.of();
+ processorData = List.of();
+ endpointData = List.of();
+ failureData = List.of();
+ redeliveryData = List.of();
+ snapshotEventCount = 0;
+ snapshotLoaded = false;
+ topPanelHeight = -1;
+ tableState.select(0);
+ processorTableState.select(0);
}
@Override
public boolean handleKeyEvent(KeyEvent ke) {
if (ke.isKey(KeyCode.F5)) {
- refreshStatus();
+ takeSnapshot();
return true;
}
- if (ke.isCharIgnoreCase('e')) {
- toggleAll(true);
+ if (ke.isChar(' ')) {
+ View[] views = View.values();
+ switchView(views[(activeView.ordinal() + 1) % views.length]);
return true;
}
- if (ke.isCharIgnoreCase('d')) {
- toggleAll(false);
+
+ if (ke.isChar('s')) {
+ cycleSortForward();
return true;
}
- if (ke.isCharIgnoreCase('j')) {
- generateJfc();
+ if (ke.isChar('S')) {
+ cycleSortReverse();
+ return true;
+ }
+
+ if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+ TableState ts = activeTableState();
+ for (int i = 0; i < 20 && ts.selected() != null && ts.selected() >
0; i++) {
+ ts.selectPrevious();
+ }
return true;
}
+ if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+ TableState ts = activeTableState();
+ int count = getRowCount();
+ for (int i = 0; i < 20; i++) {
+ ts.selectNext(count);
+ }
+ return true;
+ }
+ if (ke.isHome()) {
+ activeTableState().selectFirst();
+ return true;
+ }
+ if (ke.isEnd()) {
+ activeTableState().selectLast(getRowCount());
+ return true;
+ }
+
return false;
}
+ @Override
+ public void navigateUp() {
+ activeTableState().selectPrevious();
+ }
+
+ @Override
+ public void navigateDown() {
+ activeTableState().selectNext(getRowCount());
+ }
+
+ @Override
+ public boolean handleMouseEvent(MouseEvent me, Rect area) {
+ return handleTableClick(me, lastTableArea, activeTableState(),
getRowCount());
+ }
+
+ private TableState activeTableState() {
+ return tableState;
+ }
+
+ private void switchView(View view) {
+ if (view != activeView) {
+ activeView = view;
+ tableState.select(0);
+ }
+ }
+
+ private int getRowCount() {
+ return switch (activeView) {
+ case ROUTES -> routeData.size();
+ case PROCESSORS -> processorData.size();
+ case ENDPOINTS -> endpointData.size();
+ case FAILURES -> failureData.size();
+ case REDELIVERIES -> redeliveryData.size();
+ };
+ }
+
+ // ---- rendering ----
+
@Override
public void render(Frame frame, Rect area) {
IntegrationInfo info = ctx.findSelectedIntegration();
@@ -114,101 +262,549 @@ class JfrTab extends AbstractTab {
return;
}
- if (loading.get() && !dataLoaded) {
+ List<Rect> rows = Layout.vertical()
+ .constraints(Constraint.length(4), Constraint.fill())
+ .split(area);
+
+ renderStatusHeader(frame, rows.get(0));
+ renderDataTable(frame, rows.get(1));
+ }
+
+ private void renderStatusHeader(Frame frame, Rect area) {
+ List<Line> lines = new ArrayList<>();
+
+ List<Span> line1 = new ArrayList<>();
+ if (!statusLoaded) {
+ line1.add(Span.styled("Loading...", LABEL));
+ } else {
+ line1.add(Span.styled("Runtime Events: ", LABEL));
+ line1.add(Span.styled(registered ? "Enabled" : "Disabled",
registered ? VALUE : Theme.error()));
+
+ if (!recordings.isEmpty()) {
+ line1.add(Span.styled(" Recording: ", LABEL));
+ line1.add(Span.styled(recordings.get(0), Theme.success()));
+ } else {
+ line1.add(Span.styled(" No Active Recording", LABEL));
+ }
+ }
+
+ if (snapshotLoaded) {
+ line1.add(Span.styled(" Snapshot: ", LABEL));
+ String ageLabel = "";
+ if (snapshotTime > 0) {
+ long agoSec = (System.currentTimeMillis() - snapshotTime) /
1000;
+ if (agoSec >= 60) {
+ ageLabel = " (" + (agoSec / 60) + "m ago)";
+ } else if (agoSec >= 5) {
+ ageLabel = " (" + agoSec + "s ago)";
+ }
+ }
+ line1.add(Span.styled(snapshotEventCount + " events" + ageLabel,
VALUE));
+ }
+ lines.add(Line.from(line1));
+
+ List<Span> line2 = new ArrayList<>();
+ for (View v : View.values()) {
+ if (v.ordinal() > 0) {
+ line2.add(Span.styled(" | ", LABEL));
+ }
+ boolean active = v == activeView;
+ line2.add(Span.styled(v.label, active ? VALUE : LABEL));
+ }
+ if (errorMessage != null) {
+ line2.add(Span.styled(" " + errorMessage, Theme.error()));
+ }
+ lines.add(Line.from(line2));
+
+ frame.renderWidget(
+ Paragraph.builder().text(Text.from(lines))
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" JFR ").build())
+ .build(),
+ area);
+ }
+
+ private void renderDataTable(Frame frame, Rect area) {
+ if (!snapshotLoaded) {
+ String msg = loading.get()
+ ? " Loading..."
+ : " Press F5 to refresh JFR snapshot data";
frame.renderWidget(
Paragraph.builder()
- .text(Text.from(Line.from(Span.styled(" Loading
JFR status...", LABEL))))
+ .text(Text.from(Line.from(Span.styled(msg,
LABEL))))
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" JFR Runtime Instrumentation
").build())
+ .title(" " + activeView.label + "
").build())
.build(),
area);
return;
}
- if (errorMessage != null && !dataLoaded) {
+ switch (activeView) {
+ case ROUTES -> renderRoutesTable(frame, area);
+ case PROCESSORS -> renderProcessorsTable(frame, area);
+ case ENDPOINTS -> renderEndpointsTable(frame, area);
+ case FAILURES -> renderFailuresTable(frame, area);
+ case REDELIVERIES -> renderRedeliveriesTable(frame, area);
+ }
+ }
+
+ private void renderRoutesTable(Frame frame, Rect area) {
+ List<RouteStats> data = sortedRoutes();
+
+ if (topPanelHeight < 0) {
+ topPanelHeight = area.height() * 45 / 100;
+ }
+ topPanelHeight = Math.max(3, Math.min(topPanelHeight, area.height() -
5));
+ List<Rect> chunks = Layout.vertical()
+ .constraints(Constraint.length(topPanelHeight),
Constraint.fill())
+ .split(area);
+
+ long maxMean = data.stream().mapToLong(r -> (long)
r.meanMs).max().orElse(1);
+ if (maxMean <= 0) {
+ maxMean = 1;
+ }
+
+ List<Row> rows = new ArrayList<>();
+ for (RouteStats r : data) {
+ Style nameStyle = r.failed > 0 ? Theme.error() :
Style.EMPTY.fg(Theme.accent());
+ String bar = buildBar((long) r.meanMs, maxMean, 20);
+ Style barStyle = topTimeStyle((long) r.meanMs);
+ if (barStyle == Style.EMPTY) {
+ barStyle = Style.EMPTY.fg(Theme.accent());
+ }
+ rows.add(Row.from(
+ Cell.from(Span.styled(r.routeId, nameStyle)),
+ Cell.from(Span.styled(bar, barStyle)),
+ rightCell(String.valueOf(r.total), 8),
+ rightCell(String.valueOf(r.failed), 8, r.failed > 0 ?
Theme.error() : Style.EMPTY),
+ rightCell(formatMs(r.minMs), 10),
+ rightCell(formatMs(r.meanMs), 10, topTimeStyle((long)
r.meanMs)),
+ rightCell(formatMs(r.maxMs), 10, topTimeStyle((long)
r.maxMs))));
+ }
+
+ Table table = Table.builder()
+ .rows(rows)
+ .header(Row.from(
+ Cell.from(Span.styled(routeSortLabel("ROUTE",
"route"), routeSortStyle("route"))),
+ Cell.from(""),
+ rightCell(routeSortLabel("TOTAL", "total"), 8,
routeSortStyle("total")),
+ rightCell(routeSortLabel("FAILED", "failed"), 8,
routeSortStyle("failed")),
+ rightCell(routeSortLabel("MIN", "min"), 10,
routeSortStyle("min")),
+ rightCell(routeSortLabel("MEAN", "mean"), 10,
routeSortStyle("mean")),
+ rightCell(routeSortLabel("MAX", "max"), 10,
routeSortStyle("max"))))
+ .widths(Constraint.length(24), Constraint.fill(),
+ Constraint.length(8), Constraint.length(8),
+ Constraint.length(10), Constraint.length(10),
Constraint.length(10))
+ .highlightStyle(Theme.selectionBg())
+ .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" Routes (" + data.size() + ") ").build())
+ .build();
+ lastTableArea = chunks.get(0);
+ frame.renderStatefulWidget(table, chunks.get(0), tableState);
+ renderTableScrollbar(frame, lastTableArea, tableState, scrollState,
data.size());
+
+ Integer selectedRoute = tableState.selected();
+ if (selectedRoute != null && selectedRoute >= 0 && selectedRoute <
data.size()) {
+ RouteStats route = data.get(selectedRoute);
+ renderProcessorPanel(frame, chunks.get(1), route.routeId);
+ } else if (!data.isEmpty()) {
+ renderProcessorPanel(frame, chunks.get(1), data.get(0).routeId);
+ } else {
frame.renderWidget(
Paragraph.builder()
- .text(Text.from(Line.from(Span.styled(" " +
errorMessage, Theme.error()))))
+ .text(Text.from(Line.from(Span.styled("No routes",
Style.EMPTY.dim()))))
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" JFR Runtime Instrumentation
").build())
+ .title(" Processors ").build())
.build(),
- area);
- return;
+ chunks.get(1));
}
+ }
- List<Line> lines = new ArrayList<>();
- lines.add(Line.from(
- Span.styled(" registered: ", LABEL),
- Span.styled(String.valueOf(registered), registered ? OK :
Theme.error())));
- lines.add(Line.from(Span.raw("")));
- if (recordings.isEmpty()) {
- lines.add(Line.from(Span.styled(" no active recording", LABEL)));
- } else {
- for (String recording : recordings) {
- lines.add(Line.from(Span.styled(" recording: ", LABEL),
Span.styled(recording, OK)));
+ private void renderProcessorPanel(Frame frame, Rect area, String routeId) {
+ List<ProcessorStats> data = processorData.stream()
+ .filter(p -> routeId.equals(p.routeId))
+ .toList();
+ data = sortProcessors(data);
+
+ long maxMean = data.stream().mapToLong(p -> (long)
p.meanMs).max().orElse(1);
+ if (maxMean <= 0) {
+ maxMean = 1;
+ }
+
+ List<Row> rows = new ArrayList<>();
+ for (ProcessorStats p : data) {
+ Style nameStyle = p.failed > 0 ? Theme.error() :
Style.EMPTY.fg(Theme.accent());
+ String bar = buildBar((long) p.meanMs, maxMean, 20);
+ Style barStyle = topTimeStyle((long) p.meanMs);
+ if (barStyle == Style.EMPTY) {
+ barStyle = Style.EMPTY.fg(Theme.accent());
}
+ rows.add(Row.from(
+ Cell.from(Span.styled(p.processorId, nameStyle)),
+ Cell.from(Span.styled(p.processorType != null ?
p.processorType : "", LABEL)),
+ Cell.from(Span.styled(bar, barStyle)),
+ rightCell(String.valueOf(p.total), 8),
+ rightCell(String.valueOf(p.failed), 8, p.failed > 0 ?
Theme.error() : Style.EMPTY),
+ rightCell(formatMs(p.minMs), 10),
+ rightCell(formatMs(p.meanMs), 10, topTimeStyle((long)
p.meanMs)),
+ rightCell(formatMs(p.maxMs), 10, topTimeStyle((long)
p.maxMs))));
}
- lines.add(Line.from(Span.raw("")));
- for (Map.Entry<String, Boolean> entry : events.entrySet()) {
- boolean enabled = Boolean.TRUE.equals(entry.getValue());
- lines.add(Line.from(
- Span.styled(" " + entry.getKey() + ": ", LABEL),
- Span.styled(enabled ? "enabled" : "disabled", enabled ? OK
: Theme.error())));
+
+ Table table = Table.builder()
+ .rows(rows)
+ .header(Row.from(
+ Cell.from(Span.styled("PROCESSOR",
Style.EMPTY.bold())),
+ Cell.from(Span.styled("TYPE", Style.EMPTY.bold())),
+ Cell.from(""),
+ rightCell("TOTAL", 8, Style.EMPTY.bold()),
+ rightCell("FAILED", 8, Style.EMPTY.bold()),
+ rightCell("MIN", 10, Style.EMPTY.bold()),
+ rightCell("MEAN", 10, Style.EMPTY.bold()),
+ rightCell("MAX", 10, Style.EMPTY.bold())))
+ .widths(Constraint.fill(), Constraint.length(12),
Constraint.length(22),
+ Constraint.length(8), Constraint.length(8),
+ Constraint.length(10), Constraint.length(10),
Constraint.length(10))
+ .highlightStyle(Theme.selectionBg())
+ .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" Processors [" + routeId + "] (" + data.size()
+ ") ").build())
+ .build();
+ frame.renderStatefulWidget(table, area, processorTableState);
+ renderTableScrollbar(frame, area, processorTableState,
processorScrollState, data.size());
+ }
+
+ private void renderProcessorsTable(Frame frame, Rect area) {
+ List<ProcessorStats> data = sortProcessors(processorData);
+
+ long maxMean = data.stream().mapToLong(p -> (long)
p.meanMs).max().orElse(1);
+ if (maxMean <= 0) {
+ maxMean = 1;
}
- if (message != null) {
- lines.add(Line.from(Span.raw("")));
- lines.add(Line.from(Span.styled(" " + message, LABEL)));
+
+ List<Row> rows = new ArrayList<>();
+ for (ProcessorStats p : data) {
+ Style nameStyle = p.failed > 0 ? Theme.error() :
Style.EMPTY.fg(Theme.accent());
+ String bar = buildBar((long) p.meanMs, maxMean, 20);
+ Style barStyle = topTimeStyle((long) p.meanMs);
+ if (barStyle == Style.EMPTY) {
+ barStyle = Style.EMPTY.fg(Theme.accent());
+ }
+ rows.add(Row.from(
+ Cell.from(Span.styled(p.processorId, nameStyle)),
+ Cell.from(Span.styled(p.processorType != null ?
p.processorType : "", LABEL)),
+ Cell.from(Span.styled(p.routeId, LABEL)),
+ Cell.from(Span.styled(bar, barStyle)),
+ rightCell(String.valueOf(p.total), 8),
+ rightCell(String.valueOf(p.failed), 8, p.failed > 0 ?
Theme.error() : Style.EMPTY),
+ rightCell(formatMs(p.minMs), 10),
+ rightCell(formatMs(p.meanMs), 10, topTimeStyle((long)
p.meanMs)),
+ rightCell(formatMs(p.maxMs), 10, topTimeStyle((long)
p.maxMs))));
}
- // an error raised after the status was loaded, so it is shown inline
rather than replacing the whole pane
- if (errorMessage != null) {
- lines.add(Line.from(Span.raw("")));
- lines.add(Line.from(Span.styled(" " + errorMessage,
Theme.error())));
+
+ Table table = Table.builder()
+ .rows(rows)
+ .header(Row.from(
+ Cell.from(Span.styled(processorSortLabel("PROCESSOR",
"processor"),
+ processorSortStyle("processor"))),
+ Cell.from(Span.styled("TYPE", Style.EMPTY.bold())),
+ Cell.from(Span.styled("ROUTE", Style.EMPTY.bold())),
+ Cell.from(""),
+ rightCell(processorSortLabel("TOTAL", "total"), 8,
processorSortStyle("total")),
+ rightCell(processorSortLabel("FAILED", "failed"), 8,
processorSortStyle("failed")),
+ rightCell(processorSortLabel("MIN", "min"), 10,
processorSortStyle("min")),
+ rightCell(processorSortLabel("MEAN", "mean"), 10,
processorSortStyle("mean")),
+ rightCell(processorSortLabel("MAX", "max"), 10,
processorSortStyle("max"))))
+ .widths(Constraint.fill(), Constraint.length(12),
Constraint.length(16),
+ Constraint.length(22),
+ Constraint.length(8), Constraint.length(8),
+ Constraint.length(10), Constraint.length(10),
Constraint.length(10))
+ .highlightStyle(Theme.selectionBg())
+ .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" Processors (" + data.size() + ") ").build())
+ .build();
+ lastTableArea = area;
+ frame.renderStatefulWidget(table, area, tableState);
+ renderTableScrollbar(frame, lastTableArea, tableState, scrollState,
data.size());
+ }
+
+ private void renderEndpointsTable(Frame frame, Rect area) {
+ List<EndpointStats> data = sortedEndpoints();
+
+ long maxMean = data.stream().mapToLong(e -> (long)
e.meanMs).max().orElse(1);
+ if (maxMean <= 0) {
+ maxMean = 1;
}
- frame.renderWidget(
- Paragraph.builder()
- .text(Text.from(lines))
-
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" JFR Runtime Instrumentation
").build())
- .build(),
- area);
+ List<Row> rows = new ArrayList<>();
+ for (EndpointStats e : data) {
+ Style nameStyle = e.failed > 0 ? Theme.error() :
Style.EMPTY.fg(Theme.accent());
+ String bar = buildBar((long) e.meanMs, maxMean, 20);
+ Style barStyle = topTimeStyle((long) e.meanMs);
+ if (barStyle == Style.EMPTY) {
+ barStyle = Style.EMPTY.fg(Theme.accent());
+ }
+ rows.add(Row
+ .from(
+ Cell.from(Span.styled(e.endpointUri, nameStyle)),
+ Cell.from(Span.styled(bar, barStyle)),
+ rightCell(String.valueOf(e.total), 8),
+ rightCell(String.valueOf(e.failed), 8, e.failed > 0 ?
Theme.error() : Style.EMPTY),
+ rightCell(formatMs(e.minMs), 10),
+ rightCell(formatMs(e.meanMs), 10, topTimeStyle((long)
e.meanMs)),
+ rightCell(formatMs(e.maxMs), 10, topTimeStyle((long)
e.maxMs))));
+ }
+
+ Table table = Table.builder()
+ .rows(rows)
+ .header(Row.from(
+ Cell.from(Span.styled(endpointSortLabel("ENDPOINT",
"endpoint"),
+ endpointSortStyle("endpoint"))),
+ Cell.from(""),
+ rightCell(endpointSortLabel("TOTAL", "total"), 8,
endpointSortStyle("total")),
+ rightCell(endpointSortLabel("FAILED", "failed"), 8,
endpointSortStyle("failed")),
+ rightCell(endpointSortLabel("MIN", "min"), 10,
endpointSortStyle("min")),
+ rightCell(endpointSortLabel("MEAN", "mean"), 10,
endpointSortStyle("mean")),
+ rightCell(endpointSortLabel("MAX", "max"), 10,
endpointSortStyle("max"))))
+ .widths(Constraint.length(30), Constraint.fill(),
+ Constraint.length(8), Constraint.length(8),
+ Constraint.length(10), Constraint.length(10),
Constraint.length(10))
+ .highlightStyle(Theme.selectionBg())
+ .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" Endpoints (" + data.size() + ") ").build())
+ .build();
+ lastTableArea = area;
+ frame.renderStatefulWidget(table, area, tableState);
+ renderTableScrollbar(frame, lastTableArea, tableState, scrollState,
data.size());
+ }
+
+ private void renderFailuresTable(Frame frame, Rect area) {
+ List<Row> rows = new ArrayList<>();
+ for (FailureEntry f : failureData) {
+ String time = f.timestamp != null && f.timestamp.length() > 19
+ ? f.timestamp.substring(11, 19) : (f.timestamp != null ?
f.timestamp : "");
+ String msg = f.exceptionMessage != null
+ ? (f.exceptionMessage.length() > 60 ?
f.exceptionMessage.substring(0, 60) + "..." : f.exceptionMessage)
+ : "";
+ rows.add(Row.from(
+ Cell.from(Span.styled(time, LABEL)),
+ Cell.from(Span.styled(f.routeId != null ? f.routeId : "",
Style.EMPTY.fg(Theme.baseFg()))),
+ Cell.from(Span.styled(
+ f.exceptionType != null ?
shortClassName(f.exceptionType) : "", Theme.error())),
+ Cell.from(Span.styled(msg, LABEL))));
+ }
+
+ Style hdr = Style.EMPTY.bold();
+ Table table = Table.builder()
+ .rows(rows)
+ .header(Row.from(
+ Cell.from(Span.styled("TIME", hdr)),
+ Cell.from(Span.styled("ROUTE", hdr)),
+ Cell.from(Span.styled("EXCEPTION", hdr)),
+ Cell.from(Span.styled("MESSAGE", hdr))))
+ .widths(Constraint.length(10), Constraint.length(16),
+ Constraint.length(28), Constraint.fill())
+ .highlightStyle(Theme.selectionBg())
+ .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" Failures (" + failureData.size() + ")
").build())
+ .build();
+ lastTableArea = area;
+ frame.renderStatefulWidget(table, area, tableState);
+ renderTableScrollbar(frame, lastTableArea, tableState, scrollState,
failureData.size());
+ }
+
+ private void renderRedeliveriesTable(Frame frame, Rect area) {
+ List<Row> rows = new ArrayList<>();
+ for (RedeliveryEntry r : redeliveryData) {
+ String time = r.timestamp != null && r.timestamp.length() > 19
+ ? r.timestamp.substring(11, 19) : (r.timestamp != null ?
r.timestamp : "");
+ boolean exhausted = r.attempt >= r.maxAttempts && r.maxAttempts >
0;
+ rows.add(Row.from(
+ Cell.from(Span.styled(time, LABEL)),
+ Cell.from(Span.styled(r.routeId != null ? r.routeId : "",
Style.EMPTY.fg(Theme.baseFg()))),
+ rightCell(String.valueOf(r.attempt), 8),
+ rightCell(String.valueOf(r.maxAttempts), 8, exhausted ?
Theme.error() : Style.EMPTY)));
+ }
+
+ Style hdr = Style.EMPTY.bold();
+ Table table = Table.builder()
+ .rows(rows)
+ .header(Row.from(
+ Cell.from(Span.styled("TIME", hdr)),
+ Cell.from(Span.styled("ROUTE", hdr)),
+ rightCell("ATTEMPT", 8, hdr),
+ rightCell("MAX", 8, hdr)))
+ .widths(Constraint.length(10), Constraint.fill(),
+ Constraint.length(8), Constraint.length(8))
+ .highlightStyle(Theme.selectionBg())
+ .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" Redeliveries (" + redeliveryData.size() + ")
").build())
+ .build();
+ lastTableArea = area;
+ frame.renderStatefulWidget(table, area, tableState);
+ renderTableScrollbar(frame, lastTableArea, tableState, scrollState,
redeliveryData.size());
+ }
+
+ private static String formatMs(double ms) {
+ if (ms < 1.0) {
+ return String.format(Locale.US, "%.1fms", ms);
+ } else if (ms < 1000.0) {
+ return String.format(Locale.US, "%.0fms", ms);
+ } else {
+ return String.format(Locale.US, "%.1fs", ms / 1000.0);
+ }
+ }
+
+ private static String shortClassName(String fqcn) {
+ int dot = fqcn.lastIndexOf('.');
+ return dot >= 0 ? fqcn.substring(dot + 1) : fqcn;
}
@Override
public void renderFooter(List<Span> spans) {
hint(spans, "Esc", "back");
- hint(spans, "E", "enable all");
- hint(spans, "D", "disable all");
- hint(spans, "J", "generate .jfc");
+ hint(spans, "Space", "view");
+ if (snapshotLoaded && hasSortColumns()) {
+ hint(spans, "s", "sort");
+ }
hintLast(spans, "F5", "refresh");
}
- private void refreshStatus() {
- if (ctx.selectedPid == null) {
- return;
+ // ---- sorting ----
+
+ private void cycleSortForward() {
+ switch (activeView) {
+ case ROUTES -> {
+ routeSortIndex = (routeSortIndex + 1) %
ROUTE_SORT_COLUMNS.length;
+ routeSort = ROUTE_SORT_COLUMNS[routeSortIndex];
+ routeSortReversed = false;
+ }
+ case PROCESSORS -> {
+ processorSortIndex = (processorSortIndex + 1) %
PROCESSOR_SORT_COLUMNS.length;
+ processorSort = PROCESSOR_SORT_COLUMNS[processorSortIndex];
+ processorSortReversed = false;
+ }
+ case ENDPOINTS -> {
+ endpointSortIndex = (endpointSortIndex + 1) %
ENDPOINT_SORT_COLUMNS.length;
+ endpointSort = ENDPOINT_SORT_COLUMNS[endpointSortIndex];
+ endpointSortReversed = false;
+ }
+ default -> {
+ }
}
- if (!loading.compareAndSet(false, true)) {
- return;
+ }
+
+ private void cycleSortReverse() {
+ switch (activeView) {
+ case ROUTES -> routeSortReversed = !routeSortReversed;
+ case PROCESSORS -> processorSortReversed = !processorSortReversed;
+ case ENDPOINTS -> endpointSortReversed = !endpointSortReversed;
+ default -> {
+ }
}
+ }
- String pid = ctx.selectedPid;
- ctx.backgroundExecutor.execute(() -> {
- try {
- JsonObject root = new JsonObject();
- root.put("action", "jfr");
- root.put("command", "status");
+ private boolean hasSortColumns() {
+ return activeView == View.ROUTES || activeView == View.PROCESSORS ||
activeView == View.ENDPOINTS;
+ }
- JsonObject jo = ctx.executeAction(pid, root, 5000);
- applyStatus(jo);
- } catch (Exception e) {
- applyError("Error: " + e.getMessage());
- } finally {
- loading.set(false);
- }
- });
+ private String routeSortLabel(String label, String column) {
+ return sortLabel(label, column, routeSort, routeSortReversed);
+ }
+
+ private Style routeSortStyle(String column) {
+ return sortStyle(column, routeSort);
+ }
+
+ private String processorSortLabel(String label, String column) {
+ return sortLabel(label, column, processorSort, processorSortReversed);
+ }
+
+ private Style processorSortStyle(String column) {
+ return sortStyle(column, processorSort);
+ }
+
+ private String endpointSortLabel(String label, String column) {
+ return sortLabel(label, column, endpointSort, endpointSortReversed);
+ }
+
+ private Style endpointSortStyle(String column) {
+ return sortStyle(column, endpointSort);
}
- private void toggleAll(boolean enable) {
+ private List<RouteStats> sortedRoutes() {
+ if (routeData.isEmpty()) {
+ return routeData;
+ }
+ List<RouteStats> sorted = new ArrayList<>(routeData);
+ if ("route".equals(routeSort)) {
+ sorted.sort(routeSortReversed
+ ? Comparator.comparing(RouteStats::routeId,
String.CASE_INSENSITIVE_ORDER).reversed()
+ : Comparator.comparing(RouteStats::routeId,
String.CASE_INSENSITIVE_ORDER));
+ } else {
+ Comparator<RouteStats> cmp = switch (routeSort) {
+ case "failed" -> Comparator.comparingLong(RouteStats::failed);
+ case "min" -> Comparator.comparingDouble(RouteStats::minMs);
+ case "mean" -> Comparator.comparingDouble(RouteStats::meanMs);
+ case "max" -> Comparator.comparingDouble(RouteStats::maxMs);
+ default -> Comparator.comparingLong(RouteStats::total);
+ };
+ sorted.sort(routeSortReversed ? cmp : cmp.reversed());
+ }
+ return sorted;
+ }
+
+ private List<ProcessorStats> sortProcessors(List<ProcessorStats> input) {
+ if (input.isEmpty()) {
+ return input;
+ }
+ List<ProcessorStats> sorted = new ArrayList<>(input);
+ if ("processor".equals(processorSort)) {
+ sorted.sort(processorSortReversed
+ ? Comparator.comparing(ProcessorStats::processorId,
String.CASE_INSENSITIVE_ORDER).reversed()
+ : Comparator.comparing(ProcessorStats::processorId,
String.CASE_INSENSITIVE_ORDER));
+ } else {
+ Comparator<ProcessorStats> cmp = switch (processorSort) {
+ case "total" ->
Comparator.comparingLong(ProcessorStats::total);
+ case "failed" ->
Comparator.comparingLong(ProcessorStats::failed);
+ case "min" ->
Comparator.comparingDouble(ProcessorStats::minMs);
+ case "max" ->
Comparator.comparingDouble(ProcessorStats::maxMs);
+ default -> Comparator.comparingDouble(ProcessorStats::meanMs);
+ };
+ sorted.sort(processorSortReversed ? cmp : cmp.reversed());
+ }
+ return sorted;
+ }
+
+ private List<EndpointStats> sortedEndpoints() {
+ if (endpointData.isEmpty()) {
+ return endpointData;
+ }
+ List<EndpointStats> sorted = new ArrayList<>(endpointData);
+ if ("endpoint".equals(endpointSort)) {
+ sorted.sort(endpointSortReversed
+ ? Comparator.comparing(EndpointStats::endpointUri,
String.CASE_INSENSITIVE_ORDER).reversed()
+ : Comparator.comparing(EndpointStats::endpointUri,
String.CASE_INSENSITIVE_ORDER));
+ } else {
+ Comparator<EndpointStats> cmp = switch (endpointSort) {
+ case "failed" ->
Comparator.comparingLong(EndpointStats::failed);
+ case "min" -> Comparator.comparingDouble(EndpointStats::minMs);
+ case "mean" ->
Comparator.comparingDouble(EndpointStats::meanMs);
+ case "max" -> Comparator.comparingDouble(EndpointStats::maxMs);
+ default -> Comparator.comparingLong(EndpointStats::total);
+ };
+ sorted.sort(endpointSortReversed ? cmp : cmp.reversed());
+ }
+ return sorted;
+ }
+
+ // ---- server communication ----
+
+ private void refreshStatus() {
if (ctx.selectedPid == null) {
return;
}
@@ -218,131 +814,358 @@ class JfrTab extends AbstractTab {
String pid = ctx.selectedPid;
ctx.backgroundExecutor.execute(() -> {
+ boolean chained = false;
try {
JsonObject root = new JsonObject();
root.put("action", "jfr");
- root.put("command", enable ? "enable" : "disable");
- root.put("event", "all");
+ root.put("command", "status");
JsonObject jo = ctx.executeAction(pid, root, 5000);
- String result = jo != null ? jo.getString("result") : null;
- if (result == null) {
- applyError(errorOf(jo, "No response from integration"));
+ applyStatus(jo);
+
+ if (jo != null && jo.get("recordings") instanceof JsonArray
recs && !recs.isEmpty()
+ && !snapshotLoaded) {
+ chained = true;
+ doTakeSnapshot();
return;
}
- if (Boolean.TRUE.equals(jo.getBoolean("success"))) {
- applyMessage(result);
- } else {
- applyError(result);
- }
} catch (Exception e) {
applyError("Error: " + e.getMessage());
- return;
} finally {
- loading.set(false);
+ if (!chained) {
+ loading.set(false);
+ }
}
- // only refresh once the loading flag is released, as
refreshStatus takes it again
- refreshStatus();
});
}
- private void generateJfc() {
+ private void takeSnapshot() {
if (ctx.selectedPid == null) {
return;
}
if (!loading.compareAndSet(false, true)) {
return;
}
+ doTakeSnapshot();
+ }
+ private void doTakeSnapshot() {
String pid = ctx.selectedPid;
ctx.backgroundExecutor.execute(() -> {
try {
JsonObject root = new JsonObject();
root.put("action", "jfr");
- root.put("command", "jfc");
+ root.put("command", "snapshot");
- JsonObject jo = ctx.executeAction(pid, root, 5000);
- String jfc = jo != null ? jo.getString("jfc") : null;
- if (jfc == null) {
- applyError(errorOf(jo, "No response from integration"));
- } else if (ctx.openMarkdownCallback != null && ctx.runner !=
null) {
- ctx.runner.runOnRenderThread(
- () -> ctx.openMarkdownCallback.accept("JFR .jfc
overlay", "```\n" + jfc + "\n```"));
- } else {
- applyMessage(jfc);
- }
+ JsonObject jo = ctx.executeAction(pid, root, 30000);
+ applySnapshot(jo);
} catch (Exception e) {
- applyError("Error: " + e.getMessage());
+ applyError("Error taking snapshot: " + e.getMessage());
} finally {
loading.set(false);
}
});
}
+ // ---- apply results on render thread ----
+
private void applyStatus(JsonObject jo) {
renderThreadExecutor.accept(() -> {
if (jo == null) {
errorMessage = "No response from integration";
- dataLoaded = true;
+ statusLoaded = true;
return;
}
String error = jo.getString("error");
if (error != null) {
errorMessage = error;
- dataLoaded = true;
+ statusLoaded = true;
return;
}
- registered = Boolean.TRUE.equals(jo.getBoolean("registered"));
+ registered = Boolean.TRUE.equals(jo.getBoolean("runtimeEvents"));
List<String> recs = new ArrayList<>();
if (jo.get("recordings") instanceof JsonArray recordingsArr) {
for (Object o : recordingsArr) {
if (o instanceof JsonObject rec) {
- recs.add(rec.getString("name") + " (state=" +
rec.getString("state") + ")");
+ recs.add(rec.getString("name") + " (" +
rec.getString("state") + ")");
}
}
}
recordings = recs;
- Map<String, Boolean> evts = new LinkedHashMap<>();
- if (jo.get("events") instanceof JsonObject eventsObj) {
- eventsObj.forEach((k, v) -> evts.put(k,
Boolean.TRUE.equals(v)));
- }
- events = evts;
errorMessage = null;
- dataLoaded = true;
+ statusLoaded = true;
});
}
- /**
- * The integration reports why it could not answer in an {@code error}
field, which is more useful than the generic
- * fallback.
- */
- private static String errorOf(JsonObject jo, String fallback) {
- String error = jo != null ? jo.getString("error") : null;
- return error != null ? error : fallback;
- }
-
- private void applyMessage(String msg) {
+ private void applySnapshot(JsonObject jo) {
renderThreadExecutor.accept(() -> {
- message = msg;
+ if (jo == null) {
+ errorMessage = "No snapshot response from integration";
+ return;
+ }
+ String error = jo.getString("error");
+ if (error != null) {
+ errorMessage = error;
+ return;
+ }
+
+ snapshotEventCount = jo.getInteger("eventCount") != null ?
jo.getInteger("eventCount") : 0;
+
+ routeData = parseRoutes(jo);
+ processorData = parseProcessors(jo);
+ endpointData = parseEndpoints(jo);
+ failureData = parseFailures(jo);
+ redeliveryData = parseRedeliveries(jo);
+
errorMessage = null;
+ Long ts = jo.getLong("snapshotTimestamp");
+ snapshotTime = ts != null ? ts : System.currentTimeMillis();
+ snapshotLoaded = true;
+ tableState.select(0);
});
}
+ private static List<RouteStats> parseRoutes(JsonObject jo) {
+ List<RouteStats> result = new ArrayList<>();
+ if (jo.get("routes") instanceof JsonArray arr) {
+ for (Object o : arr) {
+ if (o instanceof JsonObject r) {
+ result.add(new RouteStats(
+ r.getString("routeId"),
+ longVal(r, "total"),
+ longVal(r, "failed"),
+ doubleVal(r, "minMs"),
+ doubleVal(r, "meanMs"),
+ doubleVal(r, "maxMs")));
+ }
+ }
+ }
+ return result;
+ }
+
+ private static List<ProcessorStats> parseProcessors(JsonObject jo) {
+ List<ProcessorStats> result = new ArrayList<>();
+ if (jo.get("processors") instanceof JsonArray arr) {
+ for (Object o : arr) {
+ if (o instanceof JsonObject p) {
+ result.add(new ProcessorStats(
+ p.getString("processorId"),
+ p.getString("processorType"),
+ p.getString("routeId"),
+ longVal(p, "total"),
+ longVal(p, "failed"),
+ doubleVal(p, "minMs"),
+ doubleVal(p, "meanMs"),
+ doubleVal(p, "maxMs")));
+ }
+ }
+ }
+ return result;
+ }
+
+ private static List<EndpointStats> parseEndpoints(JsonObject jo) {
+ List<EndpointStats> result = new ArrayList<>();
+ if (jo.get("endpoints") instanceof JsonArray arr) {
+ for (Object o : arr) {
+ if (o instanceof JsonObject e) {
+ result.add(new EndpointStats(
+ e.getString("endpointUri"),
+ longVal(e, "total"),
+ longVal(e, "failed"),
+ doubleVal(e, "minMs"),
+ doubleVal(e, "meanMs"),
+ doubleVal(e, "maxMs")));
+ }
+ }
+ }
+ return result;
+ }
+
+ private static List<FailureEntry> parseFailures(JsonObject jo) {
+ List<FailureEntry> result = new ArrayList<>();
+ if (jo.get("failures") instanceof JsonArray arr) {
+ for (Object o : arr) {
+ if (o instanceof JsonObject f) {
+ result.add(new FailureEntry(
+ f.getString("timestamp"),
+ f.getString("exchangeId"),
+ f.getString("routeId"),
+ f.getString("exceptionType"),
+ f.getString("exceptionMessage")));
+ }
+ }
+ }
+ return result;
+ }
+
+ private static List<RedeliveryEntry> parseRedeliveries(JsonObject jo) {
+ List<RedeliveryEntry> result = new ArrayList<>();
+ if (jo.get("redeliveries") instanceof JsonArray arr) {
+ for (Object o : arr) {
+ if (o instanceof JsonObject r) {
+ result.add(new RedeliveryEntry(
+ r.getString("timestamp"),
+ r.getString("exchangeId"),
+ r.getString("routeId"),
+ intVal(r, "attempt"),
+ intVal(r, "maxAttempts")));
+ }
+ }
+ }
+ return result;
+ }
+
+ private static long longVal(JsonObject jo, String key) {
+ Object v = jo.get(key);
+ if (v instanceof Number n) {
+ return n.longValue();
+ }
+ return 0;
+ }
+
+ private static double doubleVal(JsonObject jo, String key) {
+ Object v = jo.get(key);
+ if (v instanceof Number n) {
+ return n.doubleValue();
+ }
+ return 0;
+ }
+
+ private static int intVal(JsonObject jo, String key) {
+ Object v = jo.get(key);
+ if (v instanceof Number n) {
+ return n.intValue();
+ }
+ return 0;
+ }
+
private void applyError(String error) {
renderThreadExecutor.accept(() -> {
- message = null;
errorMessage = error;
});
}
@Override
public SelectionContext getSelectionContext() {
- return null;
+ if (!snapshotLoaded) {
+ return null;
+ }
+ int sel = tableState.selected() != null ? tableState.selected() : -1;
+ return switch (activeView) {
+ case ROUTES -> {
+ List<RouteStats> data = sortedRoutes();
+ yield sel >= 0 && sel < data.size()
+ ? new SelectionContext(
+ "route",
+
data.stream().map(RouteStats::routeId).toList(),
+ sel, data.size(), data.get(sel).routeId)
+ : null;
+ }
+ case PROCESSORS -> {
+ List<ProcessorStats> data = sortProcessors(processorData);
+ yield sel >= 0 && sel < data.size()
+ ? new SelectionContext(
+ "processor",
+
data.stream().map(ProcessorStats::processorId).toList(),
+ sel, data.size(), data.get(sel).processorId)
+ : null;
+ }
+ case ENDPOINTS -> {
+ List<EndpointStats> data = sortedEndpoints();
+ yield sel >= 0 && sel < data.size()
+ ? new SelectionContext(
+ "endpoint",
+
data.stream().map(EndpointStats::endpointUri).toList(),
+ sel, data.size(), data.get(sel).endpointUri)
+ : null;
+ }
+ default -> null;
+ };
+ }
+
+ @Override
+ public JsonObject getTableDataAsJson() {
+ if (!snapshotLoaded) {
+ return null;
+ }
+ JsonObject result = new JsonObject();
+ result.put("tab", "JFR");
+ result.put("view", activeView.label);
+ result.put("eventCount", snapshotEventCount);
+
+ JsonArray rows = new JsonArray();
+ switch (activeView) {
+ case ROUTES -> {
+ for (RouteStats r : routeData) {
+ JsonObject row = new JsonObject();
+ row.put("routeId", r.routeId());
+ row.put("total", r.total());
+ row.put("failed", r.failed());
+ row.put("minMs", r.minMs());
+ row.put("meanMs", r.meanMs());
+ row.put("maxMs", r.maxMs());
+ rows.add(row);
+ }
+ }
+ case PROCESSORS -> {
+ for (ProcessorStats p : processorData) {
+ JsonObject row = new JsonObject();
+ row.put("processorId", p.processorId());
+ row.put("processorType", p.processorType());
+ row.put("routeId", p.routeId());
+ row.put("total", p.total());
+ row.put("failed", p.failed());
+ row.put("minMs", p.minMs());
+ row.put("meanMs", p.meanMs());
+ row.put("maxMs", p.maxMs());
+ rows.add(row);
+ }
+ }
+ case ENDPOINTS -> {
+ for (EndpointStats e : endpointData) {
+ JsonObject row = new JsonObject();
+ row.put("endpointUri", e.endpointUri());
+ row.put("total", e.total());
+ row.put("failed", e.failed());
+ row.put("minMs", e.minMs());
+ row.put("meanMs", e.meanMs());
+ row.put("maxMs", e.maxMs());
+ rows.add(row);
+ }
+ }
+ case FAILURES -> {
+ for (FailureEntry f : failureData) {
+ JsonObject row = new JsonObject();
+ row.put("timestamp", f.timestamp());
+ row.put("exchangeId", f.exchangeId());
+ row.put("routeId", f.routeId());
+ row.put("exceptionType", f.exceptionType());
+ row.put("exceptionMessage", f.exceptionMessage());
+ rows.add(row);
+ }
+ }
+ case REDELIVERIES -> {
+ for (RedeliveryEntry r : redeliveryData) {
+ JsonObject row = new JsonObject();
+ row.put("timestamp", r.timestamp());
+ row.put("exchangeId", r.exchangeId());
+ row.put("routeId", r.routeId());
+ row.put("attempt", r.attempt());
+ row.put("maxAttempts", r.maxAttempts());
+ rows.add(row);
+ }
+ }
+ }
+ result.put("rows", rows);
+ result.put("totalRows", rows.size());
+ int sel = tableState.selected() != null ? tableState.selected() : -1;
+ result.put("selectedIndex", sel);
+ return result;
}
@Override
public String description() {
- return "JFR runtime instrumentation status and live event toggling";
+ return "JFR runtime profiling with route, processor, and endpoint
statistics";
}
@Override
@@ -350,21 +1173,30 @@ class JfrTab extends AbstractTab {
return """
# JFR
- The JFR tab shows the live status of camel-jfr's runtime
instrumentation:
- whether it is registered, any active Flight Recorder
recording(s), and the
- current enabled/disabled state of each of the six runtime
events (route,
- processor, exchange, send, failed, redelivery).
+ The JFR tab shows the live status of camel-jfr's runtime
instrumentation
+ and lets you view aggregated runtime data from JFR recordings.
+
+ ## Data Views
+
+ Press **F5** to take a snapshot of the active JFR recording.
The snapshot
+ data is aggregated into five views:
+
+ - **Routes** — per-route exchange count, failures, and timing
with
+ a processor panel below showing processors for the selected
route
+ - **Processors** — per-processor invocation count and timing
across all routes
+ - **Endpoints** — per-endpoint send count and timing
+ - **Failures** — recent exchange failures with exception
details
+ - **Redeliveries** — recent redelivery attempts
- ## Keys
+ ## Controls
- - `E` — enable all runtime events on every active recording
- - `D` — disable all runtime events on every active recording
- - `J` — generate a `.jfc` overlay with the current event
selection
- - `F5` — refresh status
+ - `F5` — refresh (take new JFR snapshot)
+ - `Space` — cycle view
+ - `s` / `S` — cycle sort column / reverse sort direction
- `Esc` — back
- Toggling requires at least one active recording; start one
first via
- `--jfr`, `jcmd <pid> JFR.start`, or JMX.
+ Requires at least one active recording; start one via `--jfr`,
+ `jcmd <pid> JFR.start`, or JMX.
""";
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index 80b783a1b2d2..c81494078da6 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -212,6 +212,9 @@ class TabRegistry {
new MoreTab(
TuiIcons.TAB_SPANS, "Spans", "&OTel Spans", spansTab,
"Observability",
List.of("opentelemetry")),
+ new MoreTab(
+ TuiIcons.TAB_JFR, "JFR", "J&FR", jfrTab,
"Observability",
+ List.of("jfr")),
// Data
new MoreTab(
TuiIcons.TAB_DATASOURCE, "JDBC DataSource", "&JDBC
DataSource", dataSourceTab, "Data",
@@ -233,9 +236,6 @@ class TabRegistry {
// JVM
new MoreTab(TuiIcons.TAB_CLASSPATH, "Classpath", "&Classpath",
classpathTab, "JVM"),
new MoreTab(TuiIcons.TAB_HEAP, "Heap Memory Histogram", "Heap
&Memory Histogram", heapHistogramTab, "JVM"),
- new MoreTab(
- TuiIcons.TAB_JFR, "Java Flight Recorder (JFR)", "Java
Fli&ght Recorder (JFR)", jfrTab, "JVM",
- List.of("jfr")),
new MoreTab(TuiIcons.TAB_MEMORY, "Memory Usage", "&Memory
Usage", memoryTab, "JVM"),
new MoreTab(TuiIcons.TAB_MEMORY_LEAK, "Memory Leak", "&Memory
Leak", memoryLeakTab, "JVM"),
new MoreTab(TuiIcons.TAB_PROCESS, "Process", "&Process",
processTab, "JVM"),
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTabRenderTest.java
index 4e6ca7a5633f..96bb71294a66 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTabRenderTest.java
@@ -22,6 +22,10 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -29,10 +33,6 @@ import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
-/**
- * Rendering tests for {@link JfrTab}. These tests render the tab into a
virtual terminal buffer via
- * {@link Frame#forTesting(Buffer)} and inspect the rendered cell content.
- */
class JfrTabRenderTest {
private MonitorContext ctx;
@@ -70,7 +70,7 @@ class JfrTabRenderTest {
void renderShowsRegisteredAndRecordingState() {
JfrTab tab = new JfrTab(ctx);
String rendered = TuiTestHelper.renderToString(tab, 120, 20);
- assertThat(rendered).contains("registered").contains("no active
recording");
+ assertThat(rendered).contains("Loading...");
}
@Test
@@ -95,6 +95,107 @@ class JfrTabRenderTest {
assertThat(footer).contains("Esc").contains("F5").contains("refresh");
}
+ @Test
+ void renderFooterShowsViewHint() {
+ JfrTab tab = new JfrTab(ctx);
+ List<Span> footerSpans = new ArrayList<>();
+ tab.renderFooter(footerSpans);
+ String footer = footerSpans.stream().map(Span::content).reduce("",
String::concat);
+
+ assertThat(footer).contains("Space").contains("view");
+ }
+
+ @Test
+ void renderSnapshotDataShowsRoutesTable() {
+ TestMonitorContext snapshotCtx = new
TestMonitorContext(dataWith(info), statusResponse())
+ .withSnapshot(snapshotResponse());
+ snapshotCtx.selectedPid = "1234";
+ JfrTab tab = new JfrTab(snapshotCtx, Runnable::run);
+
+ tab.onTabSelected();
+ tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.F5, KeyModifiers.NONE));
+
+ await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
+ String rendered = TuiTestHelper.renderToString(tab, 140, 30);
+ assertThat(rendered).contains("Routes");
+ });
+ }
+
+ @Test
+ void renderSnapshotDataShowsProcessorsPanelForSelectedRoute() {
+ TestMonitorContext snapshotCtx = new
TestMonitorContext(dataWith(info), statusResponse())
+ .withSnapshot(snapshotResponse());
+ snapshotCtx.selectedPid = "1234";
+ JfrTab tab = new JfrTab(snapshotCtx, Runnable::run);
+
+ tab.onTabSelected();
+
+ await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
+ String rendered = TuiTestHelper.renderToString(tab, 140, 30);
+ assertThat(rendered).contains("Enabled");
+ });
+
+ tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.F5, KeyModifiers.NONE));
+
+ await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
+ String rendered = TuiTestHelper.renderToString(tab, 140, 30);
+ assertThat(rendered).contains("Processors [notify]");
+ });
+ }
+
+ @Test
+ void renderFooterShowsSortHintWhenSnapshotLoaded() {
+ TestMonitorContext snapshotCtx = new
TestMonitorContext(dataWith(info), statusResponse())
+ .withSnapshot(snapshotResponse());
+ snapshotCtx.selectedPid = "1234";
+ JfrTab tab = new JfrTab(snapshotCtx, Runnable::run);
+
+ tab.onTabSelected();
+
+ await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
+ String rendered = TuiTestHelper.renderToString(tab, 140, 30);
+ assertThat(rendered).contains("Enabled");
+ });
+
+ tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.F5, KeyModifiers.NONE));
+
+ await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
+ String rendered = TuiTestHelper.renderToString(tab, 140, 30);
+ assertThat(rendered).contains("order-in");
+
+ List<Span> footerSpans = new ArrayList<>();
+ tab.renderFooter(footerSpans);
+ String footer = footerSpans.stream().map(Span::content).reduce("",
String::concat);
+ assertThat(footer).contains("s").contains("sort");
+ });
+ }
+
+ @Test
+ void viewSwitchingChangesActiveView() {
+ TestMonitorContext snapshotCtx = new
TestMonitorContext(dataWith(info), statusResponse())
+ .withSnapshot(snapshotResponse());
+ snapshotCtx.selectedPid = "1234";
+ JfrTab tab = new JfrTab(snapshotCtx, Runnable::run);
+
+ tab.onTabSelected();
+ tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.F5, KeyModifiers.NONE));
+ // Space cycles: Routes -> Processors -> Endpoints
+ tab.handleKeyEvent(KeyEvent.ofChar(' '));
+ tab.handleKeyEvent(KeyEvent.ofChar(' '));
+
+ await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
+ String rendered = TuiTestHelper.renderToString(tab, 140, 30);
+ assertThat(rendered).contains("Endpoints");
+ });
+ }
+
+ @Test
+ void promptToSnapshotWhenNoData() {
+ JfrTab tab = new JfrTab(ctx);
+ String rendered = TuiTestHelper.renderToString(tab, 140, 30);
+ assertThat(rendered).contains("F5");
+ }
+
@Test
void description() {
JfrTab tab = new JfrTab(ctx);
@@ -111,9 +212,76 @@ class JfrTabRenderTest {
return response;
}
+ private static JsonObject snapshotResponse() {
+ JsonObject response = new JsonObject();
+ response.put("snapshot", true);
+ response.put("eventCount", 150);
+
+ JsonArray routes = new JsonArray();
+ JsonObject route1 = new JsonObject();
+ route1.put("routeId", "order-in");
+ route1.put("total", 100L);
+ route1.put("failed", 5L);
+ route1.put("minMs", 1.2);
+ route1.put("meanMs", 8.5);
+ route1.put("maxMs", 250.3);
+ routes.add(route1);
+ JsonObject route2 = new JsonObject();
+ route2.put("routeId", "notify");
+ route2.put("total", 50L);
+ route2.put("failed", 0L);
+ route2.put("minMs", 2.0);
+ route2.put("meanMs", 5.0);
+ route2.put("maxMs", 45.0);
+ routes.add(route2);
+ response.put("routes", routes);
+
+ JsonArray processors = new JsonArray();
+ JsonObject proc1 = new JsonObject();
+ proc1.put("processorId", "to1");
+ proc1.put("processorType", "to");
+ proc1.put("routeId", "order-in");
+ proc1.put("total", 100L);
+ proc1.put("failed", 3L);
+ proc1.put("minMs", 0.5);
+ proc1.put("meanMs", 5.2);
+ proc1.put("maxMs", 200.1);
+ processors.add(proc1);
+ response.put("processors", processors);
+
+ JsonArray endpoints = new JsonArray();
+ JsonObject ep1 = new JsonObject();
+ ep1.put("endpointUri", "kafka://orders");
+ ep1.put("total", 100L);
+ ep1.put("failed", 3L);
+ ep1.put("minMs", 0.8);
+ ep1.put("meanMs", 4.1);
+ ep1.put("maxMs", 180.5);
+ endpoints.add(ep1);
+ response.put("endpoints", endpoints);
+
+ response.put("failures", new JsonArray());
+ response.put("redeliveries", new JsonArray());
+
+ return response;
+ }
+
+ private static JsonObject statusResponse() {
+ JsonObject response = new JsonObject();
+ response.put("runtimeEvents", true);
+ JsonArray recordings = new JsonArray();
+ JsonObject rec = new JsonObject();
+ rec.put("name", "default");
+ rec.put("state", "Running");
+ recordings.add(rec);
+ response.put("recordings", recordings);
+ return response;
+ }
+
private static final class TestMonitorContext extends MonitorContext {
private final JsonObject response;
+ private JsonObject snapshotResp;
private TestMonitorContext(
AtomicReference<List<IntegrationInfo>> data,
@@ -122,8 +290,17 @@ class JfrTabRenderTest {
this.response = response;
}
+ TestMonitorContext withSnapshot(JsonObject snapshot) {
+ this.snapshotResp = snapshot;
+ return this;
+ }
+
@Override
JsonObject executeAction(String pid, JsonObject request, long
timeoutMs) {
+ String command = request.getString("command");
+ if ("snapshot".equals(command) && snapshotResp != null) {
+ return snapshotResp;
+ }
return response;
}
}