This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26700-about in repository https://gitbox.apache.org/repos/asf/camel.git
commit dfc49bb1c38e1e5cde1478866a8be16fcd46515c Author: Claus Ibsen <[email protected]> AuthorDate: Sat Nov 7 16:09:58 2026 +0100 CAMEL-26700: About - when the worst of a route last happened, and the failure a recovery recovered from Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../dsl/jbang/core/commands/tui/AboutView.java | 59 +++++++++++++++++++--- .../dsl/jbang/core/commands/tui/AboutViewTest.java | 22 ++++++++ 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java index 5eeab4899e27..6609884bb175 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java @@ -318,12 +318,26 @@ final class AboutView { } long done = r.total(r.completed()) + r.total(r.recovered()) + r.total(r.handled()) + r.total(r.failed()); spans.add(Span.styled(" " + done, Theme.success())); - if (r.total(r.failed()) > 0) { - spans.add(Span.styled(" " + r.total(r.failed()) + " failed", Theme.error())); - } else if (r.total(r.handled()) > 0) { - spans.add(Span.styled(" " + r.total(r.handled()) + " handled", Style.EMPTY.fg(Theme.ACCENT))); - } else if (r.total(r.recovered()) > 0) { - spans.add(Span.styled(" " + r.total(r.recovered()) + " recovered", Theme.warning())); + // the worst that happened, when it last did, and for a recovery the failure it recovered from (a failed or + // handled one has its cause on the Last 5m line and on the Timeline) + long[] worst = r.total(r.failed()) > 0 ? r.failed() : r.total(r.handled()) > 0 ? r.handled() + : r.total(r.recovered()) > 0 ? r.recovered() : null; + if (worst != null) { + String what = worst == r.failed() ? " failed" : worst == r.handled() ? " handled" : " recovered"; + Style style = worst == r.failed() ? Theme.error() + : worst == r.handled() ? Style.EMPTY.fg(Theme.ACCENT) : Theme.warning(); + long at = lastAt(worst, end, seconds); + spans.add(Span.styled(" " + r.total(worst) + what, style)); + spans.add(Span.styled(" " + ago(last - at), Theme.muted())); + String cause = worst == r.recovered() ? recoveredFrom(info, r.route(), end - seconds + 1, end) : null; + if (cause != null) { + int room = width - label - columns - 40; + if (room > 10) { + spans.add(Span.styled(" · ", Theme.muted())); + spans.add(Span.styled(cause.length() > room ? cause.substring(0, room - 1) + "…" : cause, + Theme.warning())); + } + } } lines.add(Line.from(spans)); } @@ -336,6 +350,39 @@ final class AboutView { return lines; } + /** The last second of the window with a value, or the end. */ + private static long lastAt(long[] values, long end, int seconds) { + for (int i = values.length - 1; i >= 0; i--) { + if (values[i] > 0) { + return end - seconds + 1 + i; + } + } + return end; + } + + private static String ago(long seconds) { + return seconds > 0 ? ChaosTab.shortTime(seconds * 1000) + " ago" : "now"; + } + + /** + * The failure the last recovered exchange of the route was redelivered for, from the exchanges the integration + * kept; as a route called by another keeps its exchanges under the caller, any recovered exchange of the window + * will do. + */ + private String recoveredFrom(IntegrationInfo info, String routeId, long from, long to) { + String any = null; + for (Timeline.Kept k : ctx.timeline.exchanges(info.pid, from, to)) { + if (k.redelivered() && !k.failed() && !k.handled() && k.exception() != null) { + if (routeId.equals(k.routeId())) { + any = k.exception(); + } else if (any == null) { + any = k.exception(); + } + } + } + return any; + } + private static String meta(IntegrationInfo info, ProjectOverview.Overview o) { StringBuilder sb = new StringBuilder(); if (info.camelVersion != null) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java index 7dcf5d658814..c2352c5173c4 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java @@ -120,6 +120,28 @@ class AboutViewTest { assertThat(about.handleKeyEvent(KeyEvent.ofChar('x'), info)).isFalse(); } + @Test + void aRecoveryShowsWhenAndTheFailureItRecoveredFrom() { + IntegrationInfo info = new IntegrationInfo(); + info.pid = "1234"; + for (String id : List.of("orders", "stock")) { + RouteInfo r = new RouteInfo(); + r.routeId = id; + info.routes.add(r); + } + MonitorContext ctx = new MonitorContext(new AtomicReference<>(List.of(info)), new AtomicReference<>(List.of())); + TimelineJson t = new TimelineJson(T0 / 1000 + 299, 300); + for (int i = 0; i < 300; i++) { + t.route("orders", "completed", i, 1); + } + // redelivered in stock 2 minutes ago, kept under orders where it came in + t.route("stock", "recovered", 179, 1).route("stock", "redeliveryAttempts", 179, 1); + t.exchange("R1", "orders", 179, 40, "recovered", "Connection refused", null); + ctx.timeline.merge("1234", t.json(), true); + AboutView about = new AboutView(ctx); + assertThat(render(about, info)).contains("1 recovered 2m ago · Connection refused"); + } + @Test void theSendsOfARouteToAnEndpoint() { IntegrationInfo info = new IntegrationInfo();
