This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch worktree-more-tui-2 in repository https://gitbox.apache.org/repos/asf/camel.git
commit c22c6f144b86e5dca2d24d8c95838a1d8369b0cb Author: Claus Ibsen <[email protected]> AuthorDate: Tue May 19 18:10:58 2026 +0200 camel-jbang - Add live circuit breaker chart to TUI with failure rate bar, throughput sparkline, and metrics Splits the circuit breaker detail panel horizontally: state diagram on the left, live chart on the right. The chart includes a failure rate progress bar (color-coded green/yellow/red), a mirrored sparkline showing success and fail throughput over 60 seconds, and a metrics summary with total/fail/reject counts and mean/min/max latency from processor statistics. Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../dsl/jbang/core/commands/tui/CamelMonitor.java | 173 ++++++++++++++++++--- 1 file changed, 153 insertions(+), 20 deletions(-) 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 14b2caae3386..0e6961bf2b13 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 @@ -195,6 +195,12 @@ public class CamelMonitor extends CamelCommand { private final Map<String, LinkedList<long[]>> endpointRemoteStubSamples = new ConcurrentHashMap<>(); private final Map<String, Long> previousEndpointRemoteStubTime = 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<>(); + private final Map<String, LinkedList<long[]>> cbThroughputSamples = new ConcurrentHashMap<>(); + private final Map<String, Long> previousCbTime = new ConcurrentHashMap<>(); + // Load averages (EWMA) — CPU%, per PID (inflight EWMA is read from the management JSON) private final Map<String, LoadAvg> cpuLoadAvg = new ConcurrentHashMap<>(); private final Map<String, long[]> prevCpuSample = new ConcurrentHashMap<>(); @@ -2956,11 +2962,21 @@ public class CamelMonitor extends CamelCommand { frame.renderStatefulWidget(table, chunks.get(0), cbTableState); if (showDiagram) { - renderCircuitBreakerDiagram(frame, chunks.get(1), selectedCb); + renderCircuitBreakerDiagram(frame, chunks.get(1), selectedCb, info.pid); } } - private void renderCircuitBreakerDiagram(Frame frame, Rect area, CircuitBreakerInfo cb) { + private void renderCircuitBreakerDiagram(Frame frame, Rect area, CircuitBreakerInfo cb, String pid) { + // Split horizontally: state diagram on left (55 cols), chart on right (fill) + List<Rect> hSplit = Layout.horizontal() + .constraints(Constraint.length(55), Constraint.fill()) + .split(area); + + renderCbStateDiagram(frame, hSplit.get(0), cb); + renderCbChart(frame, hSplit.get(1), cb, pid); + } + + private void renderCbStateDiagram(Frame frame, Rect area, CircuitBreakerInfo cb) { String state = cb.state != null ? cb.state.toLowerCase() : ""; boolean isClosed = state.equals("closed"); boolean isOpen = state.equals("open") || state.equals("forced_open"); @@ -3005,14 +3021,13 @@ public class CamelMonitor extends CamelCommand { // │ │ │ lines.add(Line.from(Span.raw( " │ │ │"))); - // │ success wait timeout │ fail + // │ success wait timeout │ │ lines.add(Line.from( Span.raw(" │"), Span.styled(" success", lbl), - Span.raw(" "), + Span.raw(" "), Span.styled("wait timeout", lbl), - Span.raw(" │"), - Span.styled(" fail", lbl))); + Span.raw(" │ │"))); // │ │ │ lines.add(Line.from(Span.raw( " │ │ │"))); @@ -3036,20 +3051,6 @@ public class CamelMonitor extends CamelCommand { lines.add(Line.from( Span.raw(" "), Span.styled("└──────────────┘", halfOpenBox))); - // Metrics - lines.add(Line.from(Span.raw(""))); - lines.add(Line.from( - Span.raw(" "), - Span.styled("success:", lbl), - Span.raw(cb.successfulCalls + " "), - Span.styled("fail:", lbl), - Span.raw(cb.failedCalls + " "), - Span.styled("rate:", lbl), - Span.raw((cb.failureRate >= 0 ? String.format("%.0f%%", cb.failureRate) : "n/a") + " "), - Span.styled("pending:", lbl), - Span.raw(cb.bufferedCalls + " "), - Span.styled("rejected:", lbl), - Span.raw(String.valueOf(cb.notPermittedCalls)))); String title = " "; if (cb.id != null && !cb.id.isEmpty()) { @@ -3066,6 +3067,94 @@ public class CamelMonitor extends CamelCommand { .build(), area); } + private void renderCbChart(Frame frame, Rect area, CircuitBreakerInfo cb, String pid) { + String key = pid + "/" + cb.id; + + // Split vertically: failure rate bar (3 rows), sparkline (fill), metrics (4 rows) + List<Rect> vSplit = Layout.vertical() + .constraints(Constraint.length(3), Constraint.fill(), Constraint.length(4)) + .split(area); + + // Top: Failure rate bar (fixed width matching sparkline data points) + double rate = cb.failureRate >= 0 ? cb.failureRate : 0; + Style barColor; + if (rate >= 80) { + barColor = Style.EMPTY.fg(Color.LIGHT_RED); + } else if (rate >= 50) { + barColor = Style.EMPTY.fg(Color.YELLOW); + } else { + barColor = Style.EMPTY.fg(Color.GREEN); + } + String rateLabel = String.format(" %.0f%%", Math.max(0, cb.failureRate)); + int barWidth = MAX_ENDPOINT_CHART_POINTS; + int usable = barWidth - rateLabel.length(); + int filled = Math.max(0, (int) (usable * rate / 100.0)); + int empty = Math.max(0, usable - filled); + Line barLine = Line.from( + Span.raw(" "), + Span.styled("█".repeat(filled), barColor), + Span.styled(rateLabel, Style.EMPTY.bold()), + Span.styled("░".repeat(empty), Style.EMPTY.dim())); + frame.renderWidget(Paragraph.builder() + .text(Text.from(barLine)) + .block(Block.builder().borderType(BorderType.ROUNDED).title(" Failure Rate ").build()) + .build(), vSplit.get(0)); + + // Middle: Throughput sparkline (success up, fail down) + LinkedList<Long> successHist = cbSuccessHistory.get(key); + LinkedList<Long> failHist = cbFailHistory.get(key); + int renderPoints = MAX_ENDPOINT_CHART_POINTS; + long[] successArr = new long[renderPoints]; + long[] failArr = new long[renderPoints]; + if (successHist != null) { + for (int i = 0; i < renderPoints; i++) { + int idx = successHist.size() - renderPoints + i; + if (idx >= 0) { + successArr[i] = successHist.get(idx); + } + } + } + if (failHist != null) { + for (int i = 0; i < renderPoints; i++) { + int idx = failHist.size() - renderPoints + i; + if (idx >= 0) { + failArr[i] = failHist.get(idx); + } + } + } + long curSuccess = successArr[renderPoints - 1]; + long curFail = failArr[renderPoints - 1]; + Line chartTitle = Line.from( + Span.styled("▬", Style.EMPTY.fg(Color.GREEN)), + Span.raw(String.format(" ok:%-4d ", curSuccess)), + Span.styled("▬", Style.EMPTY.fg(Color.LIGHT_RED)), + Span.raw(String.format(" fail:%-4d msg/s", curFail))); + frame.renderWidget(MirroredSparkline.builder() + .topData(successArr) + .bottomData(failArr) + .topStyle(Style.EMPTY.fg(Color.GREEN)) + .bottomStyle(Style.EMPTY.fg(Color.LIGHT_RED)) + .xLabels("-60s", "-45s", "-30s", "-15s", "now") + .block(Block.builder().borderType(BorderType.ROUNDED) + .title(Title.from(chartTitle)).build()) + .build(), vSplit.get(1)); + + // Bottom: Metrics summary + Style dim = Style.EMPTY.dim(); + Line metricsLine = Line.from( + Span.raw(" "), + Span.styled("total:", dim), Span.raw(cb.total + " "), + Span.styled("fail:", dim), Span.raw(cb.totalFailed + " "), + Span.styled("reject:", dim), Span.raw(cb.notPermittedCalls + " "), + Span.styled("mean:", dim), Span.raw(cb.meanTime + "ms "), + Span.styled("min:", dim), Span.raw(cb.minTime + "ms "), + Span.styled("max:", dim), Span.raw(cb.maxTime + "ms")); + frame.renderWidget(Paragraph.builder() + .text(Text.from(Line.from(Span.raw("")), metricsLine)) + .block(Block.builder().borderType(BorderType.ROUNDED).build()) + .build(), vSplit.get(2)); + } + private String cbSortLabel(String label, String column) { return sortLabel(label, column, cbSort, cbSortReversed); } @@ -4933,6 +5022,7 @@ public class CamelMonitor extends CamelCommand { infos.add(info); updateThroughputHistory(info); updateEndpointHistory(info); + updateCbHistory(info); updateLoadMetrics(ph, info); } } @@ -4970,6 +5060,11 @@ public class CamelMonitor extends CamelCommand { 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)); } else if (!livePids.contains(entry.getKey())) { IntegrationInfo ghost = entry.getValue().info; ghost.vanishing = true; @@ -5125,6 +5220,20 @@ public class CamelMonitor extends CamelCommand { } } + private void updateCbHistory(IntegrationInfo info) { + long now = System.currentTimeMillis(); + for (CircuitBreakerInfo cb : info.circuitBreakers) { + if (cb.id == null) { + continue; + } + String key = info.pid + "/" + cb.id; + long success = cb.successfulCalls; + long failed = cb.failedCalls; + recordEndpointSample(key, now, success, failed, + cbThroughputSamples, previousCbTime, cbSuccessHistory, cbFailHistory); + } + } + // ---- Trace Data Loading ---- private void refreshTraceData(List<Long> pids) { @@ -5819,6 +5928,24 @@ public class CamelMonitor extends CamelCommand { parseCbSection(root, "fault-tolerance", info); parseCbSection(root, "circuit-breaker", info); + // Enrich circuit breakers with processor statistics (matched by id) + for (CircuitBreakerInfo cb : info.circuitBreakers) { + if (cb.id != null) { + for (RouteInfo ri : info.routes) { + for (ProcessorInfo pi : ri.processors) { + if (cb.id.equals(pi.id)) { + cb.total = pi.total; + cb.totalFailed = pi.failed; + cb.meanTime = pi.meanTime; + cb.minTime = pi.minTime; + cb.maxTime = pi.maxTime; + break; + } + } + } + } + } + // Parse REST DSL services JsonObject restsObj = (JsonObject) root.get("rests"); if (restsObj != null) { @@ -6167,6 +6294,12 @@ public class CamelMonitor extends CamelCommand { long failedCalls; long notPermittedCalls; double failureRate; // -1 means not available + // enriched from processor statistics (matched by id) + long total; + long totalFailed; + long meanTime; + long minTime; + long maxTime; } // HTTP endpoint from REST DSL or Platform-HTTP
