This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch tui-log-follow-catch-up in repository https://gitbox.apache.org/repos/asf/camel.git
commit 6b9752f3398949768b1ad158dff5a757c3ef2abf Author: Claus Ibsen <[email protected]> AuthorDate: Sat Nov 14 20:15:44 2026 +0100 chore: camel-jbang - the Log tab shows the newest lines at once after a while on another tab The log is read while the Log tab is on screen (or the log is pinned), at most 64KB at a refresh. After a while on another tab, such as browsing the Diagram, a busy log had grown well beyond that, and the tab played the lines it had missed for many seconds while it said it followed; the same on Space after a pause on another tab. Following, a log more than a read behind now starts again at its end, as when the tab opens; Home reads the lines it skipped. Paused, it reads on from where it was. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../camel/dsl/jbang/core/commands/tui/LogTab.java | 42 ++++++++++++--- .../jbang/core/commands/tui/LogTabTailTest.java | 60 ++++++++++++++++++++++ 2 files changed, 95 insertions(+), 7 deletions(-) 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 fb01ffa8c93c..6365fecaaf0c 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 @@ -82,6 +82,8 @@ class LogTab extends AbstractTab { private volatile boolean loadAllRequested; /** How much of the end of a log the tab reads when it opens it. */ static final int INITIAL_TAIL_BYTES = 32 * 1024; + /** The most of a log read at a refresh, so a refresh does not block on a log that grew a lot. */ + static final int MAX_READ_BYTES = 64 * 1024; private long logFilePos = -1; private long logFileStartPos = -1; @@ -642,6 +644,11 @@ class LogTab extends AbstractTab { frame.renderStatefulWidget(list, popup, logLevelListState); } + /** The entries the tab shows (tests). */ + List<LogEntry> entriesForTesting() { + return filteredLogEntries; + } + /** The entries of the tab, as read from the log file (tests). */ void setEntriesForTesting(List<LogEntry> entries) { mutableFilteredEntries.clear(); @@ -680,6 +687,10 @@ class LogTab extends AbstractTab { } void refreshFromFile(String pid, String fileName) { + refreshFromFile(pid, CommandLineHelper.getCamelDir().resolve(fileName)); + } + + void refreshFromFile(String pid, Path logFile) { if (!pid.equals(logFilePid)) { mutableFilteredEntries.clear(); logFilePos = -1; @@ -688,13 +699,25 @@ class LogTab extends AbstractTab { logLoading = true; } boolean changed = false; + if (followMode && pid.equals(logFilePid) && logFilePos >= 0 && unread(logFile) > MAX_READ_BYTES) { + // far behind, as after a while on another tab (the log is read while it is on screen): it starts again at + // the end of the log, as when the tab opens, rather than play the lines it missed while it says it follows; + // Home reads them + mutableFilteredEntries.clear(); + logFilePos = -1; + logFileStartPos = -1; + logTotalLinesRead = 0; + evictedSeen = 0; + logLineBuffer.setLength(0); + changed = true; + } int olderRead = 0; boolean loadAll = loadAllRequested; if (logFileStartPos > 0 && (loadAll || (!followMode && scroll == 0))) { loadAllRequested = false; List<String> olderLines = new ArrayList<>(); - readOlderLogLines(fileName, loadAll, olderLines); + readOlderLogLines(logFile, loadAll, olderLines); if (!olderLines.isEmpty()) { changed = true; List<LogEntry> olderEntries = new ArrayList<>(); @@ -707,7 +730,7 @@ class LogTab extends AbstractTab { } } List<String> newRawLines = new ArrayList<>(); - readNewLogLinesFromFile(pid, fileName, newRawLines); + readNewLogLines(pid, logFile, newRawLines); changed |= !newRawLines.isEmpty(); if (changed) { logTotalLinesRead += newRawLines.size(); @@ -729,6 +752,15 @@ class LogTab extends AbstractTab { logLoading = false; } + /** How much of the log is written beyond what the tab read. */ + private long unread(Path logFile) { + try { + return Files.size(logFile) - logFilePos; + } catch (IOException e) { + return 0; + } + } + void readNewLogLines(String pid, List<String> newLines) { readNewLogLinesFromFile(pid, pid + ".log", newLines); } @@ -758,7 +790,7 @@ class LogTab extends AbstractTab { } raf.seek(logFilePos); // Cap per-tick read to 64KB to avoid blocking the refresh cycle - byte[] buf = new byte[(int) Math.min(length - logFilePos, 64 * 1024)]; + byte[] buf = new byte[(int) Math.min(length - logFilePos, MAX_READ_BYTES)]; raf.readFully(buf); int from = 0; if (skipPartialLine) { @@ -802,10 +834,6 @@ class LogTab extends AbstractTab { return -1; } - void readOlderLogLines(String fileName, boolean loadAll, List<String> olderLines) { - readOlderLogLines(CommandLineHelper.getCamelDir().resolve(fileName), loadAll, olderLines); - } - void readOlderLogLines(Path logFile, boolean loadAll, List<String> olderLines) { if (logFilePid == null || logFileStartPos <= 0) { return; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabTailTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabTailTest.java index bd030d623ae3..30398bcb0282 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabTailTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabTailTest.java @@ -19,10 +19,13 @@ package org.apache.camel.dsl.jbang.core.commands.tui; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import dev.tamboui.tui.event.KeyEvent; +import dev.tamboui.tui.event.KeyModifiers; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -66,6 +69,63 @@ class LogTabTailTest { assertThat(all).isEqualTo(written); } + @Test + void followingALogThatGrewALotWhileTheTabWasHiddenShowsItsNewestLinesAtOnce() throws Exception { + Path log = dir.resolve("1234.log"); + List<String> written = new ArrayList<>(); + append(log, written, 100); + LogTab tab = newTab(); + tab.refreshFromFile("1234", log); + + // another tab was shown: the log was not read while it grew + append(log, written, 4 * LogTab.MAX_READ_BYTES / 100); + tab.refreshFromFile("1234", log); + + List<LogEntry> entries = tab.entriesForTesting(); + assertThat(entries.get(entries.size() - 1).raw).as("the newest line, not one read since long ago") + .isEqualTo(written.get(written.size() - 1)); + + // the lines it skipped are still there for Home + List<String> older = new ArrayList<>(); + tab.readOlderLogLines(log, true, older); + List<String> all = new ArrayList<>(older); + entries.forEach(e -> all.add(e.raw)); + assertThat(all).isEqualTo(written); + } + + @Test + void pausedTheLogIsReadOnFromWhereItWas() throws Exception { + Path log = dir.resolve("1234.log"); + List<String> written = new ArrayList<>(); + append(log, written, 100); + LogTab tab = newTab(); + tab.refreshFromFile("1234", log); + tab.handleKeyEvent(KeyEvent.ofChar(' ', KeyModifiers.NONE)); + + append(log, written, 4 * LogTab.MAX_READ_BYTES / 100); + tab.refreshFromFile("1234", log); + + List<LogEntry> entries = tab.entriesForTesting(); + assertThat(entries.get(0).raw).as("the lines on screen stay").isEqualTo(written.get(0)); + assertThat(entries.size()).as("read on a part at a time").isLessThan(written.size()); + } + + private static LogTab newTab() { + return new LogTab(new MonitorContext(new AtomicReference<>(List.of()), new AtomicReference<>(List.of()))); + } + + /** Appends about {@code count} lines of a hundred bytes each. */ + private static void append(Path log, List<String> written, int count) throws Exception { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + String line = "2026-11-14 20:08:17.646 WARN 718778 --- [timer://orders] TimerConsumer : Error processing " + + written.size(); + written.add(line); + sb.append(line).append('\n'); + } + Files.writeString(log, sb, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND); + } + @Test void aShortLogIsReadWhole() throws Exception { Path log = dir.resolve("1234.log");
