You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/05/16 13:29:11 UTC

(camel) branch fix/camel-tui-circuit-breaker created (now c21b4437bdbf)

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

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


      at c21b4437bdbf TUI: add Circuit Breaker tab (tab 5)

This branch includes the following new commits:

     new c21b4437bdbf TUI: add Circuit Breaker tab (tab 5)

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(camel) 01/01: TUI: add Circuit Breaker tab (tab 5)

Posted by da...@apache.org.
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 <cl...@gmail.com>
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 <no...@anthropic.com>
---
 .../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;