This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26864-tui-walk-inspect in repository https://gitbox.apache.org/repos/asf/camel.git
commit 50f60d3541918d9c3226ca1c52e38f2aef2b4765 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Nov 11 20:56:06 2026 +0100 CAMEL-26864: camel-jbang - Consumers has the total of an event-driven consumer, and the Info panel of Inspect says what a step is and changed - Consumers: a TOTAL column with the messages each consumer received (the exchanges of its route), also for direct and seda, which have no POLLS - Inspect diagram Info panel: message in / message out / to a remote endpoint instead of the marker *->, and Changed in words: (first step), nothing, or body, headers Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../dsl/jbang/core/commands/tui/ConsumerInfo.java | 2 ++ .../dsl/jbang/core/commands/tui/ConsumersTab.java | 11 ++++-- .../dsl/jbang/core/commands/tui/HistoryTab.java | 41 +++++++++++++++++----- .../dsl/jbang/core/commands/tui/StatusParser.java | 5 +++ .../src/main/resources/tui/help/consumers.md | 11 +++--- .../core/commands/tui/ConsumersTabRenderTest.java | 28 +++++++++++++++ .../core/commands/tui/HistoryTabRenderTest.java | 15 ++++++++ 7 files changed, 98 insertions(+), 15 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java index 403c83db4ed8..89290f9374ca 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java @@ -28,6 +28,8 @@ class ConsumerInfo { int inflight; Boolean polling; Long totalCounter; + // the messages its route received (the exchanges of the route), also for an event-driven consumer (CAMEL-26864) + long total = -1; Long delay; Long period; String schedule; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java index ab0e5a153642..d7985be91af8 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java @@ -42,7 +42,7 @@ import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*; class ConsumersTab extends AbstractTableTab { ConsumersTab(MonitorContext ctx) { - super(ctx, "id", "status", "type", "remote", "inflight", "polls", "uri"); + super(ctx, "id", "status", "type", "remote", "inflight", "polls", "total", "uri"); } @Override @@ -82,7 +82,7 @@ class ConsumersTab extends AbstractTableTab { // the URI gets the rest of the width: the borders, ">> ", 8 gaps and the other columns; a long id or URI // ends with an ellipsis where it is cut - int uriWidth = Math.max(0, area.width() - 2 - 3 - 9 - (20 + 10 + 16 + 8 + 8 + 22 + 8 + 10 + 10)); + int uriWidth = Math.max(0, area.width() - 2 - 3 - 10 - (20 + 10 + 16 + 8 + 8 + 22 + 8 + 8 + 10 + 10)); rows.add(Row.from( Cell.from(Span.styled(" " + TuiHelper.truncate(ci.id, 19), Style.EMPTY.fg(Theme.accent()))), Cell.from(Span.styled(statusText, statusStyle)), @@ -91,6 +91,7 @@ class ConsumersTab extends AbstractTableTab { rightCell(String.valueOf(ci.inflight), 8), Cell.from(schedule), rightCell(ci.totalCounter != null ? String.valueOf(ci.totalCounter) : "", 8), + rightCell(ci.total >= 0 ? String.valueOf(ci.total) : "", 8), rightCell(last, 10), lastFail.isEmpty() ? rightCell("", 10) : rightCell(lastFail, 10, Theme.error()), Cell.from(Span.styled(TuiHelper.truncate(uri, uriWidth), healthDown ? Theme.error() : Style.EMPTY)))); @@ -110,6 +111,7 @@ class ConsumersTab extends AbstractTableTab { rightCell(sortLabel("INFLIGHT", "inflight"), 8, sortStyle("inflight")), Cell.from(Span.styled("SCHEDULE", Style.EMPTY.bold())), rightCell(sortLabel("POLLS", "polls"), 8, sortStyle("polls")), + rightCell(sortLabel("TOTAL", "total"), 8, sortStyle("total")), rightCell("LAST", 10, Style.EMPTY.bold()), rightCell("LAST FAIL", 10, Style.EMPTY.bold()), Cell.from(Span.styled(sortLabel("URI", "uri"), sortStyle("uri"))))) @@ -121,6 +123,7 @@ class ConsumersTab extends AbstractTableTab { Constraint.length(8), Constraint.length(22), Constraint.length(8), + Constraint.length(8), Constraint.length(10), Constraint.length(10), Constraint.fill()) @@ -154,6 +157,7 @@ class ConsumersTab extends AbstractTableTab { long lb = b.totalCounter != null ? b.totalCounter : 0; yield Long.compare(lb, la); } + case "total" -> Long.compare(b.total, a.total); case "uri" -> { String ua = a.uri != null ? a.uri : ""; String ub = b.uri != null ? b.uri : ""; @@ -356,6 +360,9 @@ class ConsumersTab extends AbstractTableTab { if (ci.totalCounter != null) { row.put("totalCounter", ci.totalCounter); } + if (ci.total >= 0) { + row.put("total", ci.total); + } if (ci.polling != null) { row.put("polling", ci.polling); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java index 6345bae4b9dd..2395a6f2b725 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.StringJoiner; import java.util.concurrent.atomic.AtomicReference; import dev.tamboui.layout.Alignment; @@ -981,7 +982,7 @@ class HistoryTab extends AbstractTab { routeId = e.routeId; nodeId = e.nodeId; processor = e.processor; - direction = e.direction; + direction = stepKind(e.first, e.last, e.remoteEndpoint, e.stubEndpoint); timestamp = e.timestamp; threadName = e.threadName; elapsed = e.elapsed; @@ -1005,7 +1006,7 @@ class HistoryTab extends AbstractTab { routeId = e.routeId; nodeId = e.nodeId; processor = e.processor; - direction = e.direction; + direction = stepKind(e.first, e.last, e.remoteEndpoint, e.stubEndpoint); timestamp = e.timestamp; threadName = e.threadName; elapsed = e.elapsed; @@ -1091,12 +1092,7 @@ class HistoryTab extends AbstractTab { boolean propsChanged = changes.length() > 2 && changes.charAt(2) == 'P'; boolean varsChanged = changes.length() > 3 && changes.charAt(3) == 'V'; - List<Span> changeSpans = new ArrayList<>(); - changeSpans.add(Span.styled(" Changed: ", Theme.muted())); - if (!changes.isBlank()) { - changeSpans.addAll(buildChangeSpans(changes)); - } - lines.add(Line.from(changeSpans)); + lines.add(Line.from(Span.styled(" Changed: ", Theme.muted()), changedWords(changes, stepIdx == 0))); boolean isTraceMode = !diagramTraceSteps.isEmpty(); boolean showBody = isTraceMode ? showTraceBody : showHistoryBody; @@ -1931,6 +1927,35 @@ class HistoryTab extends AbstractTab { rightCell(elapsedStr, 10)); } + /** + * What a step is, in words, for the Info panel; the table has the markers of camel get history ({@code *->}, + * {@code <-*}, {@code --->}, {@code ~-->}) in a column of its own. Empty for a step inside the route. + */ + static String stepKind(boolean first, boolean last, boolean remote, boolean stub) { + String where = stub ? " (stub)" : remote ? " (remote)" : ""; + if (first) { + return "message in" + where; + } else if (last) { + return "message out" + where; + } + return stub ? "to a stub" : remote ? "to a remote endpoint" : ""; + } + + /** What the step changed, in words (the table has the letters B, H, P and V): "body, headers", or nothing. */ + static Span changedWords(String changes, boolean firstStep) { + if (firstStep) { + return Span.styled("(first step)", Style.EMPTY.dim()); + } + String[] words = { "body", "headers", "properties", "variables" }; + StringJoiner sj = new StringJoiner(", "); + for (int i = 0; i < Math.min(changes.length(), words.length); i++) { + if (changes.charAt(i) != ' ') { + sj.add(words[i]); + } + } + return sj.length() > 0 ? Span.styled(sj.toString(), Theme.change()) : Span.styled("nothing", Style.EMPTY.dim()); + } + private static List<Span> buildChangeSpans(String changes) { if (changes == null || changes.isEmpty()) { return List.of(Span.raw("")); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java index b549c4d05c8b..2434fb444959 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java @@ -478,6 +478,11 @@ final class StatusParser { ci.inflight = cj.getIntegerOrDefault("inflight", 0); ci.polling = Boolean.TRUE.equals(cj.get("polling")); ci.totalCounter = cj.getLong("totalCounter"); + for (RouteInfo r : info.routes) { + if (r.routeId != null && r.routeId.equals(ci.id)) { + ci.total = r.total; + } + } ci.delay = cj.getLong("delay"); ci.period = cj.getLong("period"); JsonObject cStats = (JsonObject) cj.get("statistics"); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/consumers.md b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/consumers.md index 34bf23e72b4c..e59e3c30dee3 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/consumers.md +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/consumers.md @@ -17,6 +17,7 @@ There are two types of consumers: - **STATUS** — Consumer state: `Started` (running normally), `Polling` (currently in a poll cycle). Shows a health warning icon if the consumer health check is `DOWN` - **TYPE** — The Camel component type (e.g., `kafka`, `file`, `timer`, `cron`, `seda`) - **POLLS** — Total number of poll cycles the scheduler has executed. This counts poll **attempts**, not messages received — a poll may check for new files and find none. For event-driven consumers (like `seda`), this column is empty +- **TOTAL** — The messages the consumer received: the exchanges of its route. It is there for every consumer, also an event-driven one (`direct`, `seda`) that has no POLLS - **SCHEDULE** — How often the consumer polls: a fixed interval like `1s` or `5000ms`, or a cron expression like `0 0/5 * * *`. Only shown for polling consumers - **LAST** — How long ago the consumer's last exchange completed (or started, when none has completed yet). A consumer that should be busy but has a long LAST is stale - **LAST FAIL** — How long ago its last exchange failed, in red; blank when none did @@ -25,13 +26,13 @@ There are two types of consumers: ## Example Screen ``` - ROUTE STATUS TYPE POLLS SCHEDULE LAST LAST FAIL URI - timer-to-log Started timer 17 2s 1s ago timer://hello?period=2000 - timer-to-seda Started timer 12 3s 0s ago 5m ago timer://pump?period=3000 - seda-consumer Started seda 0s ago seda://queue + ROUTE STATUS TYPE REMOTE INFLIGHT SCHEDULE POLLS TOTAL LAST LAST FAIL URI + timer-to-log Started timer 0 2s 17 17 1s ago timer://hello?period=2000 + timer-to-seda Started timer 0 3s 12 12 0s ago 5m ago timer://pump?period=3000 + seda-consumer Started seda 0 12 0s ago seda://queue ``` -Notice that `seda-consumer` has no POLLS or SCHEDULE because it is +Notice that `seda-consumer` has no POLLS or SCHEDULE, only a TOTAL, because it is event-driven — it reacts immediately when a message is put into the SEDA queue by another route. diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java index 5f1273f0c2e2..68d492f63336 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java @@ -27,6 +27,9 @@ import dev.tamboui.terminal.Frame; import dev.tamboui.text.Span; import dev.tamboui.tui.event.KeyEvent; import dev.tamboui.tui.event.KeyModifiers; +import org.apache.camel.util.json.JsonArray; +import org.apache.camel.util.json.JsonObject; +import org.apache.camel.util.json.Jsoner; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -243,6 +246,31 @@ class ConsumersTabRenderTest { assertThat(ConsumersTab.ago("-")).isEmpty(); } + @Test + void anEventDrivenConsumerHasTheTotalOfItsRoute() throws Exception { + // CAMEL-26864: the direct consumer of route seven received 200 messages, but had no POLLS and no count at all + JsonObject root = (JsonObject) Jsoner.deserialize(""" + {"context": {"name": "hello"}, + "routes": [{"routeId": "seven", "statistics": {"exchangesTotal": 200}}, + {"routeId": "hello", "statistics": {"exchangesTotal": 1131}}], + "consumers": {"consumers": [ + {"id": "seven", "uri": "direct://seven", "state": "Started"}, + {"id": "hello", "uri": "timer://tick", "state": "Started", "totalCounter": 1131}]}} + """); + IntegrationInfo parsed = StatusParser.parseIntegration(ProcessHandle.current(), root); + info.consumers.addAll(parsed.consumers); + + ConsumersTab tab = new ConsumersTab(ctx); + String rendered = TuiTestHelper.renderToString(tab, 200, 10); + assertThat(rendered).contains("TOTAL"); + String seven = rendered.lines().filter(l -> l.contains("direct://seven")).findFirst().orElseThrow(); + assertThat(seven).contains(" 200 "); + JsonObject row = (JsonObject) ((JsonArray) tab.getTableDataAsJson().get("rows")).get(1); + assertThat(row.getString("id")).isEqualTo("seven"); + assertThat(row.getLong("total")).isEqualTo(200L); + assertThat(row.containsKey("totalCounter")).as("a direct consumer does not poll").isFalse(); + } + private ConsumerInfo addConsumer(String id, String uri, String state, String className) { ConsumerInfo ci = new ConsumerInfo(); ci.id = id; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTabRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTabRenderTest.java index 92d762487bc7..b36a21353f6d 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTabRenderTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTabRenderTest.java @@ -289,4 +289,19 @@ class HistoryTabRenderTest { private static HistoryTab.WaterfallStep step(String id, boolean first, boolean last, long elapsed, long start) { return new HistoryTab.WaterfallStep(id, id, "-->", first, last, 0, elapsed, 0, start); } + + @Test + void theInfoPanelSaysWhatAStepIsAndWhatItChangedInWords() { + // CAMEL-26864: it showed the marker of the table ("Step: 1/6 *->") and an empty "Changed:" + assertEquals("message in", HistoryTab.stepKind(true, false, false, false)); + assertEquals("message in (remote)", HistoryTab.stepKind(true, false, true, false)); + assertEquals("message out", HistoryTab.stepKind(false, true, false, false)); + assertEquals("to a remote endpoint", HistoryTab.stepKind(false, false, true, false)); + assertEquals("to a stub", HistoryTab.stepKind(false, false, true, true)); + assertEquals("", HistoryTab.stepKind(false, false, false, false)); + + assertEquals("(first step)", HistoryTab.changedWords("", true).content()); + assertEquals("nothing", HistoryTab.changedWords(" ", false).content()); + assertEquals("body, properties", HistoryTab.changedWords("B P ", false).content()); + } }
