This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-23618 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 86a068a2acd1797a1842e7e03c9ebe72ae502032 Author: Claus Ibsen <[email protected]> AuthorDate: Tue May 26 21:45:05 2026 +0200 CAMEL-23618: camel-tui - Add payload size metrics to Endpoints tab Display body and header size columns (min/max/mean) in the Endpoints table when message size tracking is enabled. Add a mirrored sparkline chart showing IN vs OUT average body sizes over time. Add Reset Stats action to the F2 menu that resets route counters, endpoint registry stats, and all local sparkline history. Rename consumer TOTAL column to POLLS to clarify it tracks scheduler poll count, not exchange totals. Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../camel/cli/connector/LocalCliConnector.java | 5 + .../dsl/jbang/core/commands/tui/ActionsPopup.java | 17 +- .../dsl/jbang/core/commands/tui/CamelMonitor.java | 74 ++++++++- .../dsl/jbang/core/commands/tui/ConsumersTab.java | 6 +- .../dsl/jbang/core/commands/tui/EndpointInfo.java | 6 + .../dsl/jbang/core/commands/tui/EndpointsTab.java | 180 +++++++++++++++++---- .../jbang/core/commands/tui/MirroredSparkline.java | 34 +++- 7 files changed, 277 insertions(+), 45 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-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..99df0ca90dc7 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,11 @@ 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<>(); + // 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 +290,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 +298,8 @@ public class CamelMonitor extends CamelCommand { ctx, endpointInHistory, endpointOutHistory, endpointRemoteInHistory, endpointRemoteOutHistory, - endpointRemoteStubInHistory, endpointRemoteStubOutHistory); + endpointRemoteStubInHistory, endpointRemoteStubOutHistory, + endpointInSizeHistory, endpointOutSizeHistory); httpTab = new HttpTab(ctx); healthTab = new HealthTab(ctx); historyTab = new HistoryTab(ctx, traces, traceFilePositions); @@ -1610,6 +1617,39 @@ 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); + } + private void sendRouteCommand(String pid, String routeId, String command) { JsonObject root = new JsonObject(); root.put("action", "route"); @@ -1840,6 +1880,10 @@ 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()); @@ -2097,6 +2141,28 @@ 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); + } + } } private void recordEndpointSample( @@ -2844,6 +2910,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..4c5baa4909c3 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,7 +46,7 @@ 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 final MonitorContext ctx; @@ -56,6 +57,8 @@ 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 String sort = "route"; private int sortIndex = 1; @@ -69,7 +72,9 @@ 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) { this.ctx = ctx; this.endpointInHistory = endpointInHistory; this.endpointOutHistory = endpointOutHistory; @@ -77,6 +82,8 @@ class EndpointsTab implements MonitorTab { this.endpointRemoteOutHistory = endpointRemoteOutHistory; this.endpointRemoteStubInHistory = endpointRemoteStubInHistory; this.endpointRemoteStubOutHistory = endpointRemoteStubOutHistory; + this.endpointInSizeHistory = endpointInSizeHistory; + this.endpointOutSizeHistory = endpointOutSizeHistory; } @Override @@ -131,6 +138,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 +155,61 @@ 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)) .block(Block.builder().borderType(BorderType.ROUNDED) .title(" Endpoints sort:" + sort + (filter == 1 ? " filter:remote" : filter == 2 ? " filter:remote+stub" : "") @@ -191,6 +217,9 @@ class EndpointsTab implements MonitorTab { .build()) .build(); + boolean hasSizeHistory = !endpointInSizeHistory.isEmpty() + && endpointInSizeHistory.values().stream().anyMatch(h -> h.stream().anyMatch(v -> v > 0)); + List<Rect> chunks = showChart ? Layout.vertical().constraints(Constraint.fill(), Constraint.length(16)).split(area) : List.of(area); @@ -206,7 +235,16 @@ class EndpointsTab implements MonitorTab { .filter(ep -> "out".equals(ep.direction) && matchesFilter(ep)) .mapToLong(ep -> ep.hits) .sum(); - renderEndpointFlow(frame, chunks.get(1), inTotal, outTotal, info.name, info.pid); + + 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); + } } } @@ -253,6 +291,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 : ""; @@ -373,6 +413,76 @@ class EndpointsTab implements MonitorTab { .build(), rightArea); } + 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. *
