This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch chore-producers-about-strips-watch in repository https://gitbox.apache.org/repos/asf/camel.git
commit ddf7f02ba52579d80cce9f5e70ecfae9a7a41894 Author: Claus Ibsen <[email protected]> AuthorDate: Mon Nov 9 16:06:12 2026 +0100 chore: camel-jbang - Producers says what was sent, the strips of What happened fill the width, and Watch calms down once the errors stopped - Producers: TOTAL, FAIL and P50/P95/P99 of each producer, from the steps of its route that send to its endpoint (blank for a toD whose endpoint is computed per message). - About, What happened: the notes after a strip are measured instead of reserving 50 columns, and the strips may have up to 300 columns: a 200 wide screen has 150 columns of 2 seconds instead of 100 of 3. - About, Watch: says when the last error happened, and once the errors stopped more than 5 minutes ago, that there were none in the last 5 minutes, with the earlier ones dimmed. camel-cli-connector writes when the newest error happened in the error summary of the status file (lastTimestamp). Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../apache/camel/cli/connector/ErrorCounts.java | 12 ++- .../camel/cli/connector/ErrorCountsTest.java | 4 + .../dsl/jbang/core/commands/tui/AboutView.java | 61 +++++++++++-- .../jbang/core/commands/tui/IntegrationInfo.java | 2 + .../dsl/jbang/core/commands/tui/ProducersTab.java | 82 ++++++++++++++++- .../dsl/jbang/core/commands/tui/StatusParser.java | 1 + .../src/main/resources/tui/help/overview.md | 6 +- .../src/main/resources/tui/help/producers.md | 9 +- .../dsl/jbang/core/commands/tui/AboutViewTest.java | 53 ++++++++++- .../core/commands/tui/ProducersTabStatsTest.java | 100 +++++++++++++++++++++ 10 files changed, 311 insertions(+), 19 deletions(-) diff --git a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/ErrorCounts.java b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/ErrorCounts.java index 4cdb621699cc..06e85d7bea9d 100644 --- a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/ErrorCounts.java +++ b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/ErrorCounts.java @@ -39,11 +39,13 @@ final class ErrorCounts { } /** - * Adds to the summary the errors that happened ({@code total}) and the kinds that happened most ({@code kinds}: - * routeId, exceptionType, count), from the entries of the errors dev console. + * Adds to the summary the errors that happened ({@code total}), the kinds that happened most ({@code kinds}: + * routeId, exceptionType, count), and when the newest happened ({@code lastTimestamp}, epoch millis, when there is + * one), from the entries of the errors dev console. */ static void summarize(Map<String, Object> console, JsonObject summary) { Map<String, Kind> kinds = new LinkedHashMap<>(); + long last = 0; if (console.get("errors") instanceof List<?> entries) { for (Object o : entries) { if (o instanceof Map<?, ?> e) { @@ -53,6 +55,9 @@ final class ErrorCounts { String key = routeId + "|" + string(e.get("nodeId")) + "|" + type; Kind k = kinds.computeIfAbsent(key, x -> new Kind(routeId, type)); k.count = Math.max(k.count, count); + if (e.get("timestamp") instanceof Number ts) { + last = Math.max(last, ts.longValue()); + } } } } @@ -72,6 +77,9 @@ final class ErrorCounts { } summary.put("total", total); summary.put("kinds", top); + if (last > 0) { + summary.put("lastTimestamp", last); + } } private static String string(Object o) { diff --git a/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/ErrorCountsTest.java b/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/ErrorCountsTest.java index 2abebfb479cc..c3c5cf9b6368 100644 --- a/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/ErrorCountsTest.java +++ b/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/ErrorCountsTest.java @@ -47,6 +47,7 @@ class ErrorCountsTest { } }); context.start(); + long before = System.currentTimeMillis(); for (int i = 0; i < 10; i++) { send(context, "direct:supplier"); } @@ -71,6 +72,8 @@ class ErrorCountsTest { assertThat(first.getString("exceptionType")).isEqualTo("java.net.ConnectException"); assertThat(first.getLong("count")).isEqualTo(10); assertThat(((JsonObject) kinds.get(1)).getLong("count")).isEqualTo(2); + // when the newest error happened: the last of the 2 sent to the stock route + assertThat(summary.getLong("lastTimestamp")).isBetween(before, System.currentTimeMillis()); } } @@ -80,6 +83,7 @@ class ErrorCountsTest { ErrorCounts.summarize(Map.of("size", 0), summary); assertThat(summary.getLong("total")).isZero(); assertThat((List<?>) summary.get("kinds")).isEmpty(); + assertThat(summary.containsKey("lastTimestamp")).isFalse(); } private static void send(CamelContext context, String uri) { 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 4aba7b02e35f..cfd9f733f460 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 @@ -36,6 +36,7 @@ import dev.tamboui.layout.Padding; import dev.tamboui.layout.Rect; import dev.tamboui.style.Style; import dev.tamboui.terminal.Frame; +import dev.tamboui.text.CharWidth; import dev.tamboui.text.Line; import dev.tamboui.text.Span; import dev.tamboui.text.Text; @@ -67,9 +68,9 @@ final class AboutView { // the strips have at least this many columns, whatever the zoom, and as many as the width allows up to the most, // a column taking a whole number of seconds private static final int STRIP_COLUMNS = 60; - private static final int MAX_STRIP_COLUMNS = 150; - // what the line of a route has after its strip: its exchanges, an open circuit breaker, the worst that happened - private static final int STRIP_NOTES = 50; + private static final int MAX_STRIP_COLUMNS = 300; + // the cause of a recovery after the strip of a route, cut to this width when the strips need the room + private static final int CAUSE_WIDTH = 30; // the sparklines of In and Out, wider in a wider column private static final int SPARK_WIDTH = 14; private static final int MAX_SPARK_WIDTH = 30; @@ -621,14 +622,23 @@ final class AboutView { List<String> order = routeOrder(info); // the names, then one space: every line starts its columns at the same place int label = Math.min(18, Math.max(8, order.stream().mapToInt(String::length).max().orElse(8))) + 4; - int columns = stripColumns(seconds, width - label - STRIP_NOTES); + List<Timeline.RouteSeries> series = ctx.timeline.routes(info.pid, order, end, seconds); + // what the line of a route has after its strip (its exchanges, an open circuit breaker, the worst that + // happened, with the cause of a recovery up to CAUSE_WIDTH) takes the room it needs, and the strips the rest + int notes = 0; + for (Timeline.RouteSeries r : series) { + notes = Math.max(notes, width(stripNotes(info, r, last, end, seconds, CAUSE_WIDTH))); + } + int columns = stripColumns(seconds, width - label - notes - 1); + // the cause gets what the strips leave too + int causeRoom = CAUSE_WIDTH + Math.max(0, width - label - notes - 1 - columns); stripColumns = columns; int per = seconds / columns; // the columns that end before the integration started are blank, as the sparklines have them, where a dot is // a time it ran and did nothing (uptime is the time it started) long startOffset = info.uptime > 0 ? info.uptime / 1000 - (end - seconds + 1) : 0; int before = (int) Math.max(0, Math.min(columns, startOffset / per)); - for (Timeline.RouteSeries r : ctx.timeline.routes(info.pid, order, end, seconds)) { + for (Timeline.RouteSeries r : series) { if (spaced && !lines.isEmpty()) { lines.add(Line.from(Span.raw(""))); } @@ -658,7 +668,7 @@ final class AboutView { ? " " + name + " ".repeat(Math.max(1, label - 3 - name.length())) : " ".repeat(label))); spans.addAll(rows.get(row)); if (row == tall - 1) { - spans.addAll(stripNotes(info, r, last, end, seconds, width - label - columns - 40)); + spans.addAll(stripNotes(info, r, last, end, seconds, causeRoom)); } lines.add(Line.from(spans)); } @@ -741,6 +751,14 @@ final class AboutView { return spans; } + private static int width(List<Span> spans) { + int w = 0; + for (Span span : spans) { + w += CharWidth.of(span.content()); + } + return w; + } + /** * The columns of the strips: the most that fit the room, up to {@value #MAX_STRIP_COLUMNS}, with a whole number of * seconds in each; at least {@value #STRIP_COLUMNS}. @@ -1031,21 +1049,34 @@ final class AboutView { List<Span> watch = new ArrayList<>(); watch.add(Span.styled(" Watch ", Theme.muted())); if (info.errorCount > 0) { - watch.add(Span.styled("✗ " + info.errorCount + (info.errorCount == 1 ? " error" : " errors"), Theme.error())); + // errors that stopped before the last minutes are history: the line says so, and calms down + long lastError = lastError(info); + boolean old = lastError > 0 && System.currentTimeMillis() - lastError > HAPPENED_SECONDS * 1000L; + String count = info.errorCount + (info.errorCount == 1 ? " error" : " errors"); + Style style = old ? Theme.muted() : Theme.error(); + if (old) { + watch.add(Span.styled("no errors in the last " + HAPPENED_SECONDS / 60 + "m", Theme.success())); + watch.add(Span.styled(" · " + count + " before", style)); + } else { + watch.add(Span.styled("✗ " + count, style)); + } // the kinds that happened most, when there are more than one List<IntegrationInfo.ErrorKind> kinds = info.errorKinds; if (kinds.size() > 1) { for (int i = 0; i < Math.min(3, kinds.size()); i++) { IntegrationInfo.ErrorKind k = kinds.get(i); watch.add(Span.styled(i == 0 ? ": " : " · ", Theme.muted())); - watch.add(Span.styled(simpleName(k.exceptionType()) + " ×" + k.count(), Theme.error())); + watch.add(Span.styled(simpleName(k.exceptionType()) + " ×" + k.count(), style)); } if (kinds.size() > 3) { watch.add(Span.styled(" …", Theme.muted())); } } else if (kinds.size() == 1) { watch.add(Span.styled(": ", Theme.muted())); - watch.add(Span.styled(simpleName(kinds.get(0).exceptionType()), Theme.error())); + watch.add(Span.styled(simpleName(kinds.get(0).exceptionType()), style)); + } + if (lastError > 0) { + watch.add(Span.styled(" · the last " + TimeUtils.printSince(lastError) + " ago", Theme.muted())); } // the key to the Errors tab, as the Next line has its keys (a bare 9 read as a second count of errors) watch.add(Span.raw(" ")); @@ -1068,6 +1099,18 @@ final class AboutView { return lines; } + /** + * When the newest error happened (epoch millis), 0 when not known: as the integration says it (Camel 4.24), else + * from the errors the Errors tab loaded. + */ + private static long lastError(IntegrationInfo info) { + long last = info.lastErrorTimestamp; + for (ErrorInfo e : info.errors) { + last = Math.max(last, e.timestamp); + } + return last; + } + private static String simpleName(String type) { if (type == null) { return "error"; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java index 9df5ad327f7b..79d4e85762b4 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java @@ -116,6 +116,8 @@ class IntegrationInfo { final List<KafkaConsumerInfo> kafkaConsumers = new ArrayList<>(); // the errors that happened (the registry keeps only a few entries of each kind), and the kinds that happened most int errorCount; + // when the newest error happened (epoch millis), 0 when the integration does not say + long lastErrorTimestamp; volatile List<ErrorKind> errorKinds = List.of(); volatile List<ErrorInfo> errors = new ArrayList<>(); boolean inflightBrowseEnabled; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTab.java index 25ec16d59033..11a43c2c71de 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTab.java @@ -58,21 +58,29 @@ class ProducersTab extends AbstractTableTab { List<ProducerInfo> sorted = new ArrayList<>(info.producers); sorted.sort(this::sortProducer); + boolean percentiles = sorted.stream().map(pi -> stats(info, pi)) + .anyMatch(st -> st != null && st.busiest().p50Time >= 0); + List<Row> rows = new ArrayList<>(); for (ProducerInfo pi : sorted) { String type = producerType(pi); Style statusStyle = "Started".equals(pi.state) ? Theme.success() : Theme.error(); + Stats st = stats(info, pi); rows.add(Row.from( Cell.from(Span.styled(" " + (pi.routeId != null ? pi.routeId : ""), Style.EMPTY.fg(Theme.accent()))), Cell.from(Span.styled(pi.state != null ? pi.state : "", statusStyle)), Cell.from(type), Cell.from(pi.remote ? "x" : ""), + rightCell(st != null ? String.valueOf(st.total()) : "", 10), + st != null && st.failed() > 0 + ? rightCell(String.valueOf(st.failed()), 8, Theme.error()) : rightCell("", 8), + rightCell(st != null ? timing(st.busiest(), percentiles) : "", 16), Cell.from(pi.uri != null ? pi.uri : ""))); } if (rows.isEmpty()) { - rows.add(emptyRow("No producers", 5)); + rows.add(emptyRow("No producers", 8)); } Table table = Table.builder() @@ -82,12 +90,18 @@ class ProducersTab extends AbstractTableTab { Cell.from(Span.styled(sortLabel("STATUS", "status"), sortStyle("status"))), Cell.from(Span.styled(sortLabel("TYPE", "type"), sortStyle("type"))), Cell.from(Span.styled("REMOTE", Style.EMPTY.bold())), + rightCell("TOTAL", 10, Style.EMPTY.bold()), + rightCell("FAIL", 8, Style.EMPTY.bold()), + rightCell(percentiles ? "P50/P95/P99" : "MIN/MAX/MEAN", 16, Style.EMPTY.bold()), Cell.from(Span.styled(sortLabel("URI", "uri"), sortStyle("uri"))))) .widths( Constraint.length(20), Constraint.length(10), Constraint.length(20), Constraint.length(8), + Constraint.length(10), + Constraint.length(8), + Constraint.length(16), Constraint.fill()) .highlightStyle(Theme.selectionBg()) .highlightSpacing(Table.HighlightSpacing.ALWAYS) @@ -126,6 +140,59 @@ class ProducersTab extends AbstractTableTab { return sortReversed ? -result : result; } + /** + * What a producer sent: the statistics of the steps of its route that send to its endpoint (to, wireTap, enrich), + * and the busiest of them for the timings. Null when no step does, such as a toD whose endpoint is computed. + */ + record Stats(long total, long failed, ProcessorInfo busiest) { + } + + static Stats stats(IntegrationInfo info, ProducerInfo pi) { + if (pi.routeId == null || pi.uri == null) { + return null; + } + String uri = normalize(pi.uri); + long total = 0; + long failed = 0; + ProcessorInfo busiest = null; + for (RouteInfo route : info.routes) { + if (!pi.routeId.equals(route.routeId)) { + continue; + } + for (ProcessorInfo step : route.processors) { + if (step.uri != null && uri.equals(normalize(step.uri))) { + total += step.total; + failed += step.failed; + if (busiest == null || step.total > busiest.total) { + busiest = step; + } + } + } + } + return busiest != null ? new Stats(total, failed, busiest) : null; + } + + /** The same endpoint with or without the // after the scheme (direct:a and direct://a). */ + private static String normalize(String uri) { + int colon = uri.indexOf(':'); + if (colon > 0 && !uri.startsWith("//", colon + 1)) { + return uri.substring(0, colon) + "://" + uri.substring(colon + 1); + } + return uri; + } + + private static String timing(ProcessorInfo step, boolean percentiles) { + if (percentiles && step.p50Time >= 0) { + return formatDurationMs(step.p50Time) + "/" + formatDurationMs(step.p95Time) + "/" + + formatDurationMs(step.p99Time); + } + if (step.total > 0) { + return formatDurationMs(step.minTime) + "/" + formatDurationMs(step.maxTime) + "/" + + formatDurationMs(step.meanTime); + } + return ""; + } + private static String producerType(ProducerInfo pi) { if (pi.className == null) { return ""; @@ -183,6 +250,19 @@ class ProducersTab extends AbstractTableTab { if (pi.stepId != null) { row.put("stepId", pi.stepId); } + Stats st = stats(info, pi); + if (st != null) { + row.put("total", st.total()); + row.put("failed", st.failed()); + ProcessorInfo b = st.busiest(); + if (b.p50Time >= 0) { + row.put("p50Time", b.p50Time); + row.put("p95Time", b.p95Time); + row.put("p99Time", b.p99Time); + } else { + row.put("meanTime", b.meanTime); + } + } rows.add(row); } result.put("rows", rows); 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 99076064e21b..c2f017e964fc 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 @@ -128,6 +128,7 @@ final class StatusParser { } } info.errorKinds = List.copyOf(kinds); + info.lastErrorTimestamp = errorsObj.getLongOrDefault("lastTimestamp", 0L); } parseInflight(info, root); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md index 8dcd6594f3c9..40759767756d 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md @@ -28,8 +28,10 @@ happened to it, with no AI: - **Last 5m** — the exchanges and how they ended, the cause that happened most and when (a chaos rule marked 🎲), and the exchanges since it started. -- **Watch** — the errors that happened and the kinds that happened most, - routes not started, exchanges in flight. +- **Watch** — the errors that happened, the kinds that happened most and + when the last one did, routes not started, exchanges in flight. Errors + that stopped more than 5 minutes ago are history: the line says there + were none in the last 5 minutes, with the earlier ones dimmed. - **Next** — the keys that lead deeper: **4** the Timeline, **5** the Diagram, **2** the source. - **What happened** — a strip per route over the last 5 minutes (as wide diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/producers.md b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/producers.md index 17c0d8086f69..f709f0ed640f 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/producers.md +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/producers.md @@ -11,9 +11,16 @@ Unlike consumers (one per route), a route can have multiple producers - **ROUTE** — The route this producer belongs to - **STATUS** — Producer state: `Started` (running normally) or `Stopped` - **TYPE** — The Camel component type (e.g., `Kafka`, `Http`, `Log`, `Seda`) -- **REMOTE** — Whether this producer sends to a remote system (`Yes`) or is in-process (`No`) +- **REMOTE** — `x` when this producer sends to a remote system, blank when it is in-process +- **TOTAL** — The messages sent: the exchanges of the steps of the route that send to this endpoint (`to`, `wireTap`, `enrich`) +- **FAIL** — How many of those sends failed +- **P50/P95/P99** — How long a send took, from the busiest of those steps (`MIN/MAX/MEAN` when the integration does not record percentiles) - **URI** — The full endpoint URI (e.g., `kafka://my-topic`, `log://mylogger`) +The numbers are blank for a producer no step of its route sends to by +that URI, such as the one of a `toD` whose endpoint is computed per +message. + ## Keys - `Up/Down` — select producer 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 a2b942acd9f6..87213d277a56 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 @@ -112,8 +112,8 @@ class AboutViewTest { // the outage was 4.5 minutes ago, for 15 seconds ctx.timeline.merge("1234", TimelineTabRenderTest.outage(end, 300, 0).json(), true); AboutView about = new AboutView(ctx); - // 160 wide has room for 75 columns of 4 seconds - assertThat(render(about, info)).contains("What happened ─ last 5m, a column is 4s ─ + zoom in, 4 for each"); + // 160 wide has room for 100 columns of 3 seconds next to the notes of the route + assertThat(render(about, info)).contains("What happened ─ last 5m, a column is 3s ─ + zoom in, 4 for each"); assertThat(about.handleKeyEvent(KeyEvent.ofChar('+'), info)).isTrue(); // 2 minutes from the first second recorded, with the outage in it @@ -132,15 +132,60 @@ class AboutViewTest { @Test void theStripsAreAsWideAsTheRoomAllows() { - // a whole number of seconds in a column, at least 60 columns and at most 150 + // a whole number of seconds in a column, at least 60 columns and at most 300 assertThat(AboutView.stripColumns(300, 40)).isEqualTo(60); assertThat(AboutView.stripColumns(300, 94)).isEqualTo(75); assertThat(AboutView.stripColumns(300, 129)).isEqualTo(100); - assertThat(AboutView.stripColumns(300, 500)).isEqualTo(150); + assertThat(AboutView.stripColumns(300, 160)).isEqualTo(150); + assertThat(AboutView.stripColumns(300, 500)).isEqualTo(300); assertThat(AboutView.stripColumns(120, 129)).isEqualTo(120); assertThat(AboutView.stripColumns(60, 500)).isEqualTo(60); } + @Test + void theStripsTakeTheRoomTheNotesLeave() { + IntegrationInfo info = new IntegrationInfo(); + info.pid = "1234"; + RouteInfo r = new RouteInfo(); + r.routeId = "stock-check"; + info.routes.add(r); + MonitorContext ctx = new MonitorContext(new AtomicReference<>(List.of(info)), new AtomicReference<>(List.of())); + ctx.timeline.merge("1234", TimelineTabRenderTest.outage(T0 / 1000 + 299, 300, 0).json(), true); + + // a 200 wide screen: the notes after a strip are short, so the strips get 150 columns of 2 seconds, not 100 + assertThat(render(new AboutView(ctx), info, 200, 40)).contains("last 5m, a column is 2s"); + } + + @Test + void watchCalmsDownWhenTheErrorsStopped() { + IntegrationInfo info = new IntegrationInfo(); + info.pid = "1234"; + info.errorCount = 13; + info.errorKinds = List.of( + new IntegrationInfo.ErrorKind( + "stock-check", "io.github.resilience4j.circuitbreaker.CallNotPermittedException", 10), + new IntegrationInfo.ErrorKind("supplier", "java.net.ConnectException", 3)); + ErrorInfo last = new ErrorInfo(); + info.errors.add(last); + MonitorContext ctx = new MonitorContext(new AtomicReference<>(List.of(info)), new AtomicReference<>(List.of())); + + // the last error 20 seconds ago: still something to watch + last.timestamp = System.currentTimeMillis() - 20_000; + assertThat(render(new AboutView(ctx), info)) + .contains("✗ 13 errors: CallNotPermittedException ×10 · ConnectException ×3 · the last 20s ago"); + + // an hour ago: none in the last minutes, the ones before for the record + last.timestamp = System.currentTimeMillis() - 3_600_000; + String rendered = render(new AboutView(ctx), info); + assertThat(rendered).contains("no errors in the last 5m · 13 errors before: CallNotPermittedException ×10") + .contains("the last 1h0m ago").contains("9 show errors").doesNotContain("✗"); + + // the integration says when the newest error happened, without the Errors tab loading them + info.errors = List.of(); + info.lastErrorTimestamp = System.currentTimeMillis() - 20_000; + assertThat(render(new AboutView(ctx), info)).contains("✗ 13 errors").contains("the last 20s ago"); + } + @Test void aTallScreenGivesTheSectionsRoom() { IntegrationInfo info = new IntegrationInfo(); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTabStatsTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTabStatsTest.java new file mode 100644 index 000000000000..3b9decf36018 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTabStatsTest.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.tui; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * A producer has the numbers of the steps of its route that send to its endpoint. + */ +class ProducersTabStatsTest { + + private IntegrationInfo info; + private MonitorContext ctx; + + @BeforeEach + void setUp() { + Theme.resetForTesting(); + info = new IntegrationInfo(); + info.pid = "1234"; + info.name = "shop"; + ctx = new MonitorContext(new AtomicReference<>(List.of(info)), new AtomicReference<>(List.of())); + ctx.selectedPid = "1234"; + + RouteInfo route = new RouteInfo(); + route.routeId = "stock-check"; + // two sends to the supplier, written without the // after the scheme + route.processors.add(step("to1", "direct:supplier", 100, 3, 1, 4, 9)); + route.processors.add(step("to2", "direct:supplier", 20, 0, 2, 5, 7)); + route.processors.add(step("log1", null, 120, 0, 0, 0, 0)); + info.routes.add(route); + } + + @Test + void theStepsThatSendToTheEndpointAddUp() { + ProducersTab.Stats st = ProducersTab.stats(info, producer("stock-check", "direct://supplier")); + + assertThat(st.total()).isEqualTo(120); + assertThat(st.failed()).isEqualTo(3); + assertThat(st.busiest().id).isEqualTo("to1"); + } + + @Test + void aProducerNoStepSendsToHasNoNumbers() { + // a toD: the endpoint is computed per message + assertThat(ProducersTab.stats(info, producer("stock-check", "http://warehouse/stock/42"))).isNull(); + // another route + assertThat(ProducersTab.stats(info, producer("orders", "direct://supplier"))).isNull(); + } + + @Test + void theTableShowsTheNumbers() { + info.producers.add(producer("stock-check", "direct://supplier")); + + String rendered = TuiTestHelper.renderToString(new ProducersTab(ctx), 160, 10); + + assertThat(rendered).contains("TOTAL").contains("FAIL").contains("P50/P95/P99"); + assertThat(rendered).containsPattern("120 +3 +1ms/4ms/9ms +direct://supplier"); + } + + private static ProcessorInfo step(String id, String uri, long total, long failed, long p50, long p95, long p99) { + ProcessorInfo p = new ProcessorInfo(); + p.id = id; + p.uri = uri; + p.total = total; + p.failed = failed; + p.p50Time = p50; + p.p95Time = p95; + p.p99Time = p99; + return p; + } + + private static ProducerInfo producer(String routeId, String uri) { + ProducerInfo p = new ProducerInfo(); + p.routeId = routeId; + p.uri = uri; + p.state = "Started"; + p.className = "org.apache.camel.component.direct.DirectProducer"; + return p; + } +}
