This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26691-timeline-tui in repository https://gitbox.apache.org/repos/asf/camel.git
commit 68dfc4d184c0796bb185b13ed36f948588ecbbe6 Author: Claus Ibsen <[email protected]> AuthorDate: Fri Nov 6 23:29:41 2026 +0100 CAMEL-26691: timeline - a second is wider when the screen allows, and the docs Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../ROOT/pages/camel-jbang-tui-observe.adoc | 22 +++++++++++ .../modules/ROOT/pages/camel-jbang-tui.adoc | 2 +- .../dsl/jbang/core/commands/tui/TimelineTab.java | 45 +++++++++++++++------- 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc index cc4a7d617e96..b7d9d39f3456 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc @@ -240,6 +240,28 @@ right panel. While sending, the status shows a spinner. Beyond the core tabs described above, the TUI includes several specialized screens worth highlighting. +=== Timeline + +The Timeline tab (under More > Observability) shows the last minute of the selected integration on one time axis, one +column per second: what went wrong on top, and what it did to the routes under it. Read from left to right, it tells +the story of an outage: the stock service answered 503 for fifteen seconds, the orders went to the dead letter channel, +shipping had nothing to ship, and everything recovered by itself. + +image::jbang/camel-tui-timeline.png[The Timeline tab in an outage of the stock service: the errors per second on top, the routes under it, and the orders route in detail] + +* *Errors/s* stacks the errors per second by the kind of exception, with the HTTP status when there is one. With +camel-chaos, the faults of each chaos rule are a layer of their own, marked 🎲, so an injected fault and a real one stay +apart. When nothing went wrong the band is one line. +* *Routes* has a line per route. The height is the exchanges done that second, and the colour the worst that happened to +them: completed, recovered by redelivery, handled by the error handler (such as the dead letter channel), or failed. A +second a chaos rule had the route down is shaded. +* The selected route is shown in detail: its outcomes stacked per second, its mean processing time and its exchanges in +flight. The screen opens on the route that had the worst time. + +*w* switches the window between 1, 2 and 5 minutes, *Space* pauses so a moment does not scroll away (*Left* and *Right* +then go back and forth), and *Shift+F8* asks the AI to explain what happened, as a story of cause and effect. The +timeline is recorded while the TUI watches the integration, so it opens with the last minutes, not an empty chart. + === Chaos With xref:next@components:others:chaos.adoc[camel-chaos] (`camel run --chaos`), the Chaos tab (under More > diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc index 44efe2a3de04..b2e6ca9c4152 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc @@ -80,7 +80,7 @@ to any tab, or *Shift+F2* to go to a tab by name. The *More* menu (key *0*) opens a popup with tabs organized into groups: * *Routing* -- Browse Endpoints, Consumers, HTTP, Inflight, Producers, Route Controller -* *Observability* -- Chaos, Circuit Breaker, Health, JFR, Metrics, Network Services, Exchange Events, Recovery Tasks, OpenTelemetry Spans +* *Observability* -- Timeline, Chaos, Circuit Breaker, Health, JFR, Metrics, Network Services, Exchange Events, Recovery Tasks, OpenTelemetry Spans * *AI* -- Semantic (definitions, expert contracts, relationships, sample evaluation, Audit and Monitoring), Ollama (listed when an Ollama server is detected) * *Data* -- JDBC DataSource, Kafka, Secrets, SQL Query, SQL Trace * *JVM* -- Classpath, Heap Memory Histogram, Memory Usage, Memory Leak, Process, Startup, Threads diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java index ebe7474408d6..ec78fcb0fe1e 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java @@ -60,6 +60,9 @@ class TimelineTab extends AbstractTableTab { private static final int MAX_LABEL = 18; private final LongSupplier clock; + // the seconds a column has, and the characters a column is wide, as the last render worked them out + private int per = 1; + private int width = 1; private int window; // the second the screen stays at while paused, or -1 to follow now private long pausedAt = -1; @@ -220,9 +223,12 @@ class TimelineTab extends AbstractTableTab { int inner = Math.max(20, area.width() - 2); int label = Math.min(MAX_LABEL, Math.max(10, routes.stream().mapToInt(r -> r.route().length()).max().orElse(0))) + 3; - int columns = Math.max(10, Math.min(seconds(), inner - label - SUFFIX - 1)); - int per = (int) Math.ceil((double) seconds() / columns); - columns = (seconds() + per - 1) / per; + // a second is 1 to 3 characters wide as the screen allows, or a character is several seconds + int room = Math.max(10, inner - label - SUFFIX - 1); + int per = room >= seconds() ? 1 : (int) Math.ceil((double) seconds() / room); + width = per == 1 ? Math.max(1, Math.min(3, room / seconds())) : 1; + this.per = per; + int columns = (seconds() + per - 1) / per * width; List<Line> lines = new ArrayList<>(); renderCauses(lines, causes, anyCause, info, columns, per, label); @@ -278,7 +284,7 @@ class TimelineTab extends AbstractTableTab { continue; } Style style = causeStyle(colour++); - values.add(columnsOf(c.values(), per, false)); + values.add(cols(c.values(), false)); styles.add(style); legend.add(Span.styled("█ ", style)); legend.add(Span.raw((c.rule() != null ? "🎲 " : "") + c.label() + " ")); @@ -311,11 +317,11 @@ class TimelineTab extends AbstractTableTab { spans.add(selected ? Span.styled(pad(">> " + name, label), Theme.selectionBg()) : Span.raw(pad(" " + name, label))); - long[] completed = columnsOf(r.completed(), per, false); - long[] recovered = columnsOf(r.recovered(), per, false); - long[] handled = columnsOf(r.handled(), per, false); - long[] failed = columnsOf(r.failed(), per, false); - long[] down = columnsOf(r.down(), per, true); + long[] completed = cols(r.completed(), false); + long[] recovered = cols(r.recovered(), false); + long[] handled = cols(r.handled(), false); + long[] failed = cols(r.failed(), false); + long[] down = cols(r.down(), true); long max = 1; for (int c = 0; c < columns; c++) { max = Math.max(max, total(completed[c], recovered[c], handled[c], failed[c])); @@ -422,13 +428,13 @@ class TimelineTab extends AbstractTableTab { .block(block(" Route ")).build(), area); return; } - List<long[]> values = List.of(columnsOf(r.completed(), per, false), columnsOf(r.recovered(), per, false), - columnsOf(r.handled(), per, false), columnsOf(r.failed(), per, false)); + List<long[]> values = List.of(cols(r.completed(), false), cols(r.recovered(), false), + cols(r.handled(), false), cols(r.failed(), false)); List<Style> styles = List.of(Theme.success(), Theme.warning(), handledStyle(), Theme.error()); int rows = Math.max(2, Math.min(DETAIL_ROWS, area.height() - 5)); lines.addAll(stacked(values, styles, rows, columns, label)); - long[] mean = columnsOf(r.mean(), per, true); + long[] mean = cols(r.mean(), true); long maxMean = 0; long sumMean = 0; int n = 0; @@ -450,7 +456,7 @@ class TimelineTab extends AbstractTableTab { : "no exchange"))); lines.add(Line.from(meanLine)); - long[] inflight = columnsOf(r.inflight(), per, true); + long[] inflight = cols(r.inflight(), true); long maxInflight = 0; long last = 0; for (long v : r.inflight()) { @@ -544,6 +550,19 @@ class TimelineTab extends AbstractTableTab { return sb.toString(); } + /** The seconds as the columns of the chart: several seconds a column, or a second several characters wide. */ + private long[] cols(long[] seconds, boolean highest) { + long[] columns = columnsOf(seconds, per, highest); + if (width == 1) { + return columns; + } + long[] wide = new long[columns.length * width]; + for (int i = 0; i < wide.length; i++) { + wide[i] = columns[i / width]; + } + return wide; + } + /** * The seconds as columns of several seconds each: the sum of the counts, or the highest value of a level (a mean, * the in flight count). A column with no known second is -1.
