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 9e022700c9d5 CAMEL-23618: camel-tui - Add payload size metrics to
Endpoints tab
9e022700c9d5 is described below
commit 9e022700c9d50ecb1e6705cc395d32a23d0c700b
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed May 27 07:42:23 2026 +0200
CAMEL-23618: camel-tui - Add payload size metrics to Endpoints tab
Display body and header size columns in the Endpoints table when message
size tracking is enabled. Add mirrored sparkline chart showing IN vs OUT
average body sizes over time. Add per-endpoint chart selection (a key)
with three-way toggle (all/single/off). Add Reset Stats action to the
F2 menu. Rename consumer TOTAL column to POLLS for clarity.
Closes #23542
---
.../camel/cli/connector/LocalCliConnector.java | 5 +
.../main/resources/examples/message-size/README.md | 2 +-
.../dsl/jbang/core/commands/tui/ActionsPopup.java | 17 +-
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 122 ++++++-
.../dsl/jbang/core/commands/tui/ConsumersTab.java | 6 +-
.../dsl/jbang/core/commands/tui/EndpointInfo.java | 6 +
.../dsl/jbang/core/commands/tui/EndpointsTab.java | 366 ++++++++++++++++++---
.../jbang/core/commands/tui/MirroredSparkline.java | 34 +-
8 files changed, 496 insertions(+), 62 deletions(-)
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 bb5b0506492d..f32121763448 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
@@ -74,6 +74,7 @@ import org.apache.camel.spi.Resource;
import org.apache.camel.spi.ResourceLoader;
import org.apache.camel.spi.ResourceReloadStrategy;
import org.apache.camel.spi.RoutesLoader;
+import org.apache.camel.spi.RuntimeEndpointRegistry;
import org.apache.camel.spi.ShutdownPrepared;
import org.apache.camel.support.LoadOnDemandReloadStrategy;
import org.apache.camel.support.MessageHelper;
@@ -918,6 +919,10 @@ public class LocalCliConnector extends ServiceSupport
implements CliConnector, C
if (mcc != null) {
mcc.getManagedCamelContext().reset(true);
}
+ RuntimeEndpointRegistry reg =
camelContext.getRuntimeEndpointRegistry();
+ if (reg != null) {
+ reg.reset();
+ }
}
private void doActionDebugTask(JsonObject root) throws Exception {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/message-size/README.md
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/message-size/README.md
index 5332bdf435c9..49a3bdfc644c 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/message-size/README.md
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/message-size/README.md
@@ -31,7 +31,7 @@ $ camel get endpoint --verbose
To sort endpoints by body size (largest first):
```sh
-$ camel get endpoint --sort -size
+$ camel get endpoint --sort=-size
```
### How it works
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index a5050d6cc395..28c6eed536a4 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -66,7 +66,8 @@ class ActionsPopup {
private static final int ACTION_CLASSPATH = 8;
private static final int ACTION_MCP_INFO = 9;
private static final int ACTION_MCP_LOG = 10;
- private static final int ACTION_STOP_ALL = 11;
+ private static final int ACTION_RESET_STATS = 11;
+ private static final int ACTION_STOP_ALL = 12;
private final Supplier<Set<String>> runningNames;
private final Supplier<List<IntegrationInfo>> integrations;
@@ -74,6 +75,7 @@ class ActionsPopup {
private final Runnable toggleKeystrokes;
private final Supplier<Boolean> keystrokesEnabled;
private final Runnable toggleTapeRecording;
+ private Runnable resetStatsAction;
private final Supplier<Boolean> tapeRecordingActive;
private MonitorContext ctx;
private boolean mcpEnabled;
@@ -133,6 +135,10 @@ class ActionsPopup {
this.ctx = ctx;
}
+ void setResetStatsAction(Runnable resetStatsAction) {
+ this.resetStatsAction = resetStatsAction;
+ }
+
void setMcpEnabled(
boolean enabled, int port, Supplier<String> connectedClient,
Supplier<List<TuiMcpServer.LogEntry>> activityLog) {
this.mcpEnabled = enabled;
@@ -143,7 +149,7 @@ class ActionsPopup {
}
private int actionCount() {
- return mcpEnabled ? 12 : 10;
+ return mcpEnabled ? 13 : 11;
}
boolean isVisible() {
@@ -195,6 +201,7 @@ class ActionsPopup {
labels.add("Tape Recording Guide");
labels.add("Run Doctor");
labels.add("Show Classpath");
+ labels.add("Reset Stats");
if (mcpEnabled) {
labels.add("MCP Info");
labels.add("MCP Log");
@@ -348,6 +355,11 @@ class ActionsPopup {
} else if (action == ACTION_MCP_LOG) {
showActionsMenu = false;
openMcpLog();
+ } else if (action == ACTION_RESET_STATS) {
+ showActionsMenu = false;
+ if (resetStatsAction != null) {
+ resetStatsAction.run();
+ }
} else if (action == ACTION_STOP_ALL) {
showActionsMenu = false;
stopAllPopup.open();
@@ -485,6 +497,7 @@ class ActionsPopup {
items.add(ListItem.from(" 📄 Tape Recording Guide"));
items.add(ListItem.from(" 🩺 Run Doctor"));
items.add(ListItem.from(" 📦 Show Classpath"));
+ items.add(ListItem.from(" 🔄 Reset Stats"));
if (mcpEnabled) {
items.add(ListItem.from(" 🤖 MCP Info"));
items.add(ListItem.from(" 📋 MCP Log"));
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index bbe0e04ec8b8..a6d30a66f615 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -179,6 +179,17 @@ public class CamelMonitor extends CamelCommand {
private final Map<String, LinkedList<long[]>> endpointRemoteStubSamples =
new ConcurrentHashMap<>();
private final Map<String, Long> previousEndpointRemoteStubTime = new
ConcurrentHashMap<>();
+ // Endpoint payload size (mean body size) history per PID — for sparkline
+ private final Map<String, LinkedList<Long>> endpointInSizeHistory = new
ConcurrentHashMap<>();
+ private final Map<String, LinkedList<Long>> endpointOutSizeHistory = new
ConcurrentHashMap<>();
+ private final Map<String, Long> previousEndpointSizeTime = new
ConcurrentHashMap<>();
+
+ // Per-endpoint in/out rate history — keyed by pid + "|" + uri
+ private final Map<String, LinkedList<Long>> perEndpointInHistory = new
ConcurrentHashMap<>();
+ private final Map<String, LinkedList<Long>> perEndpointOutHistory = new
ConcurrentHashMap<>();
+ private final Map<String, LinkedList<long[]>> perEndpointSamples = new
ConcurrentHashMap<>();
+ private final Map<String, Long> previousPerEndpointTime = new
ConcurrentHashMap<>();
+
// Circuit breaker throughput history per PID/cbId (success + fail, one
point per second)
private final Map<String, LinkedList<Long>> cbSuccessHistory = new
ConcurrentHashMap<>();
private final Map<String, LinkedList<Long>> cbFailHistory = new
ConcurrentHashMap<>();
@@ -285,6 +296,7 @@ public class CamelMonitor extends CamelCommand {
// Create shared context and tab instances
ctx = new MonitorContext(data, infraData);
actionsPopup.setContext(ctx);
+ actionsPopup.setResetStatsAction(this::resetStats);
logTab = new LogTab(ctx);
routesTab = new RoutesTab(ctx);
consumersTab = new ConsumersTab(ctx);
@@ -292,7 +304,9 @@ public class CamelMonitor extends CamelCommand {
ctx,
endpointInHistory, endpointOutHistory,
endpointRemoteInHistory, endpointRemoteOutHistory,
- endpointRemoteStubInHistory, endpointRemoteStubOutHistory);
+ endpointRemoteStubInHistory, endpointRemoteStubOutHistory,
+ endpointInSizeHistory, endpointOutSizeHistory,
+ perEndpointInHistory, perEndpointOutHistory);
httpTab = new HttpTab(ctx);
healthTab = new HealthTab(ctx);
historyTab = new HistoryTab(ctx, traces, traceFilePositions);
@@ -1610,6 +1624,44 @@ public class CamelMonitor extends CamelCommand {
}
}
+ private void resetStats() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null) {
+ return;
+ }
+ String pid = info.pid;
+ JsonObject root = new JsonObject();
+ root.put("action", "reset-stats");
+ Path actionFile = ctx.getActionFile(pid);
+ PathUtils.writeTextSafely(root.toJson(), actionFile);
+ // Clear local sparkline history — overview
+ throughputHistory.remove(pid);
+ failedHistory.remove(pid);
+ throughputSamples.remove(pid);
+ previousExchangesTime.remove(pid);
+ // Clear local sparkline history — endpoints
+ endpointInHistory.remove(pid);
+ endpointOutHistory.remove(pid);
+ endpointSamples.remove(pid);
+ previousEndpointTime.remove(pid);
+ endpointRemoteInHistory.remove(pid);
+ endpointRemoteOutHistory.remove(pid);
+ endpointRemoteSamples.remove(pid);
+ previousEndpointRemoteTime.remove(pid);
+ endpointRemoteStubInHistory.remove(pid);
+ endpointRemoteStubOutHistory.remove(pid);
+ endpointRemoteStubSamples.remove(pid);
+ previousEndpointRemoteStubTime.remove(pid);
+ endpointInSizeHistory.remove(pid);
+ endpointOutSizeHistory.remove(pid);
+ previousEndpointSizeTime.remove(pid);
+ String perEpPrefix = pid + "|";
+ perEndpointInHistory.keySet().removeIf(k -> k.startsWith(perEpPrefix));
+ perEndpointOutHistory.keySet().removeIf(k ->
k.startsWith(perEpPrefix));
+ perEndpointSamples.keySet().removeIf(k -> k.startsWith(perEpPrefix));
+ previousPerEndpointTime.keySet().removeIf(k ->
k.startsWith(perEpPrefix));
+ }
+
private void sendRouteCommand(String pid, String routeId, String command) {
JsonObject root = new JsonObject();
root.put("action", "route");
@@ -1840,14 +1892,23 @@ public class CamelMonitor extends CamelCommand {
endpointRemoteStubInHistory.remove(entry.getKey());
endpointRemoteStubOutHistory.remove(entry.getKey());
endpointRemoteStubSamples.remove(entry.getKey());
+
+ endpointInSizeHistory.remove(entry.getKey());
+ endpointOutSizeHistory.remove(entry.getKey());
+ previousEndpointSizeTime.remove(entry.getKey());
previousEndpointRemoteStubTime.remove(entry.getKey());
cpuLoadAvg.remove(entry.getKey());
prevCpuSample.remove(entry.getKey());
- String vanishPid = entry.getKey() + "/";
- cbSuccessHistory.keySet().removeIf(k ->
k.startsWith(vanishPid));
- cbFailHistory.keySet().removeIf(k ->
k.startsWith(vanishPid));
- cbThroughputSamples.keySet().removeIf(k ->
k.startsWith(vanishPid));
- previousCbTime.keySet().removeIf(k ->
k.startsWith(vanishPid));
+ String vanishCbPrefix = entry.getKey() + "/";
+ cbSuccessHistory.keySet().removeIf(k ->
k.startsWith(vanishCbPrefix));
+ cbFailHistory.keySet().removeIf(k ->
k.startsWith(vanishCbPrefix));
+ cbThroughputSamples.keySet().removeIf(k ->
k.startsWith(vanishCbPrefix));
+ previousCbTime.keySet().removeIf(k ->
k.startsWith(vanishCbPrefix));
+ String vanishEpPrefix = entry.getKey() + "|";
+ perEndpointInHistory.keySet().removeIf(k ->
k.startsWith(vanishEpPrefix));
+ perEndpointOutHistory.keySet().removeIf(k ->
k.startsWith(vanishEpPrefix));
+ perEndpointSamples.keySet().removeIf(k ->
k.startsWith(vanishEpPrefix));
+ previousPerEndpointTime.keySet().removeIf(k ->
k.startsWith(vanishEpPrefix));
} else if (!livePids.contains(entry.getKey())) {
IntegrationInfo ghost = entry.getValue().info;
ghost.vanishing = true;
@@ -2097,6 +2158,49 @@ public class CamelMonitor extends CamelCommand {
recordEndpointSample(pid, now, inRemoteStub, outRemoteStub,
endpointRemoteStubSamples, previousEndpointRemoteStubTime,
endpointRemoteStubInHistory, endpointRemoteStubOutHistory);
+
+ // Record payload size snapshots (mean body size per direction)
+ long inMeanSize = info.endpoints.stream()
+ .filter(ep -> "in".equals(ep.direction) && ep.meanBodySize >=
0)
+ .mapToLong(ep -> ep.meanBodySize).max().orElse(0);
+ long outMeanSize = info.endpoints.stream()
+ .filter(ep -> "out".equals(ep.direction) && ep.meanBodySize >=
0)
+ .mapToLong(ep -> ep.meanBodySize).max().orElse(0);
+ Long lastSizeTime = previousEndpointSizeTime.get(pid);
+ if (lastSizeTime == null || now - lastSizeTime >= 1000) {
+ previousEndpointSizeTime.put(pid, now);
+ LinkedList<Long> inSizeHist =
endpointInSizeHistory.computeIfAbsent(pid, k -> new LinkedList<>());
+ inSizeHist.add(inMeanSize);
+ while (inSizeHist.size() > MAX_ENDPOINT_CHART_POINTS) {
+ inSizeHist.remove(0);
+ }
+ LinkedList<Long> outSizeHist =
endpointOutSizeHistory.computeIfAbsent(pid, k -> new LinkedList<>());
+ outSizeHist.add(outMeanSize);
+ while (outSizeHist.size() > MAX_ENDPOINT_CHART_POINTS) {
+ outSizeHist.remove(0);
+ }
+ }
+
+ // Per-endpoint rate history (keyed by pid|uri)
+ Map<String, long[]> perUri = new LinkedHashMap<>();
+ for (EndpointInfo ep : info.endpoints) {
+ if (ep.uri == null) {
+ continue;
+ }
+ long[] inOut = perUri.computeIfAbsent(ep.uri, k -> new long[2]);
+ if ("in".equals(ep.direction)) {
+ inOut[0] += ep.hits;
+ } else if ("out".equals(ep.direction)) {
+ inOut[1] += ep.hits;
+ }
+ }
+ for (Map.Entry<String, long[]> entry : perUri.entrySet()) {
+ String key = pid + "|" + entry.getKey();
+ long[] inOut = entry.getValue();
+ recordEndpointSample(key, now, inOut[0], inOut[1],
+ perEndpointSamples, previousPerEndpointTime,
+ perEndpointInHistory, perEndpointOutHistory);
+ }
}
private void recordEndpointSample(
@@ -2844,6 +2948,12 @@ public class CamelMonitor extends CamelCommand {
ep.hits = TuiHelper.objToLong(ej.get("hits"));
ep.stub = Boolean.TRUE.equals(ej.get("stub"));
ep.remote = !Boolean.FALSE.equals(ej.get("remote"));
+ ep.minBodySize =
TuiHelper.objToLong(ej.get("minBodySize"));
+ ep.maxBodySize =
TuiHelper.objToLong(ej.get("maxBodySize"));
+ ep.meanBodySize =
TuiHelper.objToLong(ej.get("meanBodySize"));
+ ep.minHeadersSize =
TuiHelper.objToLong(ej.get("minHeadersSize"));
+ ep.maxHeadersSize =
TuiHelper.objToLong(ej.get("maxHeadersSize"));
+ ep.meanHeadersSize =
TuiHelper.objToLong(ej.get("meanHeadersSize"));
// Extract component from URI (e.g., "timer://tick" ->
"timer")
if (ep.uri != null) {
int idx = ep.uri.indexOf(':');
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
index 193010821a16..fc2ea985d452 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
@@ -37,7 +37,7 @@ import static
org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
class ConsumersTab implements MonitorTab {
- private static final String[] SORT_COLUMNS = { "id", "status", "type",
"inflight", "total", "uri" };
+ private static final String[] SORT_COLUMNS = { "id", "status", "type",
"inflight", "polls", "uri" };
private final MonitorContext ctx;
private final TableState tableState = new TableState();
@@ -131,7 +131,7 @@ class ConsumersTab implements MonitorTab {
Cell.from(Span.styled(sortLabel("STATUS", "status"),
sortStyle("status"))),
Cell.from(Span.styled(sortLabel("TYPE", "type"),
sortStyle("type"))),
rightCell(sortLabel("INFLIGHT", "inflight"), 8,
sortStyle("inflight")),
- rightCell(sortLabel("TOTAL", "total"), 8,
sortStyle("total")),
+ rightCell(sortLabel("POLLS", "polls"), 8,
sortStyle("polls")),
rightCell("PERIOD", 10, Style.EMPTY.bold()),
Cell.from(Span.styled("SINCE-LAST",
Style.EMPTY.bold())),
Cell.from(Span.styled(sortLabel("URI", "uri"),
sortStyle("uri")))))
@@ -179,7 +179,7 @@ class ConsumersTab implements MonitorTab {
yield ta.compareToIgnoreCase(tb);
}
case "inflight" -> Integer.compare(b.inflight, a.inflight);
- case "total" -> {
+ case "polls" -> {
long la = a.totalCounter != null ? a.totalCounter : 0;
long lb = b.totalCounter != null ? b.totalCounter : 0;
yield Long.compare(lb, la);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointInfo.java
index 5f7896933468..21bef41c3e1c 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointInfo.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointInfo.java
@@ -24,4 +24,10 @@ class EndpointInfo {
long hits;
boolean stub;
boolean remote;
+ long minBodySize = -1;
+ long maxBodySize = -1;
+ long meanBodySize = -1;
+ long minHeadersSize = -1;
+ long maxHeadersSize = -1;
+ long meanHeadersSize = -1;
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
index acbb2d2ea975..5a7ca27709b8 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import dev.tamboui.layout.Constraint;
@@ -45,8 +46,11 @@ import static
org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
class EndpointsTab implements MonitorTab {
- private static final String[] SORT_COLUMNS = { "component", "route",
"dir", "total", "uri" };
+ private static final String[] SORT_COLUMNS = { "component", "route",
"dir", "total", "body", "hdr", "uri" };
private static final int MAX_CHART_POINTS = 60;
+ private static final int CHART_ALL = 0;
+ private static final int CHART_SINGLE = 1;
+ private static final int CHART_OFF = 2;
private final MonitorContext ctx;
private final TableState tableState = new TableState();
@@ -56,12 +60,16 @@ class EndpointsTab implements MonitorTab {
private final Map<String, LinkedList<Long>> endpointRemoteOutHistory;
private final Map<String, LinkedList<Long>> endpointRemoteStubInHistory;
private final Map<String, LinkedList<Long>> endpointRemoteStubOutHistory;
+ private final Map<String, LinkedList<Long>> endpointInSizeHistory;
+ private final Map<String, LinkedList<Long>> endpointOutSizeHistory;
+ private final Map<String, LinkedList<Long>> perEndpointInHistory;
+ private final Map<String, LinkedList<Long>> perEndpointOutHistory;
private String sort = "route";
private int sortIndex = 1;
private boolean sortReversed;
private int filter;
- private boolean showChart = true;
+ private int chartMode = CHART_ALL;
EndpointsTab(MonitorContext ctx,
Map<String, LinkedList<Long>> endpointInHistory,
@@ -69,7 +77,11 @@ class EndpointsTab implements MonitorTab {
Map<String, LinkedList<Long>> endpointRemoteInHistory,
Map<String, LinkedList<Long>> endpointRemoteOutHistory,
Map<String, LinkedList<Long>> endpointRemoteStubInHistory,
- Map<String, LinkedList<Long>> endpointRemoteStubOutHistory) {
+ Map<String, LinkedList<Long>> endpointRemoteStubOutHistory,
+ Map<String, LinkedList<Long>> endpointInSizeHistory,
+ Map<String, LinkedList<Long>> endpointOutSizeHistory,
+ Map<String, LinkedList<Long>> perEndpointInHistory,
+ Map<String, LinkedList<Long>> perEndpointOutHistory) {
this.ctx = ctx;
this.endpointInHistory = endpointInHistory;
this.endpointOutHistory = endpointOutHistory;
@@ -77,6 +89,10 @@ class EndpointsTab implements MonitorTab {
this.endpointRemoteOutHistory = endpointRemoteOutHistory;
this.endpointRemoteStubInHistory = endpointRemoteStubInHistory;
this.endpointRemoteStubOutHistory = endpointRemoteStubOutHistory;
+ this.endpointInSizeHistory = endpointInSizeHistory;
+ this.endpointOutSizeHistory = endpointOutSizeHistory;
+ this.perEndpointInHistory = perEndpointInHistory;
+ this.perEndpointOutHistory = perEndpointOutHistory;
}
@Override
@@ -96,7 +112,7 @@ class EndpointsTab implements MonitorTab {
return true;
}
if (ke.isCharIgnoreCase('a')) {
- showChart = !showChart;
+ chartMode = (chartMode + 1) % 3;
return true;
}
return false;
@@ -109,10 +125,21 @@ class EndpointsTab implements MonitorTab {
@Override
public void navigateUp() {
+ tableState.selectPrevious();
}
@Override
public void navigateDown() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info != null) {
+ List<EndpointInfo> filtered = new ArrayList<>(info.endpoints);
+ if (filter == 1) {
+ filtered.removeIf(ep -> !ep.remote);
+ } else if (filter == 2) {
+ filtered.removeIf(ep -> !ep.remote && !ep.stub);
+ }
+ tableState.selectNext(filtered.size());
+ }
}
@Override
@@ -131,6 +158,9 @@ class EndpointsTab implements MonitorTab {
}
sortedEndpoints.sort(this::sortEndpoint);
+ boolean hasSize = info.endpoints.stream()
+ .anyMatch(ep -> ep.meanBodySize >= 0 || ep.meanHeadersSize >=
0);
+
List<Row> rows = new ArrayList<>();
for (EndpointInfo ep : sortedEndpoints) {
String dir = ep.direction != null ? ep.direction : "";
@@ -145,45 +175,63 @@ class EndpointsTab implements MonitorTab {
default -> "↔ ";
};
- rows.add(Row.from(
- Cell.from(Span.styled(ep.component != null ? ep.component
: "", Style.EMPTY.fg(Color.CYAN))),
- Cell.from(ep.routeId != null ? ep.routeId : ""),
- Cell.from(Span.styled(arrow + dir, dirStyle)),
- rightCell(ep.hits > 0 ? String.valueOf(ep.hits) : "", 8),
- centerCell(ep.stub ? "x" : "", 6),
- centerCell(ep.remote ? "x" : "", 8),
- Cell.from(ep.uri != null ? ep.uri : "")));
+ List<Cell> cells = new ArrayList<>();
+ cells.add(Cell.from(Span.styled(ep.component != null ?
ep.component : "", Style.EMPTY.fg(Color.CYAN))));
+ cells.add(Cell.from(ep.routeId != null ? ep.routeId : ""));
+ cells.add(Cell.from(Span.styled(arrow + dir, dirStyle)));
+ cells.add(rightCell(ep.hits > 0 ? String.valueOf(ep.hits) : "",
8));
+ if (hasSize) {
+ cells.add(rightCell(sizeToString(ep.meanBodySize), 10));
+ cells.add(rightCell(sizeToString(ep.meanHeadersSize), 10));
+ }
+ cells.add(centerCell(ep.stub ? "x" : "", 6));
+ cells.add(centerCell(ep.remote ? "x" : "", 8));
+ cells.add(Cell.from(ep.uri != null ? ep.uri : ""));
+ rows.add(Row.from(cells));
}
+ int emptyCols = hasSize ? 9 : 7;
if (rows.isEmpty()) {
- rows.add(Row.from(
- Cell.from(Span.styled("No endpoints", Style.EMPTY.dim())),
- Cell.from(""),
- Cell.from(""),
- Cell.from(""),
- Cell.from(""),
- Cell.from(""),
- Cell.from("")));
+ List<Cell> emptyCells = new ArrayList<>();
+ emptyCells.add(Cell.from(Span.styled("No endpoints",
Style.EMPTY.dim())));
+ for (int i = 1; i < emptyCols; i++) {
+ emptyCells.add(Cell.from(""));
+ }
+ rows.add(Row.from(emptyCells));
+ }
+
+ List<Cell> headerCells = new ArrayList<>();
+ headerCells.add(Cell.from(Span.styled(sortLabel("COMPONENT",
"component"), sortStyle("component"))));
+ headerCells.add(Cell.from(Span.styled(sortLabel("ROUTE", "route"),
sortStyle("route"))));
+ headerCells.add(Cell.from(Span.styled(sortLabel("DIR", "dir"),
sortStyle("dir"))));
+ headerCells.add(rightCell(sortLabel("TOTAL", "total"), 8,
sortStyle("total")));
+ if (hasSize) {
+ headerCells.add(rightCell(sortLabel("BODY", "body"), 10,
sortStyle("body")));
+ headerCells.add(rightCell(sortLabel("HDR", "hdr"), 10,
sortStyle("hdr")));
+ }
+ headerCells.add(centerCell("STUB", 6, Style.EMPTY.bold()));
+ headerCells.add(centerCell("REMOTE", 8, Style.EMPTY.bold()));
+ headerCells.add(Cell.from(Span.styled(sortLabel("URI", "uri"),
sortStyle("uri"))));
+
+ List<Constraint> widths = new ArrayList<>();
+ widths.add(Constraint.length(15));
+ widths.add(Constraint.length(20));
+ widths.add(Constraint.length(8));
+ widths.add(Constraint.length(8));
+ if (hasSize) {
+ widths.add(Constraint.length(10));
+ widths.add(Constraint.length(10));
}
+ widths.add(Constraint.length(6));
+ widths.add(Constraint.length(8));
+ widths.add(Constraint.fill());
Table table = Table.builder()
.rows(rows)
- .header(Row.from(
- Cell.from(Span.styled(sortLabel("COMPONENT",
"component"), sortStyle("component"))),
- Cell.from(Span.styled(sortLabel("ROUTE", "route"),
sortStyle("route"))),
- Cell.from(Span.styled(sortLabel("DIR", "dir"),
sortStyle("dir"))),
- rightCell(sortLabel("TOTAL", "total"), 8,
sortStyle("total")),
- centerCell("STUB", 6, Style.EMPTY.bold()),
- centerCell("REMOTE", 8, Style.EMPTY.bold()),
- Cell.from(Span.styled(sortLabel("URI", "uri"),
sortStyle("uri")))))
- .widths(
- Constraint.length(15),
- Constraint.length(20),
- Constraint.length(8),
- Constraint.length(8),
- Constraint.length(6),
- Constraint.length(8),
- Constraint.fill())
+ .header(Row.from(headerCells))
+ .widths(widths.toArray(Constraint[]::new))
+ .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
+ .highlightSpacing(Table.HighlightSpacing.ALWAYS)
.block(Block.builder().borderType(BorderType.ROUNDED)
.title(" Endpoints sort:" + sort
+ (filter == 1 ? " filter:remote" : filter == 2
? " filter:remote+stub" : "")
@@ -191,6 +239,10 @@ class EndpointsTab implements MonitorTab {
.build())
.build();
+ boolean hasSizeHistory = !endpointInSizeHistory.isEmpty()
+ && endpointInSizeHistory.values().stream().anyMatch(h ->
h.stream().anyMatch(v -> v > 0));
+
+ boolean showChart = chartMode != CHART_OFF;
List<Rect> chunks = showChart
? Layout.vertical().constraints(Constraint.fill(),
Constraint.length(16)).split(area)
: List.of(area);
@@ -198,25 +250,53 @@ class EndpointsTab implements MonitorTab {
frame.renderStatefulWidget(table, chunks.get(0), tableState);
if (showChart) {
- long inTotal = info.endpoints.stream()
- .filter(ep -> "in".equals(ep.direction) &&
matchesFilter(ep))
- .mapToLong(ep -> ep.hits)
- .sum();
- long outTotal = info.endpoints.stream()
- .filter(ep -> "out".equals(ep.direction) &&
matchesFilter(ep))
- .mapToLong(ep -> ep.hits)
- .sum();
- renderEndpointFlow(frame, chunks.get(1), inTotal, outTotal,
info.name, info.pid);
+ // Determine selected endpoint URI for single-endpoint chart
+ String selectedUri = null;
+ if (chartMode == CHART_SINGLE) {
+ Integer sel = tableState.selected();
+ if (sel != null && sel >= 0 && sel < sortedEndpoints.size()) {
+ selectedUri = sortedEndpoints.get(sel).uri;
+ }
+ }
+
+ if (chartMode == CHART_SINGLE && selectedUri != null) {
+ renderSingleEndpointChart(frame, chunks.get(1), selectedUri,
info);
+ } else {
+ long inTotal = info.endpoints.stream()
+ .filter(ep -> "in".equals(ep.direction) &&
matchesFilter(ep))
+ .mapToLong(ep -> ep.hits)
+ .sum();
+ long outTotal = info.endpoints.stream()
+ .filter(ep -> "out".equals(ep.direction) &&
matchesFilter(ep))
+ .mapToLong(ep -> ep.hits)
+ .sum();
+
+ if (hasSizeHistory) {
+ List<Rect> chartSplit = Layout.horizontal()
+ .constraints(Constraint.percentage(50),
Constraint.percentage(50))
+ .split(chunks.get(1));
+ renderEndpointFlow(frame, chartSplit.get(0), inTotal,
outTotal, info.name, info.pid);
+ renderPayloadSizeChart(frame, chartSplit.get(1), info.pid);
+ } else {
+ renderEndpointFlow(frame, chunks.get(1), inTotal,
outTotal, info.name, info.pid);
+ }
+ }
}
}
@Override
public void renderFooter(List<Span> spans) {
hint(spans, "Esc", "back");
+ hint(spans, "↑↓", "navigate");
hint(spans, "s", "sort");
String[] filterLabels = { "all", "remote", "remote+stub" };
hint(spans, "f", "filter [" + filterLabels[filter] + "]");
- hint(spans, "a", "chart " + (showChart ? "[all]" : "[off]"));
+ String chartLabel = switch (chartMode) {
+ case CHART_ALL -> "[all]";
+ case CHART_SINGLE -> "[single]";
+ default -> "[off]";
+ };
+ hint(spans, "a", "chart " + chartLabel);
hint(spans, "1-9", "tabs");
}
@@ -253,6 +333,8 @@ class EndpointsTab implements MonitorTab {
yield da.compareToIgnoreCase(db);
}
case "total" -> Long.compare(b.hits, a.hits);
+ case "body" -> Long.compare(b.meanBodySize, a.meanBodySize);
+ case "hdr" -> Long.compare(b.meanHeadersSize, a.meanHeadersSize);
case "uri" -> {
String ua = a.uri != null ? a.uri : "";
String ub = b.uri != null ? b.uri : "";
@@ -264,9 +346,22 @@ class EndpointsTab implements MonitorTab {
yield ra.compareToIgnoreCase(rb);
}
};
+ if (result == 0) {
+ result = directionOrder(a.direction) - directionOrder(b.direction);
+ }
return sortReversed ? -result : result;
}
+ private static int directionOrder(String direction) {
+ if ("in".equals(direction)) {
+ return 0;
+ }
+ if ("out".equals(direction)) {
+ return 1;
+ }
+ return 2;
+ }
+
private void renderEndpointFlow(
Frame frame, Rect area, long inTotal, long outTotal, String name,
String pid) {
List<Rect> hSplit = Layout.horizontal()
@@ -373,6 +468,185 @@ class EndpointsTab implements MonitorTab {
.build(), rightArea);
}
+ private void renderSingleEndpointChart(Frame frame, Rect area, String
selectedUri, IntegrationInfo info) {
+ long inTotal = info.endpoints.stream()
+ .filter(ep -> "in".equals(ep.direction) &&
selectedUri.equals(ep.uri))
+ .mapToLong(ep -> ep.hits)
+ .sum();
+ long outTotal = info.endpoints.stream()
+ .filter(ep -> "out".equals(ep.direction) &&
selectedUri.equals(ep.uri))
+ .mapToLong(ep -> ep.hits)
+ .sum();
+
+ List<Rect> hSplit = Layout.horizontal()
+ .constraints(Constraint.length(38), Constraint.fill())
+ .split(area);
+
+ // Flow diagram with endpoint URI as label
+ int w = Math.max(10, hSplit.get(0).width() - 2);
+ String label = selectedUri;
+ if (CharWidth.of(label) > 20) {
+ label = CharWidth.truncateWithEllipsis(label, 20,
CharWidth.TruncatePosition.END);
+ }
+ String box = "[ " + label + " ]";
+ int boxLen = CharWidth.of(box);
+ int sideLen = Math.max(4, (w - boxLen - 2) / 2);
+ String arm = "─".repeat(Math.max(1, sideLen - 1));
+ String arrowStr = arm + "â–º";
+ String inStr = String.valueOf(inTotal);
+ String outStr = String.valueOf(outTotal);
+ int inPad = Math.max(0, sideLen - inStr.length());
+ int centerGap = boxLen + 2;
+ int outPad = Math.max(0, sideLen - outStr.length());
+ int inLabelPad = (sideLen - 2) / 2;
+ int outLabelPad = (sideLen - 3) / 2;
+ String inLabelStr = " ".repeat(inLabelPad) + "in" + " ".repeat(sideLen
- inLabelPad - 2);
+ String outLabelStr = " ".repeat(outLabelPad) + "out";
+
+ Style inStyle = Style.EMPTY.fg(Color.ansi(AnsiColor.BRIGHT_GREEN));
+ Style outStyle = Style.EMPTY.fg(Color.CYAN);
+ Style dimStyle = Style.EMPTY.dim();
+
+ List<Line> flowLines = new ArrayList<>();
+ flowLines.add(Line.from(Span.raw("")));
+ flowLines.add(Line.from(Span.raw("")));
+ flowLines.add(Line.from(
+ Span.styled(" ".repeat(inPad) + inStr, inTotal > 0 ? inStyle :
dimStyle),
+ Span.raw(" ".repeat(centerGap)),
+ Span.styled(outStr + " ".repeat(outPad), outTotal > 0 ?
outStyle : dimStyle)));
+ flowLines.add(Line.from(
+ Span.styled(arrowStr, inStyle),
+ Span.raw(" "),
+ Span.styled(box, Style.EMPTY.fg(Color.YELLOW).bold()),
+ Span.raw(" "),
+ Span.styled(arrowStr, outStyle)));
+ flowLines.add(Line.from(
+ Span.styled(inLabelStr, inStyle.dim()),
+ Span.raw(" ".repeat(centerGap)),
+ Span.styled(outLabelStr, outStyle.dim())));
+
+ frame.renderWidget(Paragraph.builder()
+ .text(dev.tamboui.text.Text.from(flowLines))
+ .block(Block.builder().borderType(BorderType.ROUNDED).title("
Flow ").build())
+ .build(), hSplit.get(0));
+
+ // Per-endpoint sparkline
+ String key = info.pid + "|" + selectedUri;
+ LinkedList<Long> inHist = perEndpointInHistory.getOrDefault(key, new
LinkedList<>());
+ LinkedList<Long> outHist = perEndpointOutHistory.getOrDefault(key, new
LinkedList<>());
+
+ int renderPoints = MAX_CHART_POINTS;
+ long[] inArr = new long[renderPoints];
+ long[] outArr = new long[renderPoints];
+ for (int i = 0; i < renderPoints; i++) {
+ int idx = inHist.size() - renderPoints + i;
+ if (idx >= 0) {
+ inArr[i] = inHist.get(idx);
+ }
+ idx = outHist.size() - renderPoints + i;
+ if (idx >= 0) {
+ outArr[i] = outHist.get(idx);
+ }
+ }
+ long curIn = inArr[renderPoints - 1];
+ long curOut = outArr[renderPoints - 1];
+
+ String uriLabel = selectedUri;
+ if (CharWidth.of(uriLabel) > 30) {
+ uriLabel = CharWidth.truncateWithEllipsis(uriLabel, 30,
CharWidth.TruncatePosition.END);
+ }
+
+ Line chartTitle = Line.from(
+ Span.raw(" ["),
+ Span.styled(uriLabel, Style.EMPTY.fg(Color.YELLOW)),
+ Span.raw("] "),
+ Span.styled("â–¬",
Style.EMPTY.fg(Color.ansi(AnsiColor.BRIGHT_GREEN))),
+ Span.raw(String.format(" in:%-4d ", curIn)),
+ Span.styled("â–¬", Style.EMPTY.fg(Color.CYAN)),
+ Span.raw(String.format(" out:%-4d msg/s", curOut)));
+
+ frame.renderWidget(MirroredSparkline.builder()
+ .topData(inArr)
+ .bottomData(outArr)
+ .topStyle(Style.EMPTY.fg(Color.ansi(AnsiColor.BRIGHT_GREEN)))
+ .bottomStyle(Style.EMPTY.fg(Color.CYAN))
+ .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 /
4) + "s",
+ "-" + (renderPoints / 2) + "s", "-" + (renderPoints /
4) + "s", "now")
+ .block(Block.builder().borderType(BorderType.ROUNDED)
+ .title(Title.from(chartTitle)).build())
+ .build(), hSplit.get(1));
+ }
+
+ private void renderPayloadSizeChart(Frame frame, Rect area, String pid) {
+ LinkedList<Long> inHist = endpointInSizeHistory.getOrDefault(pid, new
LinkedList<>());
+ LinkedList<Long> outHist = endpointOutSizeHistory.getOrDefault(pid,
new LinkedList<>());
+
+ int renderPoints = MAX_CHART_POINTS;
+ long[] inArr = new long[renderPoints];
+ long[] outArr = new long[renderPoints];
+ for (int i = 0; i < renderPoints; i++) {
+ int idx = inHist.size() - renderPoints + i;
+ if (idx >= 0) {
+ inArr[i] = inHist.get(idx);
+ }
+ idx = outHist.size() - renderPoints + i;
+ if (idx >= 0) {
+ outArr[i] = outHist.get(idx);
+ }
+ }
+ long curIn = inArr[renderPoints - 1];
+ long curOut = outArr[renderPoints - 1];
+
+ Line chartTitle = Line.from(
+ Span.styled("â–¬", Style.EMPTY.fg(Color.YELLOW)),
+ Span.raw(String.format(" in:%-8s ", sizeToString(curIn))),
+ Span.styled("â–¬", Style.EMPTY.fg(Color.MAGENTA)),
+ Span.raw(String.format(" out:%-8s avg body",
sizeToString(curOut))));
+
+ frame.renderWidget(MirroredSparkline.builder()
+ .topData(inArr)
+ .bottomData(outArr)
+ .topStyle(Style.EMPTY.fg(Color.YELLOW))
+ .bottomStyle(Style.EMPTY.fg(Color.MAGENTA))
+ .yLabelFormatter(EndpointsTab::sizeToYLabel)
+ .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 /
4) + "s",
+ "-" + (renderPoints / 2) + "s", "-" + (renderPoints /
4) + "s", "now")
+ .block(Block.builder().borderType(BorderType.ROUNDED)
+ .title(Title.from(chartTitle)).build())
+ .build(), area);
+ }
+
+ private static String sizeToYLabel(long size) {
+ if (size <= 0) {
+ return "0 B";
+ }
+ if (size < 1024) {
+ return size + "B";
+ } else if (size < 1024 * 1024) {
+ long kb = size / 1024;
+ return kb + "KB";
+ } else {
+ long mb = size / (1024 * 1024);
+ return mb + "MB";
+ }
+ }
+
+ static String sizeToString(long size) {
+ if (size < 0) {
+ return "-";
+ }
+ if (size == 0) {
+ return "0 B";
+ }
+ if (size < 1024) {
+ return size + " B";
+ } else if (size < 1024 * 1024) {
+ return String.format(Locale.US, "%.1f KB", size / 1024.0);
+ } else {
+ return String.format(Locale.US, "%.1f MB", size / (1024.0 *
1024.0));
+ }
+ }
+
@Override
public SelectionContext getSelectionContext() {
IntegrationInfo info = ctx.findSelectedIntegration();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MirroredSparkline.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MirroredSparkline.java
index 5f60132fd390..30b8ad52e573 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MirroredSparkline.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MirroredSparkline.java
@@ -17,6 +17,7 @@
package org.apache.camel.dsl.jbang.core.commands.tui;
import java.util.List;
+import java.util.function.LongFunction;
import dev.tamboui.buffer.Buffer;
import dev.tamboui.layout.Rect;
@@ -107,6 +108,7 @@ public final class MirroredSparkline implements Widget {
private final Sparkline.BarSet barSet;
private final boolean showYAxis;
private final String[] xLabels;
+ private final LongFunction<String> yLabelFormatter;
private MirroredSparkline(Builder builder) {
this.topData = builder.topData;
@@ -118,6 +120,7 @@ public final class MirroredSparkline implements Widget {
this.barSet = builder.barSet;
this.showYAxis = builder.showYAxis;
this.xLabels = builder.xLabels;
+ this.yLabelFormatter = builder.yLabelFormatter;
}
/**
@@ -168,12 +171,10 @@ public final class MirroredSparkline implements Widget {
if (showYAxis) {
String label;
- if (r == 0) {
- label = effectiveMax > 9999 ? "999+" :
String.format("%4d", effectiveMax);
+ if (r == 0 || r == chartBodyRows - 1) {
+ label = formatYLabel(effectiveMax);
} else if (r == centerRow) {
label = " 0";
- } else if (r == chartBodyRows - 1) {
- label = effectiveMax > 9999 ? "999+" :
String.format("%4d", effectiveMax);
} else {
label = " ";
}
@@ -251,6 +252,17 @@ public final class MirroredSparkline implements Widget {
}
}
+ private String formatYLabel(long value) {
+ if (yLabelFormatter != null) {
+ String s = yLabelFormatter.apply(value);
+ if (s.length() >= Y_LABEL_WIDTH) {
+ return s.substring(0, Y_LABEL_WIDTH);
+ }
+ return " ".repeat(Y_LABEL_WIDTH - s.length()) + s;
+ }
+ return value > 9999 ? "999+" : String.format("%4d", value);
+ }
+
private long computeMax() {
if (max != null) {
return Math.max(1, max);
@@ -278,6 +290,7 @@ public final class MirroredSparkline implements Widget {
private Sparkline.BarSet barSet = Sparkline.BarSet.NINE_LEVELS;
private boolean showYAxis = true;
private String[] xLabels;
+ private LongFunction<String> yLabelFormatter;
private Builder() {
}
@@ -418,6 +431,19 @@ public final class MirroredSparkline implements Widget {
return this;
}
+ /**
+ * Sets a custom formatter for Y-axis max labels. The function
receives the max value and should return a short
+ * string (up to 4 chars). When not set, values are formatted as
integers with {@code 999+} for values above
+ * 9999.
+ *
+ * @param formatter the formatter function
+ * @return this builder
+ */
+ public Builder yLabelFormatter(LongFunction<String> formatter) {
+ this.yLabelFormatter = formatter;
+ return this;
+ }
+
/**
* Builds the widget.
*