This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new b063abca876b chore: camel-jbang - Log tab: c switches between compact,
wide and wide collapsed views (#29426)
b063abca876b is described below
commit b063abca876b23a7a35a942df1187fdff48b47f6
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Nov 8 21:16:30 2026 +0100
chore: camel-jbang - Log tab: c switches between compact, wide and wide
collapsed views (#29426)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../ROOT/pages/camel-jbang-tui-observe.adoc | 9 ++
.../dsl/jbang/core/commands/tui/LogEntry.java | 12 +++
.../camel/dsl/jbang/core/commands/tui/LogTab.java | 104 ++++++++++++++++++---
.../jbang/core/commands/tui/LogTabRenderTest.java | 62 ++++++++++++
4 files changed, 173 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 81f3e7c03a43..0124c1064b6b 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
@@ -197,6 +197,15 @@ listed once.
image::jbang/camel-tui-log-fix-with-ai-pick.png[Shift+F8 in the Log tab with
two different ERRORs on the screen]
+The Log tab shows the log in one of three views, and *c* goes to the next one:
+
+* *compact* (the default): the time, level, logger and message of each line,
as the date, pid and thread take most of
+a row in a terminal. The same line logged again and again is one line with a
count, such as `Hello World (x17)`.
+* *wide*: every line as it was logged, one by one, with the date, pid and
thread.
+* *wide, collapsed*: the lines as they were logged, with the repeats as one
line with a count.
+
+The line at the top of the screen stays there when you change the view.
+
image::jbang/camel-tui-observe-error-diagram.png[A failed order: the path from
checkout to payment-provider, and the onException handler that parked it]
== OpenTelemetry Spans
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogEntry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogEntry.java
index c051c50b45b9..220d3e097a49 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogEntry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogEntry.java
@@ -24,4 +24,16 @@ class LogEntry {
String message = "";
/** How many identical lines in a row this entry stands for (CAMEL-24911);
1 for a line that happened once. */
int repeat = 1;
+ /** In a collapsed view: the index of the first line it stands for in the
lines read, else -1. */
+ int line = -1;
+
+ LogEntry copy() {
+ LogEntry answer = new LogEntry();
+ answer.raw = raw;
+ answer.time = time;
+ answer.level = level;
+ answer.logger = logger;
+ answer.message = message;
+ return answer;
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
index 165b6385ba07..f9d7e3b47a11 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
@@ -62,7 +62,8 @@ import static
org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
class LogTab extends AbstractTab {
- private static final int MAX_LOG_LINES = 3000;
+ // the lines read, one by one: the collapsed views fold them when they are
shown
+ private static final int MAX_LOG_LINES = 10000;
private static final int MOUSE_SCROLL_LINES = 3;
private static final String[] LOG_LEVELS = { "ERROR", "WARN", "INFO",
"DEBUG", "TRACE" };
@@ -94,8 +95,7 @@ class LogTab extends AbstractTab {
private long evictedSeen;
private boolean followMode = true;
private boolean wordWrap = true;
- // time, level, logger and message only: the date, pid and thread take
most of a row in a terminal
- private boolean compact = true;
+ private View view = View.COMPACT;
private boolean cachedCompact;
private int hScroll;
private boolean showLogLevelPopup;
@@ -214,8 +214,7 @@ class LogTab extends AbstractTab {
return true;
}
if (ke.isCharIgnoreCase('c')) {
- compact = !compact;
- hScroll = 0;
+ switchView(view.next());
return true;
}
if (!wordWrap) {
@@ -384,17 +383,17 @@ class LogTab extends AbstractTab {
int hSkip = wordWrap ? 0 : hScroll;
boolean entriesChanged = entries != cachedLogEntries;
- if (entriesChanged || hSkip != cachedLogHSkip || compact !=
cachedCompact) {
+ if (entriesChanged || hSkip != cachedLogHSkip || view.compact !=
cachedCompact) {
cachedLogEntries = entries;
cachedLogHSkip = hSkip;
- cachedCompact = compact;
+ cachedCompact = view.compact;
List<Line> built = new ArrayList<>(entries.size());
int maxW = 0;
for (int i = 0; i < entries.size(); i++) {
LogEntry entry = entries.get(i);
String raw = entry.raw != null ? entry.raw : "";
Line line;
- if (compact && !entry.time.isEmpty()) {
+ if (view.compact && !entry.time.isEmpty()) {
line = compactLine(entry);
if (!wordWrap) {
maxW = Math.max(maxW, line.width());
@@ -505,7 +504,7 @@ class LogTab extends AbstractTab {
}
search.renderSearchHints(spans);
hint(spans, "w", "wrap" + (wordWrap ? " [on]" : " [off]"));
- hint(spans, "c", "compact" + (compact ? " [on]" : " [off]"));
+ hint(spans, "c", "view [" + view.label + "]");
if (!ctx.isInfraSelected()) {
hint(spans, "l", "level");
}
@@ -628,7 +627,9 @@ class LogTab extends AbstractTab {
/** The entries of the tab, as read from the log file (tests). */
void setEntriesForTesting(List<LogEntry> entries) {
- filteredLogEntries = new ArrayList<>(entries);
+ mutableFilteredEntries.clear();
+ mutableFilteredEntries.addAll(entries);
+ filteredLogEntries = view.entries(mutableFilteredEntries);
logLoading = false;
}
@@ -670,6 +671,7 @@ class LogTab extends AbstractTab {
logLoading = true;
}
boolean changed = false;
+ int olderRead = 0;
boolean loadAll = loadAllRequested;
if (logFileStartPos > 0
&& (loadAll || (!followMode && scroll == 0))) {
@@ -680,11 +682,11 @@ class LogTab extends AbstractTab {
changed = true;
List<LogEntry> olderEntries = new ArrayList<>();
for (String line : olderLines) {
- addFolded(olderEntries, parseLogLine(line));
+ olderEntries.add(parseLogLine(line));
}
mutableFilteredEntries.addAll(0, olderEntries);
logTotalLinesRead += olderLines.size();
- scroll = olderEntries.size();
+ olderRead = olderEntries.size();
}
}
List<String> newRawLines = new ArrayList<>();
@@ -693,7 +695,7 @@ class LogTab extends AbstractTab {
if (changed) {
logTotalLinesRead += newRawLines.size();
for (String line : newRawLines) {
- addFolded(mutableFilteredEntries, parseLogLine(line));
+ mutableFilteredEntries.add(parseLogLine(line));
}
if (mutableFilteredEntries.size() > MAX_LOG_LINES) {
mutableFilteredEntries.subList(0,
mutableFilteredEntries.size() - MAX_LOG_LINES)
@@ -701,7 +703,11 @@ class LogTab extends AbstractTab {
}
}
if (changed || logLoading) {
- filteredLogEntries = new ArrayList<>(mutableFilteredEntries);
+ filteredLogEntries = view.entries(mutableFilteredEntries);
+ if (olderRead > 0) {
+ // the older lines go above: keep the line that was at the top
of the screen there
+ scroll = shownIndexOf(filteredLogEntries, olderRead,
view.collapse);
+ }
}
logLoading = false;
}
@@ -919,12 +925,82 @@ class LogTab extends AbstractTab {
&& !entry.message.isEmpty()) {
// keep the newest line, so its timestamp is the last time this
happened
entry.repeat = last.repeat + 1;
+ entry.line = last.line;
entries.set(entries.size() - 1, entry);
} else {
entries.add(entry);
}
}
+ /** The lines read, folded as {@link #addFolded}: copies, so the lines
themselves stay one by one. */
+ static List<LogEntry> fold(List<LogEntry> lines) {
+ List<LogEntry> answer = new ArrayList<>();
+ for (int i = 0; i < lines.size(); i++) {
+ LogEntry entry = lines.get(i).copy();
+ entry.line = i;
+ addFolded(answer, entry);
+ }
+ return answer;
+ }
+
+ /** The index in the shown entries of the line read at {@code line}: the
entry it is folded into, if collapsed. */
+ static int shownIndexOf(List<LogEntry> shown, int line, boolean collapsed)
{
+ if (!collapsed) {
+ return Math.min(line, Math.max(0, shown.size() - 1));
+ }
+ int answer = 0;
+ for (int i = 0; i < shown.size() && shown.get(i).line <= line; i++) {
+ answer = i;
+ }
+ return answer;
+ }
+
+ /** The next view (c): the line at the top of the screen stays there. */
+ void switchView(View next) {
+ List<LogEntry> shown = filteredLogEntries;
+ int top = scroll < shown.size() ? scroll : Math.max(0, shown.size() -
1);
+ int line = view.collapse && !shown.isEmpty() ? Math.max(0,
shown.get(top).line) : top;
+ view = next;
+ hScroll = 0;
+ if (!mutableFilteredEntries.isEmpty()) {
+ filteredLogEntries = view.entries(mutableFilteredEntries);
+ scroll = shownIndexOf(filteredLogEntries, line, view.collapse);
+ }
+ }
+
+ View view() {
+ return view;
+ }
+
+ /**
+ * How the log is shown (c): compact is the time, level, logger and
message, as the date, pid and thread take most
+ * of a row in a terminal, with the same line in a row collapsed into one
with a count (CAMEL-24911); wide is the
+ * line as logged, one by one; wide and collapsed is the line as logged,
with the repeats collapsed.
+ */
+ enum View {
+ COMPACT("compact", true, true),
+ WIDE("wide", false, false),
+ WIDE_COLLAPSED("wide, collapsed", false, true);
+
+ final String label;
+ final boolean compact;
+ final boolean collapse;
+
+ View(String label, boolean compact, boolean collapse) {
+ this.label = label;
+ this.compact = compact;
+ this.collapse = collapse;
+ }
+
+ View next() {
+ return values()[(ordinal() + 1) % values().length];
+ }
+
+ List<LogEntry> entries(List<LogEntry> lines) {
+ return collapse ? fold(lines) : new ArrayList<>(lines);
+ }
+ }
+
static LogEntry parseLogLine(String line) {
LogEntry entry = new LogEntry();
entry.raw = line;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabRenderTest.java
index 34c7851a80a8..64e020e845a6 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabRenderTest.java
@@ -31,6 +31,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -73,6 +74,67 @@ class LogTabRenderTest {
assertEquals(1, entries.get(0).repeat, "a line that happened once is
not a repeat");
}
+ @Test
+ void cSwitchesBetweenCompactWideAndWideCollapsed() {
+ List<LogEntry> entries = new ArrayList<>();
+ for (int i = 0; i < 17; i++) {
+ entries.add(LogTab.parseLogLine("2026-11-08 10:00:" +
String.format("%02d", i)
+ + ".000 INFO 42 --- [ -
timer://java] route1 : Hello World"));
+ }
+ LogTab tab = new LogTab(ctx);
+ tab.setEntriesForTesting(entries);
+
+ // compact: the short heading, and the same line in a row is one line
with a count
+ assertEquals(LogTab.View.COMPACT, tab.view());
+ String compact = TuiTestHelper.renderToString(tab, 140, 30);
+ assertTrue(compact.contains("10:00:16.000 INFO route1") &&
compact.contains("Hello World (x17)"), compact);
+ assertTrue(footer(tab).contains("view [compact]"), footer(tab));
+
+ // wide: every line as logged, one by one
+ tab.handleKeyEvent(KeyEvent.ofChar('c'));
+ String wide = TuiTestHelper.renderToString(tab, 140, 30);
+ assertTrue(wide.contains("2026-11-08 10:00:00.000 INFO 42 --- [ -
timer://java] route1 : Hello World"), wide);
+ assertTrue(wide.contains("2026-11-08 10:00:16.000"), wide);
+ assertFalse(wide.contains("(x"), wide);
+ assertTrue(footer(tab).contains("view [wide]"), footer(tab));
+
+ // wide and collapsed: the line as logged, with the repeats collapsed
+ tab.handleKeyEvent(KeyEvent.ofChar('c'));
+ String collapsed = TuiTestHelper.renderToString(tab, 140, 30);
+ assertTrue(collapsed.contains("2026-11-08 10:00:16.000 INFO 42 --- [
- timer://java] route1 : Hello World (x17)"),
+ collapsed);
+ assertFalse(collapsed.contains("2026-11-08 10:00:00.000"), collapsed);
+ assertTrue(footer(tab).contains("view [wide, collapsed]"),
footer(tab));
+
+ tab.handleKeyEvent(KeyEvent.ofChar('c'));
+ assertEquals(LogTab.View.COMPACT, tab.view());
+ }
+
+ private static String footer(LogTab tab) {
+ List<Span> spans = new ArrayList<>();
+ tab.renderFooter(spans);
+ return spans.stream().map(Span::content).reduce("", String::concat);
+ }
+
+ @Test
+ void switchingTheViewKeepsTheLineAtTheTop() {
+ List<LogEntry> lines = new ArrayList<>();
+ lines.add(LogTab.parseLogLine("2026-11-08 10:00:00.000 INFO 42 ---
[main] route1 : Started"));
+ for (int i = 0; i < 5; i++) {
+ lines.add(LogTab.parseLogLine("2026-11-08 10:00:0" + (i + 1) +
".000 INFO 42 --- [main] route1 : Hello"));
+ }
+ lines.add(LogTab.parseLogLine("2026-11-08 10:00:07.000 INFO 42 ---
[main] route1 : Done"));
+ List<LogEntry> shown = LogTab.fold(lines);
+
+ assertEquals(3, shown.size());
+ assertEquals(1, shown.get(1).line, "the folded entry starts at the
first of its lines");
+ assertEquals(1, lines.get(3).repeat, "the lines read stay one by one");
+ // line 4 (a Hello) is in the folded entry 1, and line 6 (Done) is
entry 2
+ assertEquals(1, LogTab.shownIndexOf(shown, 4, true));
+ assertEquals(2, LogTab.shownIndexOf(shown, 6, true));
+ assertEquals(4, LogTab.shownIndexOf(lines, 4, false));
+ }
+
@Test
void theCompactViewShowsTimeLevelLoggerAndMessage() {
LogEntry entry = LogTab.parseLogLine("2026-10-01 17:24:30.257 INFO
85013 --- [ntloop-thread-0]"