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 fb95633ad068 CAMEL-26911: camel-jbang - camel log keeps a stack trace 
with its record, and closes the logs it read (#29827)
fb95633ad068 is described below

commit fb95633ad06801210e415eeb74e93babcd144606
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Nov 13 16:40:08 2026 +0100

    CAMEL-26911: camel-jbang - camel log keeps a stack trace with its record, 
and closes the logs it read (#29827)
    
    With several integrations, the lines without a timestamp were sorted to the
    top; and the logs were not closed, also not of an integration that stopped
    during --follow.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../jbang/core/commands/action/CamelLogAction.java | 224 +++++++++++----------
 .../core/commands/action/CamelLogActionTest.java   |  48 +++++
 2 files changed, 166 insertions(+), 106 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogAction.java
index 159da3a39a84..d1ece3fd2ac4 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogAction.java
@@ -24,6 +24,7 @@ import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -51,6 +52,7 @@ import 
org.apache.camel.dsl.jbang.core.common.CompletionCandidates;
 import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
 import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
 import org.apache.camel.dsl.jbang.run.common.PreviousRun;
+import org.apache.camel.util.IOHelper;
 import org.apache.camel.util.StopWatch;
 import org.apache.camel.util.StringHelper;
 import org.apache.camel.util.TimeUtils;
@@ -160,82 +162,87 @@ public class CamelLogAction extends ActionBaseCommand {
 
         Map<Long, Row> rows = new LinkedHashMap<>();
 
-        // before reading, as an integration that starts later is read with 
them too
-        if (find != null) {
-            findAnsi = 
Ansi.ansi().fg(Ansi.Color.BLACK).bg(Ansi.Color.YELLOW).a("$0").reset().toString();
-            findPatterns = quoteAndCompilePatterns(find);
-        }
-        if (grep != null) {
-            findAnsi = 
Ansi.ansi().fg(Ansi.Color.BLACK).bg(Ansi.Color.YELLOW).a("$0").reset().toString();
-            grepPatterns = quoteAndCompilePatterns(grep);
-        }
-
-        if (previous) {
-            previousRun(rows);
-        } else {
-            // find new pids
-            updatePids(rows);
-            if (rows.isEmpty() && !follow) {
-                // shown once: a name that matches no running integration is 
not found
-                findRunning();
+        try {
+            // before reading, as an integration that starts later is read 
with them too
+            if (find != null) {
+                findAnsi = 
Ansi.ansi().fg(Ansi.Color.BLACK).bg(Ansi.Color.YELLOW).a("$0").reset().toString();
+                findPatterns = quoteAndCompilePatterns(find);
             }
-        }
-        if (!rows.isEmpty()) {
-            // read existing log files (skip by tail/since)
-            Date limit = null;
-            if (since != null) {
-                long millis;
-                if (StringHelper.isDigit(since)) {
-                    // is in seconds by default
-                    millis = TimeUtils.toMilliSeconds(since) * 1000;
-                } else {
-                    millis = TimeUtils.toMilliSeconds(since);
+            if (grep != null) {
+                findAnsi = 
Ansi.ansi().fg(Ansi.Color.BLACK).bg(Ansi.Color.YELLOW).a("$0").reset().toString();
+                grepPatterns = quoteAndCompilePatterns(grep);
+            }
+
+            if (previous) {
+                previousRun(rows);
+            } else {
+                // find new pids
+                updatePids(rows);
+                if (rows.isEmpty() && !follow) {
+                    // shown once: a name that matches no running integration 
is not found
+                    findRunning();
                 }
-                limit = new Date(System.currentTimeMillis() - millis);
             }
-            if (startup) {
-                follow = false;
-                // only log startup logs until Camel was started
-                tailStartupLogFiles(rows);
-                dumpLogFiles(rows, 0);
-            } else if (tail != 0) {
-                // dump existing log lines
-                tailLogFiles(rows, tail, limit);
-                dumpLogFiles(rows, tail);
+            if (!rows.isEmpty()) {
+                // read existing log files (skip by tail/since)
+                Date limit = null;
+                if (since != null) {
+                    long millis;
+                    if (StringHelper.isDigit(since)) {
+                        // is in seconds by default
+                        millis = TimeUtils.toMilliSeconds(since) * 1000;
+                    } else {
+                        millis = TimeUtils.toMilliSeconds(since);
+                    }
+                    limit = new Date(System.currentTimeMillis() - millis);
+                }
+                if (startup) {
+                    follow = false;
+                    // only log startup logs until Camel was started
+                    tailStartupLogFiles(rows);
+                    dumpLogFiles(rows, 0);
+                } else if (tail != 0) {
+                    // dump existing log lines
+                    tailLogFiles(rows, tail, limit);
+                    dumpLogFiles(rows, tail);
+                }
             }
-        }
 
-        if (follow) {
-            boolean waitMessage = true;
-            // until Enter is pressed, or the --duration has passed
-            CommandHelper.RunUntil running = 
CommandHelper.RunUntil.start(durationOption.getDuration());
-            StopWatch watch = new StopWatch();
-            do {
-                if (rows.isEmpty()) {
-                    if (waitMessage) {
-                        printer().println("Waiting for logs ...");
-                        waitMessage = false;
-                    }
-                    Thread.sleep(500);
-                    updatePids(rows);
-                } else {
-                    waitMessage = true;
-                    if (watch.taken() > 500) {
-                        // check for new logs
+            if (follow) {
+                boolean waitMessage = true;
+                // until Enter is pressed, or the --duration has passed
+                CommandHelper.RunUntil running = 
CommandHelper.RunUntil.start(durationOption.getDuration());
+                StopWatch watch = new StopWatch();
+                do {
+                    if (rows.isEmpty()) {
+                        if (waitMessage) {
+                            printer().println("Waiting for logs ...");
+                            waitMessage = false;
+                        }
+                        Thread.sleep(500);
                         updatePids(rows);
-                        watch.restart();
-                    }
-                    int lines = readLogFiles(rows);
-                    if (lines > 0) {
-                        dumpLogFiles(rows, 0);
                     } else {
-                        Thread.sleep(100);
+                        waitMessage = true;
+                        if (watch.taken() > 500) {
+                            // check for new logs
+                            updatePids(rows);
+                            watch.restart();
+                        }
+                        int lines = readLogFiles(rows);
+                        if (lines > 0) {
+                            dumpLogFiles(rows, 0);
+                        } else {
+                            Thread.sleep(100);
+                        }
                     }
-                }
-            } while (running.isRunning());
-        }
+                } while (running.isRunning());
+            }
 
-        return 0;
+            return 0;
+        } finally {
+            // the readers of the logs, also when camel log runs in the JVM of 
the shell or the TUI (CAMEL-26911)
+            rows.values().forEach(r -> IOHelper.close(r.reader));
+        }
     }
 
     /**
@@ -333,7 +340,11 @@ public class CamelLogAction extends ActionBaseCommand {
             }
         }
         for (long pid : remove) {
-            rows.remove(pid);
+            // the integration has stopped: its log is not read anymore
+            Row row = rows.remove(pid);
+            if (row != null) {
+                IOHelper.close(row.reader);
+            }
         }
     }
 
@@ -385,13 +396,21 @@ public class CamelLogAction extends ActionBaseCommand {
 
     private void dumpLogFiles(Map<Long, Row> rows, int tail) {
         Set<String> names = new HashSet<>();
-        List<String> lines = new ArrayList<>();
+        // the records of the logs: a line with a timestamp, and the lines 
after it without one, such as a stack trace
+        List<LogRecord> records = new ArrayList<>();
+        final SimpleDateFormat sdf = new SimpleDateFormat(TIMESTAMP_MAIN);
         for (Row row : rows.values()) {
             Queue<String> queue = row.fifo;
             if (queue != null) {
+                LogRecord current = null;
                 for (String l : queue) {
                     names.add(row.name);
-                    lines.add(row.name + "| " + l);
+                    String timestamp = timestampOf(sdf, l);
+                    if (current == null || timestamp != null) {
+                        current = new LogRecord(timestamp, new ArrayList<>());
+                        records.add(current);
+                    }
+                    current.lines().add(row.name + "| " + l);
                 }
                 row.fifo.clear();
             }
@@ -399,42 +418,13 @@ public class CamelLogAction extends ActionBaseCommand {
 
         // only sort if there are multiple Camels running
         if (names.size() > 1) {
-            // sort lines
-            final SimpleDateFormat sdf = new SimpleDateFormat(TIMESTAMP_MAIN);
-            lines.sort((l1, l2) -> {
-                l1 = unescapeAnsi(l1);
-                l2 = unescapeAnsi(l2);
-
-                String t1 = StringHelper.after(l1, "| ");
-                t1 = StringHelper.before(t1, "  ");
-                String t2 = StringHelper.after(l2, "| ");
-                t2 = StringHelper.before(t2, "  ");
-
-                // there may be a stacktrace and no timestamps
-                if (t1 != null) {
-                    try {
-                        sdf.parse(t1);
-                    } catch (ParseException e) {
-                        t1 = null;
-                    }
-                }
-                if (t2 != null) {
-                    try {
-                        sdf.parse(t2);
-                    } catch (ParseException e) {
-                        t2 = null;
-                    }
-                }
-
-                if (t1 == null && t2 == null) {
-                    return 0;
-                } else if (t1 == null) {
-                    return -1;
-                } else if (t2 == null) {
-                    return 1;
-                }
-                return t1.compareTo(t2);
-            });
+            // by the time of the records, which keeps a stack trace with its 
record (CAMEL-26911); the sort is stable,
+            // so the records of a log at the same time stay in order
+            records.sort(Comparator.comparing(LogRecord::timestamp, 
Comparator.nullsFirst(Comparator.naturalOrder())));
+        }
+        List<String> lines = new ArrayList<>();
+        for (LogRecord r : records) {
+            lines.addAll(r.lines());
         }
         if (tail > 0) {
             // cut according to tail
@@ -515,6 +505,28 @@ public class CamelLogAction extends ActionBaseCommand {
         }
     }
 
+    /**
+     * A record of a log: a line with its timestamp, and the lines after it 
without a timestamp.
+     */
+    private record LogRecord(String timestamp, List<String> lines) {
+    }
+
+    /**
+     * The timestamp at the start of a line of a log, or null for a line 
without one (such as a line of a stack trace).
+     */
+    private String timestampOf(SimpleDateFormat sdf, String line) {
+        String t = StringHelper.before(unescapeAnsi(line), "  ");
+        if (t == null) {
+            return null;
+        }
+        try {
+            sdf.parse(t);
+            return t;
+        } catch (ParseException e) {
+            return null;
+        }
+    }
+
     private static Path logFile(Row row) {
         return row.file != null ? row.file : logFile(row.pid, row.name);
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogActionTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogActionTest.java
index a51dde1aca13..475d56e50d2b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogActionTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogActionTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.action;
 
+import java.lang.management.ManagementFactory;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.time.LocalDateTime;
@@ -23,6 +24,7 @@ import java.time.format.DateTimeFormatter;
 import java.util.List;
 import java.util.stream.Stream;
 
+import com.sun.management.UnixOperatingSystemMXBean;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.CliExitCode;
@@ -30,7 +32,10 @@ import 
org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.CommandLineTestHelper;
 import org.apache.camel.dsl.jbang.run.common.PreviousRun;
 import org.apache.camel.dsl.jbang.run.common.StringPrinter;
+import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.junit.jupiter.MockitoExtension;
 
@@ -132,6 +137,49 @@ class CamelLogActionTest extends ActionCommandTestSupport {
         assertThat(lines().get(0)).startsWith("orders    | ");
     }
 
+    @Test
+    void aStackTraceStaysWithItsRecordInTheLogsOfSeveralIntegrations() throws 
Exception {
+        long otherPid = TEST_PID + 1;
+        writeStatusFile(TEST_PID, "orders");
+        writeStatusFile(otherPid, "billing");
+        writeLog(TEST_PID + ".log", line(-300, "Starting"), line(-100, "ERROR 
Failed"),
+                "java.lang.IllegalStateException: boom", "\tat 
com.acme.Orders.process(Orders.java:42)",
+                line(-1, "Stopping"));
+        writeLog(otherPid + ".log", line(-200, "Billing"), line(-50, 
"Billed"));
+        CamelLogAction command = new CamelLogAction(new 
CamelJBangMain().withPrinter(printer));
+        CommandLineTestHelper.parse(command, "*", "--follow=false", 
"--logging-color=false");
+
+        assertThat(callWithResponse(command, new JsonObject(), 
otherPid)).isZero();
+
+        // by time, and the lines of the stack trace right after the record 
they belong to
+        List<String> lines = lines();
+        assertThat(lines).hasSize(7);
+        assertThat(lines.get(0)).endsWith(": Starting");
+        assertThat(lines.get(1)).endsWith(": Billing");
+        assertThat(lines.get(2)).endsWith(": ERROR Failed");
+        assertThat(lines.get(3)).endsWith("java.lang.IllegalStateException: 
boom");
+        assertThat(lines.get(4)).endsWith("at 
com.acme.Orders.process(Orders.java:42)");
+        assertThat(lines.get(5)).endsWith(": Billed");
+        assertThat(lines.get(6)).endsWith(": Stopping");
+    }
+
+    @Test
+    @EnabledOnOs({ OS.LINUX, OS.MAC })
+    void closesTheLogsItRead() throws Exception {
+        writeLog(TEST_PID + ".log", line(-300, "Starting"), line(-1, 
"Failed"));
+        UnixOperatingSystemMXBean os = (UnixOperatingSystemMXBean) 
ManagementFactory.getOperatingSystemMXBean();
+        // warm up: classes and the JARs they are loaded from
+        assertThat(callWithSingleProcess(command())).isZero();
+        long before = os.getOpenFileDescriptorCount();
+
+        // as the shell and the TUI run camel log, in their JVM
+        for (int i = 0; i < 50; i++) {
+            assertThat(callWithSingleProcess(command())).isZero();
+        }
+
+        assertThat(os.getOpenFileDescriptorCount() - before).as("open 
files").isLessThan(10);
+    }
+
     @Test
     void logNamedAfterTheIntegration() throws Exception {
         // camel run --logging-file-name, or an older version of Camel

Reply via email to