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

Reply via email to