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;
