This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch fix/camel-tui-circuit-breaker
in repository https://gitbox.apache.org/repos/asf/camel.git

commit c21b4437bdbfbdba79e808aab8a08305acc82246
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat May 16 15:29:00 2026 +0200

    TUI: add Circuit Breaker tab (tab 5)
    
    New tab showing all circuit breakers for the selected integration,
    inserted between Endpoints and Health. Supports all three Camel
    implementations: resilience4j, fault-tolerance, and core
    (ThrottlingExceptionRoutePolicy).
    
    Columns: ROUTE · ID · COMPONENT · STATE · PENDING · SUCCESS · FAIL · RATE% 
· REJECT
    - STATE is colour-coded: green=CLOSED, red=OPEN/FORCED_OPEN, 
yellow=HALF_OPEN
    - PENDING and REJECT are resilience4j-only
    - RATE% is resilience4j-only (failure rate percentage)
    - fault-tolerance shows only ROUTE · ID · STATE (its dev console provides 
no counters)
    
    Sort: press 's' to cycle ROUTE → ID → COMPONENT → STATE.
    Tab badge shows circuit breaker count; empty table shows "No circuit 
breakers".
    
    Tab numbering shifted: Circuit Breaker=5, Health=6, History=7, Trace=8.
    All footer hints updated to "1-8 tabs". Health tab gains red "(N DOWN)" 
badge
    when any check is DOWN.
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  | 222 +++++++++++++++++++--
 1 file changed, 206 insertions(+), 16 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 1fe15c28d78c..d266188aa94e 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
@@ -106,16 +106,17 @@ public class CamelMonitor extends CamelCommand {
     private static final int MAX_SPARKLINE_POINTS = 60;
     private static final int MAX_LOG_LINES = 5000;
     private static final int MAX_TRACES = 200;
-    private static final int NUM_TABS = 7;
+    private static final int NUM_TABS = 8;
 
     // Tab indices
     private static final int TAB_OVERVIEW = 0;
     private static final int TAB_LOG = 1;
     private static final int TAB_ROUTES = 2;
     private static final int TAB_ENDPOINTS = 3;
-    private static final int TAB_HEALTH = 4;
-    private static final int TAB_HISTORY = 5;
-    private static final int TAB_TRACE = 6;
+    private static final int TAB_CIRCUIT_BREAKER = 4;
+    private static final int TAB_HEALTH = 5;
+    private static final int TAB_HISTORY = 6;
+    private static final int TAB_TRACE = 7;
 
     // Overview sort columns
     private static final String[] OVERVIEW_SORT_COLUMNS = { "pid", "name", 
"status", "total", "fail" };
@@ -126,6 +127,9 @@ public class CamelMonitor extends CamelCommand {
     // Endpoint sort columns (order matches table column order)
     private static final String[] ENDPOINT_SORT_COLUMNS = { "component", 
"route", "dir", "total", "uri" };
 
+    // Circuit breaker sort columns (order matches table column order)
+    private static final String[] CB_SORT_COLUMNS = { "route", "id", 
"component", "state" };
+
     @CommandLine.Parameters(description = "Name or pid of running Camel 
integration", arity = "0..1")
     String name = "*";
 
@@ -141,6 +145,7 @@ public class CamelMonitor extends CamelCommand {
     private final TableState routeTableState = new TableState();
     private final TableState healthTableState = new TableState();
     private final TableState endpointTableState = new TableState();
+    private final TableState cbTableState = new TableState();
     private final TableState processorTableState = new TableState();
     private final TableState routeHeaderTableState = new TableState();
     private final TabsState tabsState = new TabsState(TAB_OVERVIEW);
@@ -167,6 +172,10 @@ public class CamelMonitor extends CamelCommand {
     // Endpoint filter state
     private boolean showOnlyRemote;
 
+    // Circuit breaker sort state (default: route = index 0)
+    private String cbSort = "route";
+    private int cbSortIndex = 0;
+
     // Health filter state
     private boolean showOnlyDown;
 
@@ -317,12 +326,15 @@ public class CamelMonitor extends CamelCommand {
                 return handleTabKey(TAB_ENDPOINTS);
             }
             if (ke.isChar('5')) {
-                return handleTabKey(TAB_HEALTH);
+                return handleTabKey(TAB_CIRCUIT_BREAKER);
             }
             if (ke.isChar('6')) {
-                return handleTabKey(TAB_HISTORY);
+                return handleTabKey(TAB_HEALTH);
             }
             if (ke.isChar('7')) {
+                return handleTabKey(TAB_HISTORY);
+            }
+            if (ke.isChar('8')) {
                 return handleTabKey(TAB_TRACE);
             }
 
@@ -435,6 +447,13 @@ public class CamelMonitor extends CamelCommand {
                 return true;
             }
 
+            // Circuit breaker tab: sort
+            if (tab == TAB_CIRCUIT_BREAKER && ke.isCharIgnoreCase('s')) {
+                cbSortIndex = (cbSortIndex + 1) % CB_SORT_COLUMNS.length;
+                cbSort = CB_SORT_COLUMNS[cbSortIndex];
+                return true;
+            }
+
             // Endpoints tab: sort and filter
             if (tab == TAB_ENDPOINTS && ke.isCharIgnoreCase('s')) {
                 endpointSortIndex = (endpointSortIndex + 1) % 
ENDPOINT_SORT_COLUMNS.length;
@@ -793,7 +812,10 @@ public class CamelMonitor extends CamelCommand {
         boolean hasSelection = selectedPid != null && sel != null;
         int routeCount = hasSelection ? sel.routes.size() : 0;
         int endpointCount = hasSelection ? sel.endpoints.size() : 0;
+        int cbCount = hasSelection ? sel.circuitBreakers.size() : 0;
         int healthCount = hasSelection ? sel.healthChecks.size() : 0;
+        long healthDownCount = hasSelection
+                ? sel.healthChecks.stream().filter(hc -> 
"DOWN".equals(hc.state)).count() : 0;
         int historyCount = hasSelection ? historyEntries.size() : 0;
         boolean hasTraces = hasSelection && !traces.get().isEmpty();
 
@@ -803,12 +825,13 @@ public class CamelMonitor extends CamelCommand {
                         Line.from(" 2 Log "),
                         badge(" 3 Routes ", routeCount),
                         badge(" 4 Endpoints ", endpointCount),
-                        badge(" 5 Health ", healthCount),
-                        badge(" 6 History ", historyCount),
+                        badge(" 5 Circuit Breaker ", cbCount),
+                        badgeHealth(" 6 Health ", healthCount, 
healthDownCount),
+                        badge(" 7 History ", historyCount),
                         hasTraces
-                                ? Line.from(Span.raw(" 7 Trace "), 
Span.styled("(*)", Style.EMPTY.fg(Color.YELLOW).bold()),
+                                ? Line.from(Span.raw(" 8 Trace "), 
Span.styled("(*)", Style.EMPTY.fg(Color.YELLOW).bold()),
                                         Span.raw(" "))
-                                : Line.from(" 7 Trace "))
+                                : Line.from(" 8 Trace "))
                 .highlightStyle(Style.EMPTY.fg(Color.rgb(0xF6, 0x91, 
0x23)).bold())
                 .divider(Span.styled(" | ", Style.EMPTY.dim()))
                 .build();
@@ -824,8 +847,9 @@ public class CamelMonitor extends CamelCommand {
         switch (tabsState.selected()) {
             case TAB_OVERVIEW -> renderOverview(frame, area);
             case TAB_ROUTES -> renderRoutes(frame, area);
-            case TAB_HEALTH -> renderHealth(frame, area);
             case TAB_ENDPOINTS -> renderEndpoints(frame, area);
+            case TAB_CIRCUIT_BREAKER -> renderCircuitBreaker(frame, area);
+            case TAB_HEALTH -> renderHealth(frame, area);
             case TAB_LOG -> renderLog(frame, area);
             case TAB_TRACE -> renderTrace(frame, area);
             case TAB_HISTORY -> renderHistory(frame, area);
@@ -1802,6 +1826,108 @@ public class CamelMonitor extends CamelCommand {
         return null;
     }
 
+    // ---- Tab 5: Circuit Breaker ----
+
+    private void renderCircuitBreaker(Frame frame, Rect area) {
+        IntegrationInfo info = findSelectedIntegration();
+        if (info == null) {
+            renderNoSelection(frame, area);
+            return;
+        }
+
+        List<CircuitBreakerInfo> sorted = new 
ArrayList<>(info.circuitBreakers);
+        sorted.sort(this::sortCircuitBreaker);
+
+        List<Row> rows = new ArrayList<>();
+        for (CircuitBreakerInfo cb : sorted) {
+            Style stateStyle = switch (cb.state != null ? 
cb.state.toLowerCase() : "") {
+                case "closed" -> Style.EMPTY.fg(Color.GREEN);
+                case "open", "forced_open" -> Style.EMPTY.fg(Color.RED);
+                default -> Style.EMPTY.fg(Color.YELLOW); // half_open / half 
opened / unknown
+            };
+            String state = cb.state != null ? cb.state : "";
+            String pending = cb.bufferedCalls > 0 ? 
String.valueOf(cb.bufferedCalls) : "";
+            String success = cb.successfulCalls > 0 ? 
String.valueOf(cb.successfulCalls) : "";
+            String failed = cb.failedCalls > 0 ? 
String.valueOf(cb.failedCalls) : "";
+            String reject = cb.notPermittedCalls > 0 ? 
String.valueOf(cb.notPermittedCalls) : "";
+            String rate = cb.failureRate >= 0 ? String.format("%.0f%%", 
cb.failureRate) : "";
+
+            rows.add(Row.from(
+                    Cell.from(Span.styled(cb.routeId != null ? cb.routeId : 
"", Style.EMPTY.fg(Color.CYAN))),
+                    Cell.from(cb.id != null ? cb.id : ""),
+                    Cell.from(cb.component != null ? cb.component : ""),
+                    Cell.from(Span.styled(state, stateStyle)),
+                    rightCell(pending, 8),
+                    rightCell(success, 8),
+                    rightCell(failed, 8),
+                    rightCell(rate, 6),
+                    rightCell(reject, 8)));
+        }
+
+        if (rows.isEmpty()) {
+            rows.add(Row.from(
+                    Cell.from(Span.styled("No circuit breakers", 
Style.EMPTY.dim())),
+                    Cell.from(""), Cell.from(""), Cell.from(""),
+                    Cell.from(""), Cell.from(""), Cell.from(""), 
Cell.from(""), Cell.from("")));
+        }
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        Cell.from(Span.styled(cbSortLabel("ROUTE", "route"), 
cbSortStyle("route"))),
+                        Cell.from(Span.styled(cbSortLabel("ID", "id"), 
cbSortStyle("id"))),
+                        Cell.from(Span.styled(cbSortLabel("COMPONENT", 
"component"), cbSortStyle("component"))),
+                        Cell.from(Span.styled(cbSortLabel("STATE", "state"), 
cbSortStyle("state"))),
+                        rightCell("PENDING", 8, Style.EMPTY.bold()),
+                        rightCell("SUCCESS", 8, Style.EMPTY.bold()),
+                        rightCell("FAIL", 8, Style.EMPTY.bold()),
+                        rightCell("RATE%", 6, Style.EMPTY.bold()),
+                        rightCell("REJECT", 8, Style.EMPTY.bold())))
+                .widths(
+                        Constraint.length(20),
+                        Constraint.length(20),
+                        Constraint.length(16),
+                        Constraint.length(12),
+                        Constraint.length(8),
+                        Constraint.length(8),
+                        Constraint.length(8),
+                        Constraint.length(6),
+                        Constraint.fill())
+                .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
+                .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+                .block(Block.builder().borderType(BorderType.ROUNDED).title(" 
Circuit Breaker ").build())
+                .build();
+
+        frame.renderStatefulWidget(table, area, cbTableState);
+    }
+
+    private String cbSortLabel(String label, String column) {
+        return cbSort.equals(column) ? label + " ▴" : label;
+    }
+
+    private Style cbSortStyle(String column) {
+        return cbSort.equals(column) ? Style.EMPTY.fg(Color.YELLOW).bold() : 
Style.EMPTY.bold();
+    }
+
+    private int sortCircuitBreaker(CircuitBreakerInfo a, CircuitBreakerInfo b) 
{
+        return switch (cbSort) {
+            case "id" -> compareStr(a.id, b.id);
+            case "component" -> compareStr(a.component, b.component);
+            case "state" -> compareStr(a.state, b.state);
+            default -> compareStr(a.routeId, b.routeId); // "route"
+        };
+    }
+
+    private static int compareStr(String a, String b) {
+        if (a == null && b == null)
+            return 0;
+        if (a == null)
+            return 1;
+        if (b == null)
+            return -1;
+        return a.compareToIgnoreCase(b);
+    }
+
     // ---- Tab 3: Health ----
 
     private void renderHealth(Frame frame, Rect area) {
@@ -2700,7 +2826,7 @@ public class CamelMonitor extends CamelCommand {
             if (selectedPid != null) {
                 hint(spans, "Esc", "unselect");
             }
-            hint(spans, "1-7", "tabs");
+            hint(spans, "1-8", "tabs");
         } else if (tab == TAB_ROUTES && showDiagram) {
             String closeKey = diagramTextMode ? "D" : "d";
             hint(spans, closeKey + "/Esc", "close");
@@ -2719,18 +2845,23 @@ public class CamelMonitor extends CamelCommand {
             hint(spans, "s", "sort");
             hint(spans, "d", "diagram");
             hint(spans, "D", "text diagram");
-            hint(spans, "1-7", "tabs");
+            hint(spans, "1-8", "tabs");
         } else if (tab == TAB_ENDPOINTS) {
             hint(spans, "Esc", "back");
             hint(spans, "\u2191\u2193", "navigate");
             hint(spans, "s", "sort");
             hint(spans, "r", "remote" + (showOnlyRemote ? " [on]" : " [off]"));
-            hint(spans, "1-7", "tabs");
+            hint(spans, "1-8", "tabs");
+        } else if (tab == TAB_CIRCUIT_BREAKER) {
+            hint(spans, "Esc", "back");
+            hint(spans, "\u2191\u2193", "navigate");
+            hint(spans, "s", "sort");
+            hint(spans, "1-8", "tabs");
         } else if (tab == TAB_HEALTH) {
             hint(spans, "Esc", "back");
             hint(spans, "\u2191\u2193", "navigate");
             hint(spans, "d", "toggle DOWN");
-            hint(spans, "1-7", "tabs");
+            hint(spans, "1-8", "tabs");
         } else if (tab == TAB_LOG) {
             hint(spans, "Esc", "back");
             hint(spans, "\u2191\u2193", "scroll");
@@ -2775,7 +2906,7 @@ public class CamelMonitor extends CamelCommand {
         } else {
             hint(spans, "Esc", "back");
             hint(spans, "\u2191\u2193", "navigate");
-            hint(spans, "1-7", "tabs");
+            hint(spans, "1-8", "tabs");
         }
 
         frame.renderWidget(Paragraph.from(Line.from(spans)), area);
@@ -2807,6 +2938,16 @@ public class CamelMonitor extends CamelCommand {
         return Cell.from(Span.styled(" ".repeat(leftPad) + text, style));
     }
 
+    private static Line badgeHealth(String label, long total, long down) {
+        if (down > 0) {
+            return Line.from(
+                    Span.raw(label),
+                    Span.styled("(" + down + " DOWN)", 
Style.EMPTY.fg(Color.RED).bold()),
+                    Span.raw(" "));
+        }
+        return badge(label, total);
+    }
+
     private static Line badge(String label, long count) {
         if (count > 0) {
             return Line.from(
@@ -3542,9 +3683,45 @@ public class CamelMonitor extends CamelCommand {
             }
         }
 
+        // Parse circuit breakers: resilience4j, fault-tolerance, core
+        parseCbSection(root, "resilience4j", info);
+        parseCbSection(root, "fault-tolerance", info);
+        parseCbSection(root, "circuit-breaker", info);
+
         return info;
     }
 
+    private static void parseCbSection(JsonObject root, String key, 
IntegrationInfo info) {
+        JsonObject section = (JsonObject) root.get(key);
+        if (section == null) {
+            return;
+        }
+        JsonArray breakers = (JsonArray) section.get("circuitBreakers");
+        if (breakers == null) {
+            return;
+        }
+        String component = switch (key) {
+            case "resilience4j" -> "resilience4j";
+            case "fault-tolerance" -> "fault-tolerance";
+            default -> "core";
+        };
+        for (Object b : breakers) {
+            JsonObject bj = (JsonObject) b;
+            CircuitBreakerInfo cb = new CircuitBreakerInfo();
+            cb.component = component;
+            cb.routeId = bj.getString("routeId");
+            cb.id = bj.getString("id");
+            cb.state = bj.getString("state");
+            cb.bufferedCalls = bj.getIntegerOrDefault("bufferedCalls", 0);
+            cb.successfulCalls = 
TuiHelper.objToLong(bj.get("successfulCalls"));
+            cb.failedCalls = TuiHelper.objToLong(bj.get("failedCalls"));
+            cb.notPermittedCalls = 
TuiHelper.objToLong(bj.get("notPermittedCalls"));
+            Object fr = bj.get("failureRate");
+            cb.failureRate = fr instanceof Number n ? n.doubleValue() : -1;
+            info.circuitBreakers.add(cb);
+        }
+    }
+
     // ---- Helpers ----
 
     private IntegrationInfo findSelectedIntegration() {
@@ -3671,6 +3848,7 @@ public class CamelMonitor extends CamelCommand {
         final List<RouteInfo> routes = new ArrayList<>();
         final List<HealthCheckInfo> healthChecks = new ArrayList<>();
         final List<EndpointInfo> endpoints = new ArrayList<>();
+        final List<CircuitBreakerInfo> circuitBreakers = new ArrayList<>();
     }
 
     static class RouteInfo {
@@ -3723,6 +3901,18 @@ public class CamelMonitor extends CamelCommand {
         boolean remote;
     }
 
+    static class CircuitBreakerInfo {
+        String routeId;
+        String id;
+        String component; // "resilience4j", "fault-tolerance", "core"
+        String state;
+        int bufferedCalls;
+        long successfulCalls;
+        long failedCalls;
+        long notPermittedCalls;
+        double failureRate; // -1 means not available
+    }
+
     static class TraceEntry {
         String pid;
         String uid;

Reply via email to