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 5d7706c0c417 CAMEL-25784: camel-jbang - camel run keeps the log of the 
previous run, camel log --previous (#28107)
5d7706c0c417 is described below

commit 5d7706c0c417562ae64eaaee8bcd6c84cdcaa518
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 19 21:41:39 2026 +0200

    CAMEL-25784: camel-jbang - camel run keeps the log of the previous run, 
camel log --previous (#28107)
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   8 +
 .../ROOT/pages/camel-jbang-configuration.adoc      |   3 +
 .../modules/ROOT/pages/camel-jbang-managing.adoc   |  27 ++
 .../ROOT/pages/jbang-commands/camel-jbang-log.adoc |   1 +
 .../META-INF/camel-jbang-commands-metadata.json    |   2 +-
 .../apache/camel/dsl/jbang/core/commands/Run.java  |  58 ++-
 .../jbang/core/commands/action/CamelLogAction.java |  97 +++-
 .../dsl/jbang/core/commands/process/Dirty.java     |   5 +
 .../camel/dsl/jbang/core/common/PreviousRun.java   | 517 +++++++++++++++++++++
 .../jbang/core/commands/RunPreviousLogTest.java    | 126 +++++
 .../core/commands/action/CamelLogActionTest.java   |  90 ++++
 .../dsl/jbang/core/commands/process/DirtyTest.java |  21 +
 .../dsl/jbang/core/common/PreviousRunTest.java     | 309 ++++++++++++
 13 files changed, 1241 insertions(+), 23 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 6045480f4d23..a29fa617f76d 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -1837,3 +1837,11 @@ With live reloading (such as `camel run --dev`), a 
change to application.propert
 
 `PropertiesReload` has a new default method, `onChanged`, which a properties 
file that changed calls with the properties
 that changed. A context reload (such as for secret rotation) still calls 
`onReload`.
+
+=== camel-jbang - camel run keeps the log of the previous run
+
+When `camel run` ends, its log is no longer deleted. It is kept as 
`~/.camel/<name>-previous.log`, by the name of the
+integration, with `~/.camel/<name>-previous.json`, which says when and how the 
run ended. The next run of that name that
+ends replaces both, and at most the last 10 MB of a log are kept. `camel log 
<name> --previous` prints the log, like
+`kubectl logs --previous` in Kubernetes. The log of an integration that was 
killed is kept the same way, the next time a
+previous run is looked up. `camel dirty` does not count these files.
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-configuration.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-configuration.adoc
index 86fa276a28eb..357fe0a30c2f 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-configuration.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-configuration.adoc
@@ -131,6 +131,9 @@ camel dirty
 camel dirty --clean
 ----
 
+The log of the previous run of an integration (`~/.camel/<name>-previous.log`, 
see `camel log --previous`) is kept on
+purpose, so `camel dirty` leaves it.
+
 An error is printed as one line that starts with `ERROR:`. To see the stack 
trace of every error, set the
 environment variable `CAMEL_CLI_DEBUG` to `true`. See 
xref:camel-jbang-exit-codes.adoc[Exit Codes and Output]
 for what each exit code means.
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
index 9293339bb643..4a7d5e65a271 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
@@ -628,6 +628,33 @@ camel log chuck --grep=error
 
 By default, `log` tails continuously. Use `--follow=false` to output matching 
lines and exit.
 
+=== The log of the previous run
+
+When an integration stops, its log is kept as the log of its previous run, 
like `kubectl logs --previous` in Kubernetes.
+Use `--previous` to see why an integration stopped, such as one that failed to 
start:
+
+[source,bash]
+----
+$ camel log chuck --previous
+The previous run of chuck (pid 80093) ended at 2026-10-19 10:32:01 (2m3s ago) 
with exit code 70: Failed to create route: route1 ...
+2026-10-19 10:32:00.912  INFO 80093 --- [           main] 
org.apache.camel.main.MainSupport   : Apache Camel (JBang) 4.24.0 is starting
+...
+----
+
+The first line, on stderr, says which run it was and how it ended. The log 
follows on stdout.
+A run that Ctrl+C or SIGTERM stopped ended with exit code 130 or 143 
(`(SIGINT)` or `(SIGTERM)` follows the code): the
+JVM exits with 128 and the number of the signal, as a shell and Kubernetes 
show it.
+The options `--tail`, `--since`, `--grep`, `--find` and `--startup` work as 
they do for a running integration.
+When you run `camel log` with the name of an integration that is not running, 
the error says how its previous run ended.
+
+Each name keeps the log of its last run that ended, as 
`~/.camel/<name>-previous.log`.
+The next run of that name that ends replaces it. Like Kubernetes, at most the 
last 10 MB of a log are kept.
+An integration that was killed, or whose JVM crashed, leaves its log behind.
+That log becomes the log of its previous run the next time you look one up.
+
+NOTE: The log is kept for the `jbang` runtime, which runs Camel in the JVM of 
the Camel CLI. This is the default, also
+with `--background`. The runtimes that run with Maven 
(`--runtime=spring-boot`, `quarkus` or `main`) do not keep it.
+
 == Configuring logging levels
 
 View and change logging levels at runtime:
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-log.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-log.adoc
index 80e617648309..de0278434bfd 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-log.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-log.adoc
@@ -25,6 +25,7 @@ camel log [options]
 | `--grep` | Filter logs to only output lines matching text (ignore case). |  
| String
 | `--logging-color` | Use colored logging | true | boolean
 | `--prefix` | Print prefix with running Camel integration name. auto=only 
prefix when running multiple integrations. true=always prefix. false=prefix 
off. | auto | String
+| `--previous` | Print the log of the previous run of the integration instead: 
the last run of that name that ended, such as one that failed to start (like 
kubectl logs --previous). | false | boolean
 | `--since` | Return logs newer than a relative duration like 5s, 2m, or 1h. 
The value is in seconds if no unit specified. |  | String
 | `--startup` | Only shows logs from the starting phase to make it quick to 
look at how Camel was started. | false | boolean
 | `--tail` | The number of lines from the end of the logs to show. Use -1 to 
read from the beginning. Use 0 to read only new lines. Defaults to showing all 
logs from beginning. | -1 | int
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index fcf519c28b5a..0e2ca16f0d15 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -21,7 +21,7 @@
     { "name": "infra", "fullName": "infra", "description": "List and Run 
external services for testing and prototyping", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.infra.InfraCommand", "options": [ { 
"names": "--json", "description": "Output in JSON Format", "javaType": 
"boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display 
the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], 
"subcommands": [ { "name": "get", "fullName": "infra  [...]
     { "name": "init", "fullName": "init", "description": "Creates a new Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Init", 
"options": [ { "names": "--clean-dir,--clean-directory", "description": 
"Whether to clean directory first (deletes all files in directory)", 
"javaType": "boolean", "type": "boolean" }, { "names": "--dir,--directory", 
"description": "Directory relative path where the new Camel integration will be 
saved", "defaultValue": ".", "javaType" [...]
     { "name": "jolokia", "fullName": "jolokia", "description": "Attach Jolokia 
JVM Agent to a running Camel integration", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.process.Jolokia", "options": [ { 
"names": "--port", "description": "To use a specific port number when attaching 
Jolokia JVM Agent (default a free port is found in range 8778-9999)", 
"javaType": "int", "type": "integer" }, { "names": "--stop", "description": 
"Stops the Jolokia JVM Agent in the running Camel inte [...]
-    { "name": "log", "fullName": "log", "description": "Tail logs from running 
Camel integrations", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelLogAction", "options": [ 
{ "names": "--duration", "description": "How long to keep watching or 
following, in millis or as a duration such as 30s (default: until Enter is 
pressed)", "javaType": "long", "type": "integer" }, { "names": "--find", 
"description": "Find and highlight matching text (ignore case).", "javaType": 
"j [...]
+    { "name": "log", "fullName": "log", "description": "Tail logs from running 
Camel integrations", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelLogAction", "options": [ 
{ "names": "--duration", "description": "How long to keep watching or 
following, in millis or as a duration such as 30s (default: until Enter is 
pressed)", "javaType": "long", "type": "integer" }, { "names": "--find", 
"description": "Find and highlight matching text (ignore case).", "javaType": 
"j [...]
     { "name": "nano", "fullName": "nano", "description": "Nano editor to edit 
file", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Nano", 
"options": [ { "names": "-h,--help", "description": "Display the help and 
sub-commands", "javaType": "boolean", "type": "boolean" } ] },
     { "name": "overview", "fullName": "overview", "description": "High-level 
overview of the integrations in a project, optionally explained by AI\/LLM", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.Overview", "options": 
[ { "names": "--ai", "description": "Ask an LLM to explain the project (an 
overview, the capabilities, labels and notes of routes that have none) and save 
it in camel-summary.md, marked as AI-assisted", "javaType": "boolean", "type": 
"boolean" }, { "names": " [...]
     { "name": "plugin", "fullName": "plugin", "description": "Manage plugins 
that add sub-commands to this CLI", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.plugin.PluginCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "add", 
"fullName": "plugin add", "description": "Add new plugin", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.plugin.PluginA [...]
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
index c81ca8ef00ee..e6bf47779e3a 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
@@ -50,6 +50,7 @@ import org.apache.camel.CamelContext;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.dsl.jbang.core.commands.ai.YamlLoadFailureReport;
+import 
org.apache.camel.dsl.jbang.core.commands.exceptionhandler.CliExecutionExceptionHandler;
 import org.apache.camel.dsl.jbang.core.common.BackgroundLog;
 import org.apache.camel.dsl.jbang.core.common.ChildProcess;
 import org.apache.camel.dsl.jbang.core.common.CliException;
@@ -65,6 +66,7 @@ import org.apache.camel.dsl.jbang.core.common.MavenCommand;
 import org.apache.camel.dsl.jbang.core.common.Plugin;
 import org.apache.camel.dsl.jbang.core.common.PluginExporter;
 import org.apache.camel.dsl.jbang.core.common.PluginHelper;
+import org.apache.camel.dsl.jbang.core.common.PreviousRun;
 import org.apache.camel.dsl.jbang.core.common.Printer;
 import org.apache.camel.dsl.jbang.core.common.ProfileCompletionCandidates;
 import org.apache.camel.dsl.jbang.core.common.PropertyResolver;
@@ -178,7 +180,8 @@ public class Run extends CamelCommand {
     boolean transformMessageRun;
     boolean debugRun;
 
-    private Path logFile;
+    // package-private for RunPreviousLogTest
+    Path logFile;
     public long spawnPid;
 
     private Printer quietPrinter;
@@ -3052,26 +3055,63 @@ public class Run extends CamelCommand {
 
     protected int runKameletMain(KameletMain main) throws Exception {
         Recording jfrRecording = startJfrRecording();
+        PreviousRun.Keeper previousLog = null;
+        if (logFile != null && keepsPreviousLog()) {
+            // CAMEL-25784: the log is kept as the previous log of the 
integration when the run ends, and so are the
+            // logs that runs which were killed left behind
+            PreviousRun.keepOrphans();
+            previousLog = new PreviousRun.Keeper(logFile, () -> runName(main));
+        }
+        Integer exitCode = null;
+        String failure = null;
         try {
             main.start();
             if (hasNoRoutes(main)) {
                 // it would run doing nothing: a file without routes, or a 
mistake in it
-                printer().printErr(files.isEmpty()
-                        ? "No routes found" : "No routes found in: " + 
String.join(", ", files));
+                failure = files.isEmpty() ? "No routes found" : "No routes 
found in: " + String.join(", ", files);
+                printer().printErr(failure);
                 main.stop();
-                return CliExitCode.FAILURE;
+                exitCode = CliExitCode.FAILURE;
+                return exitCode;
             }
             main.run();
+            int code = main.getExitCode();
+            // no exit code is set when Camel is stopped from outside (such as 
camel stop)
+            exitCode = code == Integer.MIN_VALUE ? CliExitCode.OK : code;
+            return exitCode;
+        } catch (Exception | Error e) {
+            // such as a route that failed to start
+            failure = e.getMessage() != null ? e.getMessage() : 
e.getClass().getName();
+            exitCode = new 
CliExecutionExceptionHandler(getMain()).getExitCode(e);
+            throw e;
         } finally {
             stopJfrRecording(jfrRecording);
-            // cleanup and delete log file
-            if (logFile != null) {
+            if (previousLog != null) {
+                previousLog.ended(exitCode, failure);
+            } else if (logFile != null) {
+                // cleanup and delete log file
                 FileUtil.deleteFile(logFile);
             }
         }
-        int code = main.getExitCode();
-        // no exit code is set when Camel is stopped from outside (such as 
camel stop)
-        return code == Integer.MIN_VALUE ? CliExitCode.OK : code;
+    }
+
+    /**
+     * Whether the log of the run is kept as the previous log of the 
integration when the run ends (CAMEL-25784):
+     * {@code camel run} on the command line, not a Camel that another command 
runs in its JVM (such as
+     * {@code camel cmd send --local} and {@code camel transform}).
+     */
+    boolean keepsPreviousLog() {
+        return spec != null && !exportRun && !transformRun && 
!transformMessageRun && !scriptRun;
+    }
+
+    /**
+     * The name of the integration, as {@code camel ps} and {@code camel log} 
find it: the name of its CamelContext,
+     * which is {@code camel.main.name}, else the name of the run when Camel 
did not get that far.
+     */
+    private String runName(KameletMain main) {
+        CamelContext context = main.getCamelContext();
+        String answer = context != null ? context.getName() : null;
+        return answer != null && !answer.isBlank() ? answer : name;
     }
 
     /**
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 50f900d2e97f..f10009dbeb5d 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
@@ -33,14 +33,17 @@ import java.util.List;
 import java.util.Map;
 import java.util.Queue;
 import java.util.Set;
+import java.util.StringJoiner;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.regex.Pattern;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.CommandHelper;
 import org.apache.camel.dsl.jbang.core.commands.DurationMixin;
+import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
+import org.apache.camel.dsl.jbang.core.common.PreviousRun;
 import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
 import org.apache.camel.util.StopWatch;
 import org.apache.camel.util.StringHelper;
@@ -55,7 +58,8 @@ import picocli.CommandLine;
                              "%nExamples:",
                              "  camel log hello",
                              "  camel log *",
-                             "  camel log hello --tail=50" })
+                             "  camel log hello --tail=50",
+                             "  camel log hello --previous" })
 public class CamelLogAction extends ActionBaseCommand {
 
     private static final int NAME_MAX_WIDTH = 25;
@@ -75,7 +79,8 @@ public class CamelLogAction extends ActionBaseCommand {
         }
     }
 
-    @CommandLine.Parameters(description = "Name or pid of running Camel 
integration. (default selects all)", arity = "0..1")
+    @CommandLine.Parameters(description = "Name or pid of running Camel 
integration, or of the run that ended with --previous. (default selects all)",
+                            arity = "0..1")
     String name = "*";
 
     @CommandLine.Option(names = { "--logging-color" }, defaultValue = "true", 
arity = "0..1", fallbackValue = "true",
@@ -118,6 +123,11 @@ public class CamelLogAction extends ActionBaseCommand {
                         description = "Filter logs to only output lines 
matching text (ignore case).", arity = "0..*")
     String[] grep;
 
+    @CommandLine.Option(names = { "--previous" }, defaultValue = "false",
+                        description = "Print the log of the previous run of 
the integration instead: the last run of that name that ended,"
+                                      + " such as one that failed to start 
(like kubectl logs --previous).")
+    boolean previous;
+
     String findAnsi;
     Pattern[] grepPatterns;
     Pattern[] findPatterns;
@@ -135,7 +145,8 @@ public class CamelLogAction extends ActionBaseCommand {
 
     @Override
     public Integer doCall() throws Exception {
-        follow = isFollowing(follow, durationOption.getDuration());
+        // a previous run has ended, so there is nothing to follow
+        follow = !previous && isFollowing(follow, 
durationOption.getDuration());
         if (follow && EnvironmentHelper.isEmbedded()) {
             follow = false;
             printer().printTip("press F3 to switch to the Log tab for live log 
streaming");
@@ -153,11 +164,15 @@ public class CamelLogAction extends ActionBaseCommand {
             grepPatterns = quoteAndCompilePatterns(grep);
         }
 
-        // find new pids
-        updatePids(rows);
-        if (rows.isEmpty() && !follow) {
-            // shown once: a name that matches no running integration is not 
found
-            findProcesses(name);
+        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();
+            }
         }
         if (!rows.isEmpty()) {
             // read existing log files (skip by tail/since)
@@ -217,6 +232,60 @@ public class CamelLogAction extends ActionBaseCommand {
         return 0;
     }
 
+    /**
+     * The previous run of the integration (CAMEL-25784): the one that matches 
the name, after a line on stderr that
+     * says which run it was and how it ended.
+     *
+     * @throws CliException with exit code 3 when no previous run, or more 
than one, matches
+     */
+    private void previousRun(Map<Long, Row> rows) {
+        boolean all = name == null || name.isBlank() || "*".equals(name);
+        List<PreviousRun.Info> found = PreviousRun.find(name);
+        if (found.isEmpty()) {
+            throw CliException.notFound(all ? "No previous run of a Camel 
integration" : "No previous run matches: " + name);
+        }
+        if (found.size() > 1) {
+            StringJoiner names = new StringJoiner(", ");
+            found.forEach(i -> names.add(i.name()));
+            throw CliException.notFound((all ? "There are " : "Name " + name + 
" matches ") + found.size()
+                                        + " previous runs: " + names + ". 
Specify the name of one.");
+        }
+        PreviousRun.Info info = found.get(0);
+        printer().printDiagnostic(info.describe());
+        Row row = new Row();
+        row.pid = Long.toString(info.pid());
+        row.name = info.name();
+        row.file = info.log();
+        nameMaxWidth = Math.max(NAME_MIN_WIDTH, Math.min(NAME_MAX_WIDTH, 
row.name.length()));
+        rows.put(info.pid(), row);
+    }
+
+    /**
+     * Fails when a name given matches no running integration, saying where 
the log of its previous run is, which tells
+     * why it is not running.
+     */
+    private void findRunning() {
+        try {
+            if (findProcesses(name).isEmpty()) {
+                // nothing runs: the newest previous run (findAll is newest 
first)
+                List<PreviousRun.Info> found = PreviousRun.findAll();
+                if (!found.isEmpty()) {
+                    printer().printTip(previousRunHint(found.get(0)));
+                }
+            }
+        } catch (CliException e) {
+            List<PreviousRun.Info> found = PreviousRun.find(name);
+            if (found.size() != 1) {
+                throw e;
+            }
+            throw CliException.notFound(e.getMessage() + ". " + 
previousRunHint(found.get(0)));
+        }
+    }
+
+    private static String previousRunHint(PreviousRun.Info info) {
+        return "camel log " + info.name() + " --previous prints the log of its 
previous run, which " + info.howItEnded();
+    }
+
     private void updatePids(Map<Long, Row> rows) {
         List<Long> pids = findPids(name);
         ProcessHandle.allProcesses()
@@ -267,7 +336,7 @@ public class CamelLogAction extends ActionBaseCommand {
 
         for (Row row : rows.values()) {
             if (row.reader == null) {
-                Path file = logFile(row.pid, row.name);
+                Path file = logFile(row);
                 if (Files.exists(file)) {
                     row.reader = new 
LineNumberReader(Files.newBufferedReader(file));
                     if (tail == 0) {
@@ -440,8 +509,8 @@ public class CamelLogAction extends ActionBaseCommand {
         }
     }
 
-    private static Path logFile(String pid) {
-        return logFile(pid, null);
+    private static Path logFile(Row row) {
+        return row.file != null ? row.file : logFile(row.pid, row.name);
     }
 
     private static Path logFile(String pid, String appName) {
@@ -458,7 +527,7 @@ public class CamelLogAction extends ActionBaseCommand {
 
     private void tailStartupLogFiles(Map<Long, Row> rows) throws Exception {
         for (Row row : rows.values()) {
-            Path log = logFile(row.pid, row.name);
+            Path log = logFile(row);
             if (Files.exists(log)) {
                 row.fifo = new ArrayDeque<>();
                 row.reader = new 
LineNumberReader(Files.newBufferedReader(log));
@@ -480,7 +549,7 @@ public class CamelLogAction extends ActionBaseCommand {
 
     private void tailLogFiles(Map<Long, Row> rows, int tail, Date limit) 
throws Exception {
         for (Row row : rows.values()) {
-            Path log = logFile(row.pid, row.name);
+            Path log = logFile(row);
             if (Files.exists(log)) {
                 row.reader = new 
LineNumberReader(Files.newBufferedReader(log));
                 String line;
@@ -595,6 +664,8 @@ public class CamelLogAction extends ActionBaseCommand {
     private static class Row {
         String pid;
         String name;
+        // the log of a previous run, else the log of the running integration
+        Path file;
         Queue<String> fifo;
         LineNumberReader reader;
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/Dirty.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/Dirty.java
index 6dcb4cae0dcb..5f887eb9b9b5 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/Dirty.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/Dirty.java
@@ -21,6 +21,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.PreviousRun;
 import org.apache.camel.util.FileUtil;
 import picocli.CommandLine;
 
@@ -51,6 +52,10 @@ public class Dirty extends ProcessBaseCommand {
 
         List<File> dirty = new ArrayList<>();
         for (File f : files) {
+            if (PreviousRun.isPreviousRunFile(f.getName())) {
+                // kept on purpose: the log of the previous run of an 
integration (camel log --previous)
+                continue;
+            }
             // the files of a process that is alive are in use (such as the 
launcher of an integration that runs in a
             // child process, whose Camel has stopped), and so are the files 
of ourselves
             Long pid = pidOf(f.getName());
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PreviousRun.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PreviousRun.java
new file mode 100644
index 000000000000..c6343cb1a8c6
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PreviousRun.java
@@ -0,0 +1,517 @@
+/*
+ * 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.common;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.camel.support.PatternHelper;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import sun.misc.Signal;
+import sun.misc.SignalHandler;
+
+/**
+ * The log of the previous run of an integration, as <tt>kubectl logs 
--previous</tt> shows the log of the previous
+ * instance of a container in Kubernetes.
+ * <p/>
+ * When <tt>camel run</tt> ends (it was stopped, it failed to start, or it 
stopped by itself), its log is kept as
+ * <tt>~/.camel/&lt;name&gt;-previous.log</tt>, by the name of the 
integration, with
+ * <tt>~/.camel/&lt;name&gt;-previous.json</tt>, which says when and how the 
run ended. The next run of that name that
+ * ends replaces both. Like Kubernetes, at most the last 10 MB of a log are 
kept.
+ * <p/>
+ * A process that is gone without keeping its log (it was killed, or its JVM 
crashed) leaves its
+ * <tt>~/.camel/&lt;pid&gt;.log</tt> behind. That log is kept for it, under 
the name in its status file, the next time
+ * the previous runs are looked up, and the next time an integration is run.
+ */
+public final class PreviousRun {
+
+    /**
+     * The end of a log that is kept, at most: Kubernetes keeps at most 10 MB 
of the log of a container by default.
+     */
+    static final long MAX_SIZE = 10L * 1024 * 1024;
+
+    /**
+     * How old the log of a process that is gone may get, when there is no 
name to keep it by, before it is removed.
+     */
+    static final long ORPHAN_AGE = TimeUnit.DAYS.toMillis(1);
+
+    /**
+     * What a process that is gone without keeping its log ended with, as far 
as is known.
+     */
+    static final String GONE = "the process was gone without stopping Camel 
(killed, or its JVM crashed)";
+
+    private static final String LOG_SUFFIX = "-previous.log";
+    private static final String INFO_SUFFIX = "-previous.json";
+    private static final Pattern PID_LOG = Pattern.compile("(\\d+)\\.log");
+    private static final int MAX_FAILURE = 500;
+    private static final DateTimeFormatter TIME
+            = DateTimeFormatter.ofPattern("yyyy-MM-dd 
HH:mm:ss").withZone(ZoneId.systemDefault());
+
+    /**
+     * A run that ended, and the log it kept.
+     *
+     * @param name      the name of the integration
+     * @param pid       the pid it ran as, 0 when not known
+     * @param directory the directory it ran in, or <tt>null</tt>
+     * @param started   when it started, in epoch millis, 0 when not known
+     * @param ended     when it ended, in epoch millis
+     * @param exitCode  the exit code, or <tt>null</tt> when not known (such 
as of a process that was killed)
+     * @param signal    the signal that stopped the run, such as 
<tt>SIGTERM</tt>, or <tt>null</tt>
+     * @param failure   why it ended, when it failed (such as the error it 
failed to start with), or <tt>null</tt>
+     * @param log       the log it kept
+     */
+    public record Info(String name, long pid, String directory, long started, 
long ended, Integer exitCode,
+            String signal, String failure, Path log) {
+
+        /**
+         * Which run it was, and how it ended, in one line.
+         */
+        public String describe() {
+            return "The previous run of " + name + (pid > 0 ? " (pid " + pid + 
")" : "") + " " + howItEnded();
+        }
+
+        /**
+         * How the run ended: when, with which exit code, and why when it 
failed.
+         */
+        public String howItEnded() {
+            StringBuilder sb = new StringBuilder("ended at 
").append(TIME.format(Instant.ofEpochMilli(ended)));
+            long ago = System.currentTimeMillis() - ended;
+            if (ago >= 0) {
+                sb.append(" (").append(TimeUtils.printDuration(ago, 
false)).append(" ago)");
+            }
+            if (exitCode != null) {
+                sb.append(" with exit code ").append(exitCode);
+            }
+            if (signal != null) {
+                sb.append(" (").append(signal).append(')');
+            }
+            if (failure != null) {
+                sb.append(": ").append(failure);
+            }
+            return sb.toString();
+        }
+    }
+
+    private PreviousRun() {
+    }
+
+    /**
+     * Keeps the log of a run of this JVM that ended as the previous log of 
its name.
+     *
+     * @param  log      the log of the run
+     * @param  name     the name of the integration
+     * @param  pid      the pid it ran as, 0 when not known
+     * @param  exitCode the exit code, or <tt>null</tt> when not known
+     * @param  failure  why it ended, when it failed, or <tt>null</tt>
+     * @return          the kept log, or <tt>null</tt> when there was no log 
to keep
+     */
+    public static Path keep(Path log, String name, long pid, Integer exitCode, 
String failure) {
+        return keep(log, thisRun(name, pid, exitCode, null, failure));
+    }
+
+    // a run of this JVM that ends now
+    private static Info thisRun(String name, long pid, Integer exitCode, 
String signal, String failure) {
+        long started = 
ProcessHandle.current().info().startInstant().map(Instant::toEpochMilli).orElse(0L);
+        return new Info(
+                name, pid, System.getProperty("user.dir"), started, 
System.currentTimeMillis(), exitCode, signal,
+                failure, null);
+    }
+
+    /**
+     * Keeps the log of a run that ended as the previous log of its name, in 
place of the one kept before.
+     *
+     * @return the kept log, or <tt>null</tt> when there was no log to keep
+     */
+    static Path keep(Path log, Info info) {
+        if (log == null || info.name() == null || info.name().isBlank() || 
!Files.isRegularFile(log)) {
+            return null;
+        }
+        Path dir = CommandLineHelper.getCamelDir();
+        String file = fileName(info.name());
+        Path target = dir.resolve(file + LOG_SUFFIX);
+        try {
+            // what was kept about the run before goes first, so a keep that 
fails (such as on a full disk) does not
+            // leave it next to the log of this run
+            Files.deleteIfExists(dir.resolve(file + INFO_SUFFIX));
+            if (Files.size(log) > MAX_SIZE || !move(log, target)) {
+                copyEnd(log, target, MAX_SIZE);
+                try {
+                    Files.deleteIfExists(log);
+                } catch (IOException e) {
+                    // still open (Windows): removed when the process is gone 
(see keepOrphans)
+                }
+            }
+            JsonObject jo = new JsonObject();
+            jo.put("name", info.name());
+            if (info.pid() > 0) {
+                jo.put("pid", info.pid());
+            }
+            if (info.directory() != null) {
+                jo.put("directory", info.directory());
+            }
+            if (info.started() > 0) {
+                jo.put("started", info.started());
+            }
+            jo.put("ended", info.ended());
+            if (info.exitCode() != null) {
+                jo.put("exitCode", info.exitCode());
+            }
+            if (info.signal() != null) {
+                jo.put("signal", info.signal());
+            }
+            if (info.failure() != null) {
+                String failure = info.failure().strip();
+                jo.put("failure", failure.length() > MAX_FAILURE ? 
failure.substring(0, MAX_FAILURE) + "..." : failure);
+            }
+            Files.writeString(dir.resolve(file + INFO_SUFFIX), jo.toJson());
+            return target;
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    /**
+     * The previous run of each name, the newest first.
+     */
+    public static List<Info> findAll() {
+        keepOrphans();
+        List<Info> answer = new ArrayList<>();
+        File[] files = CommandLineHelper.getCamelDir().toFile().listFiles((d, 
n) -> n.endsWith(INFO_SUFFIX));
+        if (files != null) {
+            for (File f : files) {
+                Info info = read(f.toPath());
+                if (info != null) {
+                    answer.add(info);
+                }
+            }
+        }
+        answer.sort(Comparator.comparingLong(Info::ended).reversed());
+        return answer;
+    }
+
+    /**
+     * The previous runs that match the name or pid, as a running integration 
is found (see {@link ProcessFinder}): a
+     * number is the pid it ran as, and a name is a prefix (<tt>foo</tt> 
matches <tt>foo-bar</tt>) unless it ends with
+     * <tt>!</tt>, and may use wildcards. A name that is the whole name of a 
previous run matches only that run.
+     *
+     * @param nameOrPid a name, a pattern or a pid; <tt>null</tt> or 
<tt>*</tt> for every previous run
+     */
+    public static List<Info> find(String nameOrPid) {
+        List<Info> all = findAll();
+        if (nameOrPid == null || nameOrPid.isBlank() || "*".equals(nameOrPid)) 
{
+            return all;
+        }
+        if (nameOrPid.matches("\\d+")) {
+            long pid = Long.parseLong(nameOrPid);
+            return all.stream().filter(i -> i.pid() == pid).toList();
+        }
+        boolean exact = nameOrPid.endsWith("!");
+        String name = exact ? nameOrPid.substring(0, nameOrPid.length() - 1) : 
nameOrPid;
+        List<Info> same = all.stream().filter(i -> 
i.name().equals(name)).toList();
+        if (exact || !same.isEmpty()) {
+            return same;
+        }
+        String pattern = name.endsWith("*") ? name : name + "*";
+        return all.stream().filter(i -> PatternHelper.matchPattern(i.name(), 
pattern)).toList();
+    }
+
+    /**
+     * Whether the file in the Camel directory is a log of a previous run, or 
what it says about that run.
+     */
+    public static boolean isPreviousRunFile(String fileName) {
+        return fileName.endsWith(LOG_SUFFIX) || fileName.endsWith(INFO_SUFFIX);
+    }
+
+    /**
+     * Keeps the logs that processes which are gone left behind (they were 
killed, or their JVM crashed), under the name
+     * in their status file. A log older than the previous run of its name is 
removed, and so is a log without a name
+     * that is older than a day.
+     */
+    public static void keepOrphans() {
+        File[] files = CommandLineHelper.getCamelDir().toFile().listFiles((d, 
n) -> PID_LOG.matcher(n).matches());
+        if (files == null) {
+            return;
+        }
+        long now = System.currentTimeMillis();
+        for (File f : files) {
+            Matcher m = PID_LOG.matcher(f.getName());
+            if (!m.matches()) {
+                continue;
+            }
+            long pid;
+            try {
+                pid = Long.parseLong(m.group(1));
+            } catch (NumberFormatException e) {
+                continue;
+            }
+            if 
(ProcessHandle.of(pid).map(ProcessHandle::isAlive).orElse(false)) {
+                // still running (or its pid is taken by another process)
+                continue;
+            }
+            long modified = f.lastModified();
+            JsonObject status = 
readJson(CommandLineHelper.getCamelDir().resolve(pid + "-status.json"));
+            String name = status != null && status.get("context") instanceof 
JsonObject context
+                    ? context.getString("name") : null;
+            if (name != null && !name.isBlank()) {
+                Info before = 
read(CommandLineHelper.getCamelDir().resolve(fileName(name) + INFO_SUFFIX));
+                // the time of the kept log, not when its run ended, so both 
times come from the file system (a
+                // network file system has a clock of its own)
+                if (before == null || before.log().toFile().lastModified() < 
modified) {
+                    String directory = status.get("runtime") instanceof 
JsonObject runtime
+                            ? runtime.getString("directory") : null;
+                    // when the process was gone is not known: the last write 
of its log is the closest time to it
+                    keep(f.toPath(), new Info(name, pid, directory, 0, 
modified, null, null, GONE, null));
+                    continue;
+                }
+            } else if (now - modified < ORPHAN_AGE) {
+                continue;
+            }
+            // older than the previous run of its name, or without a name to 
keep it by
+            try {
+                Files.deleteIfExists(f.toPath());
+            } catch (IOException e) {
+                // removed by camel dirty --clean
+            }
+        }
+    }
+
+    /**
+     * Keeps the log of the run of this JVM once, when the run ends: see 
{@link #ended(Integer, String)}. When the JVM
+     * exits before the run has ended (such as on Ctrl+C, while Camel stops), 
the log is kept as the JVM exits: the run
+     * gets a few seconds to end and say how it ended, else the log is kept 
without that.
+     * <p/>
+     * A run that Ctrl+C or SIGTERM stops ends with the exit code of the JVM: 
128 and the number of the signal (130 for
+     * SIGINT, 143 for SIGTERM), as a shell and Kubernetes show it. The keeper 
watches these signals for that, and
+     * passes each on to the handler of the JVM, which runs the shutdown hooks 
and exits.
+     */
+    public static final class Keeper {
+
+        /**
+         * How long a JVM that exits waits for the run to end, in seconds.
+         */
+        static final int EXIT_WAIT_SECONDS = 10;
+
+        /**
+         * The signals the JVM exits on, running its shutdown hooks.
+         */
+        private static final List<String> STOP_SIGNALS = List.of("INT", 
"TERM");
+
+        private final Path log;
+        private final Supplier<String> name;
+        private final Thread hook;
+        private final CountDownLatch ended = new CountDownLatch(1);
+        private final List<Watch> watches = new ArrayList<>();
+        private volatile Signal stoppedBy;
+        private boolean kept;
+
+        // a signal that stops the JVM: the handler of the keeper, and the 
handler of the JVM it passes the signal on to
+        private record Watch(Signal signal, SignalHandler handler, 
AtomicReference<SignalHandler> jvm) {
+        }
+
+        /**
+         * A run that has started, and keeps its log when it ends.
+         *
+         * @param log  the log of the run
+         * @param name the name of the integration, asked for when the run ends
+         */
+        public Keeper(Path log, Supplier<String> name) {
+            this.log = log;
+            this.name = name;
+            this.hook = new Thread(() -> exiting(EXIT_WAIT_SECONDS), 
"CamelKeepPreviousLog");
+            Runtime.getRuntime().addShutdownHook(hook);
+            watchSignals();
+        }
+
+        /**
+         * The run has ended: keeps its log as the previous log of its name.
+         *
+         * @param exitCode the exit code, or <tt>null</tt> when not known
+         * @param failure  why it ended, when it failed, or <tt>null</tt>
+         */
+        public void ended(Integer exitCode, String failure) {
+            keep(exitCode, failure);
+            ended.countDown();
+            try {
+                Runtime.getRuntime().removeShutdownHook(hook);
+                // the JVM goes on (such as in a test): the signals go to the 
handlers of the JVM again
+                unwatchSignals();
+            } catch (IllegalStateException e) {
+                // the JVM is exiting: the hook finds the log kept
+            }
+        }
+
+        /**
+         * The signal stopped the JVM; visible for tests, as a test cannot 
send it.
+         */
+        void stoppedBy(Signal signal) {
+            stoppedBy = signal;
+        }
+
+        private void watchSignals() {
+            for (String name : STOP_SIGNALS) {
+                try {
+                    Signal signal = new Signal(name);
+                    AtomicReference<SignalHandler> jvm = new 
AtomicReference<>();
+                    SignalHandler handler = s -> {
+                        stoppedBy = s;
+                        SignalHandler next = jvm.get();
+                        if (next != null) {
+                            next.handle(s);
+                        } else {
+                            // what the JVM does on the signal
+                            System.exit(128 + s.getNumber());
+                        }
+                    };
+                    SignalHandler before = Signal.handle(signal, handler);
+                    if (before == SignalHandler.SIG_DFL || before == 
SignalHandler.SIG_IGN) {
+                        // the JVM does not exit on it here (such as a signal 
that is ignored): left as it was
+                        Signal.handle(signal, before);
+                    } else {
+                        jvm.set(before);
+                        watches.add(new Watch(signal, handler, jvm));
+                    }
+                } catch (IllegalArgumentException e) {
+                    // a signal this platform does not have, or that the JVM 
keeps (such as with -Xrs)
+                }
+            }
+        }
+
+        private void unwatchSignals() {
+            for (Watch watch : watches) {
+                try {
+                    SignalHandler current = Signal.handle(watch.signal(), 
watch.jvm().get());
+                    if (current != watch.handler()) {
+                        // a handler that came after the keeper stays
+                        Signal.handle(watch.signal(), current);
+                    }
+                } catch (IllegalArgumentException e) {
+                    // left as it is
+                }
+            }
+            watches.clear();
+        }
+
+        // the JVM exits: the run gets the seconds to end, and say how it ended
+        void exiting(int waitSeconds) {
+            try {
+                ended.await(waitSeconds, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+            keep(null, null);
+        }
+
+        private synchronized void keep(Integer exitCode, String failure) {
+            if (!kept) {
+                kept = true;
+                Signal signal = stoppedBy;
+                // the JVM exits with the exit code of the signal, whatever 
the run answered
+                PreviousRun.keep(log, thisRun(name.get(), 
ProcessHandle.current().pid(),
+                        signal != null ? Integer.valueOf(128 + 
signal.getNumber()) : exitCode,
+                        signal != null ? "SIG" + signal.getName() : null, 
failure));
+            }
+        }
+    }
+
+    // the name of an integration as a file name: what is not a letter, a 
digit, a dot, a dash or an underscore
+    // becomes an underscore
+    static String fileName(String name) {
+        String answer = name.replaceAll("[^A-Za-z0-9._-]", "_");
+        return answer.isBlank() || answer.startsWith(".") ? "_" + answer : 
answer;
+    }
+
+    private static boolean move(Path log, Path target) {
+        try {
+            Files.move(log, target, StandardCopyOption.REPLACE_EXISTING);
+            return true;
+        } catch (IOException e) {
+            // such as a log that is still open (Windows)
+            return false;
+        }
+    }
+
+    // the end of the log, at most max bytes, from the start of a line
+    static void copyEnd(Path log, Path target, long max) throws IOException {
+        try (RandomAccessFile in = new RandomAccessFile(log.toFile(), "r");
+             OutputStream out = new 
BufferedOutputStream(Files.newOutputStream(target))) {
+            long from = Math.max(0, in.length() - max);
+            in.seek(from);
+            if (from > 0) {
+                int b;
+                do {
+                    b = in.read();
+                } while (b != -1 && b != '\n');
+            }
+            byte[] buffer = new byte[8192];
+            int n;
+            while ((n = in.read(buffer)) != -1) {
+                out.write(buffer, 0, n);
+            }
+        }
+    }
+
+    // a previous run, with its log, or null
+    private static Info read(Path info) {
+        JsonObject jo = readJson(info);
+        String name = jo != null ? jo.getString("name") : null;
+        if (name == null) {
+            return null;
+        }
+        String file = info.getFileName().toString();
+        Path log = info.resolveSibling(file.substring(0, file.length() - 
INFO_SUFFIX.length()) + LOG_SUFFIX);
+        if (!Files.isRegularFile(log)) {
+            return null;
+        }
+        return new Info(
+                name, jo.getLongOrDefault("pid", 0), jo.getString("directory"),
+                jo.getLongOrDefault("started", 0), 
jo.getLongOrDefault("ended", 0), jo.getInteger("exitCode"),
+                jo.getString("signal"), jo.getString("failure"), log);
+    }
+
+    private static JsonObject readJson(Path file) {
+        try {
+            if (!Files.isRegularFile(file)) {
+                return null;
+            }
+            String text = Files.readString(file);
+            return text.isBlank() ? null : Jsoner.deserialize(text, 
(JsonObject) null);
+        } catch (Exception e) {
+            // being written, or gone
+            return null;
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunPreviousLogTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunPreviousLogTest.java
new file mode 100644
index 000000000000..ed1805fff95c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunPreviousLogTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.camel.FailedToStartRouteException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.PreviousRun;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.main.KameletMain;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * CAMEL-25784: camel run keeps its log as the previous log of the integration 
when it ends.
+ */
+class RunPreviousLogTest extends CamelCommandBaseTestSupport {
+
+    private Path log;
+
+    @BeforeEach
+    void logOfTheRun() throws Exception {
+        useTestHomeDir();
+        Files.createDirectories(CommandLineHelper.getCamelDir());
+        log = Files.write(CommandLineHelper.getCamelDir().resolve("4242.log"), 
List.of("Starting", "Failed"));
+    }
+
+    @Test
+    void theLogOfARunThatFailedToStartIsKept() throws Exception {
+        KameletMain main = mock(KameletMain.class);
+        doThrow(new FailedToStartRouteException("route1", 
"boom")).when(main).start();
+        Run run = commandLineRun();
+
+        assertThatThrownBy(() -> 
run.runKameletMain(main)).isInstanceOf(FailedToStartRouteException.class);
+
+        // Camel did not get as far as a CamelContext: the name of the run
+        PreviousRun.Info info = only(PreviousRun.find("orders"));
+        assertThat(info.pid()).isEqualTo(ProcessHandle.current().pid());
+        // the exit code the CLI exits with
+        assertThat(info.exitCode()).isEqualTo(CliExitCode.INTERNAL);
+        assertThat(info.failure()).isEqualTo("Failed to start route: route1 
because: boom");
+        assertThat(Files.readAllLines(info.log())).containsExactly("Starting", 
"Failed");
+        assertThat(log).doesNotExist();
+    }
+
+    @Test
+    void theLogOfARunWithoutRoutesIsKeptByTheNameOfItsCamelContext() throws 
Exception {
+        KameletMain main = mock(KameletMain.class);
+        DefaultCamelContext context = new DefaultCamelContext();
+        context.getCamelContextExtension().setName("orders-eu");
+        when(main.getCamelContext()).thenReturn(context);
+        Run run = commandLineRun();
+
+        assertThat(run.runKameletMain(main)).isEqualTo(CliExitCode.FAILURE);
+
+        PreviousRun.Info info = only(PreviousRun.find("orders-eu"));
+        assertThat(info.exitCode()).isEqualTo(CliExitCode.FAILURE);
+        assertThat(info.failure()).isEqualTo("No routes found");
+    }
+
+    @Test
+    void theLogOfARunThatWasStoppedIsKept() throws Exception {
+        KameletMain main = mock(KameletMain.class);
+        // no exit code: stopped from outside, such as by camel stop
+        when(main.getExitCode()).thenReturn(Integer.MIN_VALUE);
+        Run run = commandLineRun();
+
+        assertThat(run.runKameletMain(main)).isZero();
+
+        PreviousRun.Info info = only(PreviousRun.find("orders"));
+        assertThat(info.exitCode()).isZero();
+        assertThat(info.failure()).isNull();
+    }
+
+    @Test
+    void aCamelThatAnotherCommandRunsRemovesItsLog() throws Exception {
+        KameletMain main = mock(KameletMain.class);
+        // such as camel cmd send --local, which runs a Camel of its own
+        Run run = new Run(new CamelJBangMain().withPrinter(printer));
+        run.logFile = log;
+
+        assertThat(run.runKameletMain(main)).isZero();
+
+        assertThat(log).doesNotExist();
+        assertThat(PreviousRun.findAll()).isEmpty();
+    }
+
+    // camel run on the command line
+    private Run commandLineRun() {
+        Run run = new Run(new CamelJBangMain().withPrinter(printer));
+        new CommandLine(run);
+        run.name = "orders";
+        run.logFile = log;
+        return run;
+    }
+
+    private static PreviousRun.Info only(List<PreviousRun.Info> found) {
+        assertThat(found).hasSize(1);
+        return found.get(0);
+    }
+}
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 1380faaf597c..c7873d39c790 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
@@ -17,6 +17,7 @@
 package org.apache.camel.dsl.jbang.core.commands.action;
 
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.List;
@@ -26,6 +27,7 @@ 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;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.PreviousRun;
 import org.apache.camel.dsl.jbang.core.common.StringPrinter;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -166,6 +168,82 @@ class CamelLogActionTest extends ActionCommandTestSupport {
         assertThat(lines().get(1)).startsWith("orders").endsWith(": Failed");
     }
 
+    @Test
+    void previousPrintsTheLogOfTheRunThatEnded() throws Exception {
+        keepPreviousRun("orders", line(-300, "Starting"), line(-1, "Failed to 
start route route1"));
+
+        assertThat(callWithSingleProcess(command("--previous"))).isZero();
+
+        // which run it was, and how it ended, on stderr; the log on stdout
+        assertThat(printer.getStderr()).startsWith("The previous run of orders 
(pid 4242) ended at ")
+                .endsWith(" with exit code 70: Failed to start route route1 
because: boom");
+        assertThat(stdoutLines()).hasSize(2)
+                .last().asString().endsWith(": Failed to start route route1");
+    }
+
+    @Test
+    void previousTakesTheOptionsOfTheLog() throws Exception {
+        keepPreviousRun("orders", line(-300, "Starting"), line(-60, "Slow"), 
line(-1, "Failed to start route route1"));
+
+        assertThat(callWithSingleProcess(command("--previous", "--tail=2", 
"--grep=start"))).isZero();
+
+        assertThat(stdoutLines()).hasSize(2)
+                .first().asString().endsWith(": Starting");
+    }
+
+    @Test
+    void previousPrintsTheLogOfAnIntegrationThatWasKilled() throws Exception {
+        // the process is gone, and left its log and its status file
+        writeLog(TEST_PID + ".log", line(-60, "Started"), line(-1, 
"Processing"));
+
+        assertThat(callWithSingleProcess(command("--previous"))).isZero();
+
+        assertThat(printer.getStderr()).startsWith("The previous run of orders 
(pid " + TEST_PID + ") ended at ")
+                .endsWith("ago): " + "the process was gone without stopping 
Camel (killed, or its JVM crashed)");
+        assertThat(stdoutLines()).hasSize(2);
+    }
+
+    @Test
+    void previousOfANameWithoutPreviousRunIsNotFound() throws Exception {
+        keepPreviousRun("orders", line(-1, "Failed"));
+        CamelLogAction command = new CamelLogAction(new 
CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(command, "payments", "--previous");
+
+        assertThatThrownBy(() -> callWithSingleProcess(command))
+                .isInstanceOfSatisfying(CliException.class,
+                        e -> 
assertThat(e.getExitCode()).isEqualTo(CliExitCode.NOT_FOUND))
+                .hasMessage("No previous run matches: payments");
+    }
+
+    @Test
+    void previousOfANameThatMatchesSeveralRunsAsksForOne() throws Exception {
+        keepPreviousRun("orders", line(-1, "Failed"));
+        keepPreviousRun("orders-eu", line(-1, "Failed"));
+        CamelLogAction command = new CamelLogAction(new 
CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(command, "ord", "--previous");
+
+        assertThatThrownBy(() -> callWithSingleProcess(command))
+                .isInstanceOfSatisfying(CliException.class,
+                        e -> 
assertThat(e.getExitCode()).isEqualTo(CliExitCode.NOT_FOUND))
+                .hasMessageStartingWith("Name ord matches 2 previous runs: ")
+                .hasMessageEndingWith(". Specify the name of one.");
+    }
+
+    @Test
+    void aNameThatIsNotRunningSaysWhereTheLogOfItsPreviousRunIs() throws 
Exception {
+        writeStatusFile(TEST_PID, "orders");
+        keepPreviousRun("payments", line(-1, "Failed"));
+        CamelLogAction command = new CamelLogAction(new 
CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(command, "payments", "--follow=false");
+
+        assertThatThrownBy(() -> callWithSingleProcess(command))
+                .isInstanceOfSatisfying(CliException.class,
+                        e -> 
assertThat(e.getExitCode()).isEqualTo(CliExitCode.NOT_FOUND))
+                .hasMessageStartingWith("No running Camel integration matches: 
payments. camel log payments --previous"
+                                        + " prints the log of its previous 
run, which ended at ")
+                .hasMessageEndingWith(" with exit code 70: Failed to start 
route route1 because: boom");
+    }
+
     private CamelLogAction command(String... args) throws Exception {
         writeStatusFile(TEST_PID, "orders");
         CamelLogAction command = new CamelLogAction(new 
CamelJBangMain().withPrinter(printer));
@@ -175,6 +253,11 @@ class CamelLogActionTest extends ActionCommandTestSupport {
         return command;
     }
 
+    // the log only (stdout), without the highlighting of --find and --grep
+    private List<String> stdoutLines() {
+        return printer.getStdout().lines().map(l -> 
l.replaceAll("\u001B\\[[;\\d]*m", "")).toList();
+    }
+
     // without the highlighting of --find and --grep
     private List<String> lines() throws Exception {
         return printer.getLines().stream().map(l -> 
l.replaceAll("\u001B\\[[;\\d]*m", "")).toList();
@@ -184,6 +267,13 @@ class CamelLogActionTest extends ActionCommandTestSupport {
         Files.write(CommandLineHelper.getCamelDir().resolve(name), 
List.of(lines));
     }
 
+    // a run of the name that failed to start
+    private static void keepPreviousRun(String name, String... lines) throws 
Exception {
+        Path log = CommandLineHelper.getCamelDir().resolve("4242.log");
+        Files.write(log, List.of(lines));
+        PreviousRun.keep(log, name, 4242, 70, "Failed to start route route1 
because: boom");
+    }
+
     private static String line(long seconds, String message) {
         return line(seconds, "o.a.c.Test", message);
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/DirtyTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/DirtyTest.java
index 0863681675bc..9e1026d7c90e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/DirtyTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/DirtyTest.java
@@ -102,6 +102,27 @@ class DirtyTest extends ProcessCommandTestSupport {
         }
     }
 
+    @Test
+    void testTheLogOfAPreviousRunIsNotDirty() throws Exception {
+        // kept on purpose (CAMEL-25784), and the next run of that name 
replaces it
+        
Files.writeString(CommandLineHelper.getCamelDir().resolve("orders-previous.log"),
 "Started");
+        
Files.writeString(CommandLineHelper.getCamelDir().resolve("orders-previous.json"),
 "{}");
+        // a name can be a number
+        
Files.writeString(CommandLineHelper.getCamelDir().resolve("123-previous.log"), 
"Started");
+
+        Dirty command = new Dirty(new CamelJBangMain().withPrinter(printer));
+        command.clean = true;
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("", printer.getOutput().trim());
+            
assertTrue(Files.exists(CommandLineHelper.getCamelDir().resolve("orders-previous.log")));
+            
assertTrue(Files.exists(CommandLineHelper.getCamelDir().resolve("123-previous.log")));
+        }
+    }
+
     @Test
     void testFileBelongsToTheWholePid() {
         // pid 12 does not own the files of pid 123
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PreviousRunTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PreviousRunTest.java
new file mode 100644
index 000000000000..7dfe2d5b71cc
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PreviousRunTest.java
@@ -0,0 +1,309 @@
+/*
+ * 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.common;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import sun.misc.Signal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-25784: the log of a run that ended is kept by the name of the 
integration, as kubectl logs --previous shows the
+ * log of the previous instance of a container.
+ */
+class PreviousRunTest {
+
+    // pids no process has (above the highest pid of Linux and macOS)
+    private static final long GONE_PID = 999_999_991L;
+    private static final long GONE_PID_2 = 999_999_992L;
+
+    @TempDir
+    Path home;
+
+    private Path previousHome;
+    private Path camelDir;
+
+    @BeforeEach
+    void useHome() throws Exception {
+        previousHome = CommandLineHelper.getHomeDir();
+        CommandLineHelper.useHomeDir(home.toString());
+        camelDir = CommandLineHelper.getCamelDir();
+        Files.createDirectories(camelDir);
+    }
+
+    @AfterEach
+    void restoreHome() {
+        CommandLineHelper.useHomeDir(previousHome.toString());
+    }
+
+    @Test
+    void theLogIsKeptByTheNameOfTheIntegration() throws Exception {
+        Path log = writeLog("123.log", "Starting", "Failed to start route 
route1");
+
+        Path kept = PreviousRun.keep(log, "orders", 123, 70, "Failed to start 
route route1 because: boom");
+
+        assertThat(kept).isEqualTo(camelDir.resolve("orders-previous.log"));
+        assertThat(log).doesNotExist();
+        assertThat(Files.readAllLines(kept)).containsExactly("Starting", 
"Failed to start route route1");
+        PreviousRun.Info info = only(PreviousRun.find("orders"));
+        assertThat(info.pid()).isEqualTo(123);
+        assertThat(info.exitCode()).isEqualTo(70);
+        assertThat(info.failure()).isEqualTo("Failed to start route route1 
because: boom");
+        assertThat(info.directory()).isEqualTo(System.getProperty("user.dir"));
+        assertThat(info.started()).isPositive();
+        assertThat(info.ended()).isGreaterThanOrEqualTo(info.started());
+        assertThat(info.describe()).startsWith("The previous run of orders 
(pid 123) ended at ")
+                .endsWith(" ago) with exit code 70: Failed to start route 
route1 because: boom");
+    }
+
+    @Test
+    void theNextRunThatEndsReplacesIt() throws Exception {
+        PreviousRun.keep(writeLog("123.log", "first run"), "orders", 123, 70, 
"boom");
+        PreviousRun.keep(writeLog("456.log", "second run"), "orders", 456, 0, 
null);
+
+        PreviousRun.Info info = only(PreviousRun.find("orders"));
+        assertThat(info.pid()).isEqualTo(456);
+        assertThat(info.exitCode()).isZero();
+        assertThat(info.failure()).isNull();
+        assertThat(info.describe()).endsWith(" with exit code 0");
+        assertThat(Files.readAllLines(info.log())).containsExactly("second 
run");
+    }
+
+    @Test
+    void aRunIsFoundAsARunningIntegrationIs() throws Exception {
+        PreviousRun.keep(writeLog("5.log", "orders"), "orders", 5, 0, null);
+        PreviousRun.keep(writeLog("6.log", "orders-eu"), "orders-eu", 6, 0, 
null);
+        PreviousRun.keep(writeLog("7.log", "payments"), "payments", 7, 0, 
null);
+
+        // the whole name of a run is that run, a part of it a prefix
+        
assertThat(names(PreviousRun.find("orders"))).containsExactly("orders");
+        
assertThat(names(PreviousRun.find("ord"))).containsExactlyInAnyOrder("orders", 
"orders-eu");
+        
assertThat(names(PreviousRun.find("orders-eu!"))).containsExactly("orders-eu");
+        assertThat(names(PreviousRun.find("ord!"))).isEmpty();
+        
assertThat(names(PreviousRun.find("pay*"))).containsExactly("payments");
+        // a number is the pid it ran as
+        assertThat(names(PreviousRun.find("7"))).containsExactly("payments");
+        assertThat(PreviousRun.find("*")).hasSize(3);
+        assertThat(PreviousRun.find("invoices")).isEmpty();
+    }
+
+    @Test
+    void theNewestRunComesFirst() throws Exception {
+        PreviousRun.keep(writeLog("5.log", "x"), new 
PreviousRun.Info("orders", 5, null, 0, 1000, 0, null, null, null));
+        PreviousRun.keep(writeLog("6.log", "x"), new 
PreviousRun.Info("payments", 6, null, 0, 3000, 0, null, null, null));
+        PreviousRun.keep(writeLog("7.log", "x"), new 
PreviousRun.Info("invoices", 7, null, 0, 2000, 0, null, null, null));
+
+        assertThat(names(PreviousRun.findAll())).containsExactly("payments", 
"invoices", "orders");
+    }
+
+    @Test
+    void aLargeLogKeepsItsEndFromTheStartOfALine() throws Exception {
+        Path log = writeLog("123.log", "first line", "second line", "third");
+        Path target = camelDir.resolve("copy.log");
+
+        // 10 bytes from the end starts in the second line
+        PreviousRun.copyEnd(log, target, 10);
+
+        assertThat(Files.readString(target)).isEqualTo("third" + 
System.lineSeparator());
+    }
+
+    @Test
+    void theLogOfAKilledProcessIsKeptByTheNameInItsStatus() throws Exception {
+        writeLog(GONE_PID + ".log", "Started", "Processing");
+        writeStatus(GONE_PID, "orders");
+
+        PreviousRun.Info info = only(PreviousRun.find("orders"));
+
+        assertThat(info.pid()).isEqualTo(GONE_PID);
+        assertThat(info.exitCode()).isNull();
+        assertThat(info.failure()).isEqualTo(PreviousRun.GONE);
+        assertThat(info.directory()).isEqualTo("/work/orders");
+        assertThat(Files.readAllLines(info.log())).containsExactly("Started", 
"Processing");
+        assertThat(camelDir.resolve(GONE_PID + ".log")).doesNotExist();
+    }
+
+    @Test
+    void theLogOfAKilledProcessOlderThanThePreviousRunIsRemoved() throws 
Exception {
+        PreviousRun.keep(writeLog("123.log", "newer run"), "orders", 123, 0, 
null);
+        Path old = writeLog(GONE_PID + ".log", "older run");
+        Files.setLastModifiedTime(old, FileTime.from(Instant.now().minus(1, 
ChronoUnit.HOURS)));
+        writeStatus(GONE_PID, "orders");
+
+        PreviousRun.Info info = only(PreviousRun.find("orders"));
+
+        assertThat(Files.readAllLines(info.log())).containsExactly("newer 
run");
+        assertThat(old).doesNotExist();
+    }
+
+    @Test
+    void theLogOfAKilledProcessIsComparedByTheTimesOfTheFiles() throws 
Exception {
+        // a run kept by a JVM whose clock is a day ahead of the file system 
(a network file system)
+        long ahead = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1);
+        Path kept = PreviousRun.keep(writeLog("123.log", "older run"),
+                new PreviousRun.Info("orders", 123, null, 0, ahead, 0, null, 
null, null));
+        Files.setLastModifiedTime(kept, FileTime.from(Instant.now().minus(2, 
ChronoUnit.HOURS)));
+        Path killed = writeLog(GONE_PID + ".log", "newer run, killed");
+        Files.setLastModifiedTime(killed, FileTime.from(Instant.now().minus(1, 
ChronoUnit.HOURS)));
+        writeStatus(GONE_PID, "orders");
+
+        PreviousRun.Info info = only(PreviousRun.find("orders"));
+
+        assertThat(info.pid()).isEqualTo(GONE_PID);
+        assertThat(Files.readAllLines(info.log())).containsExactly("newer run, 
killed");
+    }
+
+    @Test
+    void aKeepThatFailsLeavesNoInfoOfTheRunBefore() throws Exception {
+        PreviousRun.keep(writeLog("123.log", "first run"), "orders", 123, 0, 
null);
+        // the log cannot be written: a directory that is not empty has its 
name
+        Path kept = camelDir.resolve("orders-previous.log");
+        Files.delete(kept);
+        Files.createDirectories(kept.resolve("in-the-way"));
+        Path log = writeLog("456.log", "second run");
+
+        assertThat(PreviousRun.keep(log, "orders", 456, 70, "boom")).isNull();
+
+        // what was kept about the first run is not left next to something 
else, and the log of the second run stays
+        assertThat(camelDir.resolve("orders-previous.json")).doesNotExist();
+        assertThat(PreviousRun.find("orders")).isEmpty();
+        assertThat(log).exists();
+    }
+
+    @Test
+    void theLogOfAProcessWithoutNameIsRemovedAfterADay() throws Exception {
+        Path old = writeLog(GONE_PID + ".log", "old");
+        Files.setLastModifiedTime(old, FileTime.from(Instant.now().minus(2, 
ChronoUnit.DAYS)));
+        Path recent = writeLog(GONE_PID_2 + ".log", "recent");
+        Files.setLastModifiedTime(recent, FileTime.from(Instant.now().minus(1, 
ChronoUnit.HOURS)));
+
+        assertThat(PreviousRun.findAll()).isEmpty();
+
+        assertThat(old).doesNotExist();
+        assertThat(recent).exists();
+    }
+
+    @Test
+    void theLogOfARunningProcessIsLeftAlone() throws Exception {
+        long pid = ProcessHandle.current().pid();
+        Path log = writeLog(pid + ".log", "running");
+        Files.setLastModifiedTime(log, FileTime.from(Instant.now().minus(2, 
ChronoUnit.DAYS)));
+        writeStatus(pid, "orders");
+
+        assertThat(PreviousRun.findAll()).isEmpty();
+
+        assertThat(log).exists();
+    }
+
+    @Test
+    void aRunThatEndedKeepsItsLogOnce() throws Exception {
+        Path log = writeLog("123.log", "the run");
+        PreviousRun.Keeper keeper = new PreviousRun.Keeper(log, () -> 
"orders");
+
+        keeper.ended(0, null);
+        // a log of the same name afterwards (such as another run in this JVM) 
is not kept as the JVM exits
+        writeLog("123.log", "after the run");
+        keeper.exiting(0);
+
+        PreviousRun.Info info = only(PreviousRun.find("orders"));
+        assertThat(info.pid()).isEqualTo(ProcessHandle.current().pid());
+        assertThat(info.exitCode()).isZero();
+        assertThat(Files.readAllLines(info.log())).containsExactly("the run");
+    }
+
+    @Test
+    void aRunThatASignalStoppedEndsWithTheExitCodeOfTheJvm() throws Exception {
+        Path log = writeLog("123.log", "the run");
+        PreviousRun.Keeper keeper = new PreviousRun.Keeper(log, () -> 
"orders");
+        // SIGTERM, on which the JVM exits with 128 + the number of the signal
+        keeper.stoppedBy(new Signal("TERM"));
+
+        // Camel stopped as it should, so the run answers 0
+        keeper.ended(0, null);
+
+        PreviousRun.Info info = only(PreviousRun.find("orders"));
+        assertThat(info.exitCode()).isEqualTo(128 + new 
Signal("TERM").getNumber());
+        assertThat(info.signal()).isEqualTo("SIGTERM");
+        assertThat(info.describe()).endsWith(" with exit code " + 
info.exitCode() + " (SIGTERM)");
+    }
+
+    @Test
+    void aJvmThatExitsKeepsTheLogOfARunThatDidNotEnd() throws Exception {
+        Path log = writeLog("123.log", "the run");
+        PreviousRun.Keeper keeper = new PreviousRun.Keeper(log, () -> 
"orders");
+        try {
+            // the run gets a few seconds to end: not in a test
+            keeper.exiting(0);
+
+            PreviousRun.Info info = only(PreviousRun.find("orders"));
+            assertThat(info.exitCode()).isNull();
+            assertThat(Files.readAllLines(info.log())).containsExactly("the 
run");
+        } finally {
+            keeper.ended(null, null);
+        }
+    }
+
+    @Test
+    void aNameIsAFileName() {
+        
assertThat(PreviousRun.fileName("orders-eu.v2")).isEqualTo("orders-eu.v2");
+        assertThat(PreviousRun.fileName("my app/1")).isEqualTo("my_app_1");
+        assertThat(PreviousRun.fileName("..")).isEqualTo("_..");
+    }
+
+    @Test
+    void theFilesOfAPreviousRun() {
+        
assertThat(PreviousRun.isPreviousRunFile("orders-previous.log")).isTrue();
+        
assertThat(PreviousRun.isPreviousRunFile("orders-previous.json")).isTrue();
+        assertThat(PreviousRun.isPreviousRunFile("123.log")).isFalse();
+        assertThat(PreviousRun.isPreviousRunFile("123-status.json")).isFalse();
+    }
+
+    private Path writeLog(String name, String... lines) throws Exception {
+        return Files.write(camelDir.resolve(name), List.of(lines));
+    }
+
+    private void writeStatus(long pid, String name) throws Exception {
+        JsonObject context = new JsonObject();
+        context.put("name", name);
+        JsonObject runtime = new JsonObject();
+        runtime.put("directory", "/work/" + name);
+        JsonObject root = new JsonObject();
+        root.put("context", context);
+        root.put("runtime", runtime);
+        Files.writeString(camelDir.resolve(pid + "-status.json"), 
root.toJson());
+    }
+
+    private static PreviousRun.Info only(List<PreviousRun.Info> found) {
+        assertThat(found).hasSize(1);
+        return found.get(0);
+    }
+
+    private static List<String> names(List<PreviousRun.Info> found) {
+        return found.stream().map(PreviousRun.Info::name).toList();
+    }
+}

Reply via email to