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 458c9b85b75c CAMEL-26713: camel-console - the timeline records the 
faults of the chaos rules and the routes they had down (#29381)
458c9b85b75c is described below

commit 458c9b85b75c78dd2a2ca592eca617f36cea88b6
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Nov 7 19:48:06 2026 +0100

    CAMEL-26713: camel-console - the timeline records the faults of the chaos 
rules and the routes they had down (#29381)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../apache/camel/catalog/dev-consoles-openapi.json | 43 ++++++++++
 .../apache/camel/catalog/dev-consoles/chaos.json   | 17 ++++
 .../camel/catalog/dev-consoles/timeline.json       | 31 +++++++
 .../org/apache/camel/dev-console/chaos.json        | 17 ++++
 .../camel/component/chaos/ChaosDevConsole.java     |  9 +-
 .../camel/component/chaos/ChaosDevConsoleTest.java | 12 +++
 .../org/apache/camel/dev-console/timeline.json     | 31 +++++++
 .../camel/impl/console/TimelineDevConsole.java     | 95 ++++++++++++++++++++--
 .../camel/impl/console/TimelineRecorder.java       | 50 ++++++++++++
 .../camel/impl/console/TimelineDevConsoleTest.java | 49 +++++++++++
 .../camel/impl/console/TimelineRecorderTest.java   | 20 +++++
 .../ROOT/pages/camel-jbang-diagnostics.adoc        |  4 +
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |  5 +-
 .../ROOT/pages/camel-jbang-tui-observe.adoc        |  3 +-
 .../dsl/jbang/core/commands/ai/ToolRegistry.java   |  7 +-
 .../jbang/core/commands/process/ListTimeline.java  | 61 +++++++++++---
 .../core/commands/process/ListTimelineTest.java    | 29 +++++++
 .../dsl/jbang/core/commands/mcp/RuntimeTools.java  |  7 +-
 .../dsl/jbang/core/commands/tui/Timeline.java      | 70 ++++++++++++++++
 .../src/main/resources/tui/help/timeline.md        |  4 +-
 .../dsl/jbang/core/commands/tui/TimelineTest.java  | 40 +++++++++
 21 files changed, 573 insertions(+), 31 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
index 95a590e3d97a..39d0fc65bd30 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
@@ -762,6 +762,18 @@
                                "summary": "Chaos",
                                "description": "The chaos rules and their 
report",
                                "operationId": "chaos",
+                               "parameters": [
+                                       {
+                                               "name": "report",
+                                               "in": "query",
+                                               "description": "Whether to 
include the report; without it only the rules, as the timeline console reads 
them each second",
+                                               "required": false,
+                                               "schema": {
+                                                       "type": "boolean",
+                                                       "default": true
+                                               }
+                                       }
+                               ],
                                "responses": {
                                        "200": {
                                                "description": "Chaos output",
@@ -9143,6 +9155,13 @@
                                                                                
                                        "type": "integer"
                                                                                
                                },
                                                                                
                                "description": "Redelivery attempts per second 
in this route"
+                                                                               
                        },
+                                                                               
                        "down": {
+                                                                               
                                "type": "array",
+                                                                               
                                "items": {
+                                                                               
                                        "type": "integer"
+                                                                               
                                },
+                                                                               
                                "description": "Per second 1 when a camel-chaos 
rule had the route down (stall or a lifecycle fault), else 0 (only present with 
camel-chaos)"
                                                                                
                        }
                                                                                
                }
                                                                                
        },
@@ -9180,6 +9199,30 @@
                                                                                
        },
                                                                                
        "description": "The errors by route and exception (only present when 
there are any)"
                                                                                
},
+                                                                               
"chaos": {
+                                                                               
        "type": "array",
+                                                                               
        "items": {
+                                                                               
                "type": "object",
+                                                                               
                "properties": {
+                                                                               
                        "id": {
+                                                                               
                                "type": "string",
+                                                                               
                                "description": "The id of the camel-chaos rule"
+                                                                               
                        },
+                                                                               
                        "rule": {
+                                                                               
                                "type": "string",
+                                                                               
                                "description": "The rule in its one-line text 
form"
+                                                                               
                        },
+                                                                               
                        "faults": {
+                                                                               
                                "type": "array",
+                                                                               
                                "items": {
+                                                                               
                                        "type": "integer"
+                                                                               
                                },
+                                                                               
                                "description": "The faults the rule fired per 
second"
+                                                                               
                        }
+                                                                               
                }
+                                                                               
        },
+                                                                               
        "description": "The faults per second of each camel-chaos rule that 
fired in the seconds (only present when one did)"
+                                                                               
},
                                                                                
"exchanges": {
                                                                                
        "type": "array",
                                                                                
        "items": {
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/chaos.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/chaos.json
index 26a404083cf0..b5b81840929f 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/chaos.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/chaos.json
@@ -12,6 +12,23 @@
     "artifactId": "camel-chaos",
     "version": "4.24.0-SNAPSHOT"
   },
+  "options": {
+    "report": {
+      "index": 0,
+      "kind": "option",
+      "displayName": "Report",
+      "group": "query",
+      "label": "query",
+      "required": false,
+      "type": "boolean",
+      "javaType": "java.lang.Boolean",
+      "deprecated": false,
+      "autowired": false,
+      "secret": false,
+      "defaultValue": true,
+      "description": "Whether to include the report; without it only the 
rules, as the timeline console reads them each second"
+    }
+  },
   "response": {
     "type": "object",
     "properties": {
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/timeline.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/timeline.json
index b7455fb47488..fc9b5da8c4b2 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/timeline.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/timeline.json
@@ -150,6 +150,13 @@
                 "type": "integer"
               },
               "description": "Redelivery attempts per second in this route"
+            },
+            "down": {
+              "type": "array",
+              "items": {
+                "type": "integer"
+              },
+              "description": "Per second 1 when a camel-chaos rule had the 
route down (stall or a lifecycle fault), else 0 (only present with camel-chaos)"
             }
           }
         },
@@ -187,6 +194,30 @@
         },
         "description": "The errors by route and exception (only present when 
there are any)"
       },
+      "chaos": {
+        "type": "array",
+        "items": {
+          "type": "object",
+          "properties": {
+            "id": {
+              "type": "string",
+              "description": "The id of the camel-chaos rule"
+            },
+            "rule": {
+              "type": "string",
+              "description": "The rule in its one-line text form"
+            },
+            "faults": {
+              "type": "array",
+              "items": {
+                "type": "integer"
+              },
+              "description": "The faults the rule fired per second"
+            }
+          }
+        },
+        "description": "The faults per second of each camel-chaos rule that 
fired in the seconds (only present when one did)"
+      },
       "exchanges": {
         "type": "array",
         "items": {
diff --git 
a/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/dev-console/chaos.json
 
b/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/dev-console/chaos.json
index 26a404083cf0..b5b81840929f 100644
--- 
a/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/dev-console/chaos.json
+++ 
b/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/dev-console/chaos.json
@@ -12,6 +12,23 @@
     "artifactId": "camel-chaos",
     "version": "4.24.0-SNAPSHOT"
   },
+  "options": {
+    "report": {
+      "index": 0,
+      "kind": "option",
+      "displayName": "Report",
+      "group": "query",
+      "label": "query",
+      "required": false,
+      "type": "boolean",
+      "javaType": "java.lang.Boolean",
+      "deprecated": false,
+      "autowired": false,
+      "secret": false,
+      "defaultValue": true,
+      "description": "Whether to include the report; without it only the 
rules, as the timeline console reads them each second"
+    }
+  },
   "response": {
     "type": "object",
     "properties": {
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosDevConsole.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosDevConsole.java
index 2bd5f625912c..c7d733c415ea 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosDevConsole.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosDevConsole.java
@@ -70,6 +70,11 @@ public class ChaosDevConsole extends AbstractDevConsole {
             @Metadata(description = "The report: verdict, final, seed, 
seedGiven, lost, reasons, all rules with the removed and replaced ones, the 
counts per route, and the recent faults (the last 20, the latest first: 
timestamp, ago, rule, version, fault, hook, route, endpoint or step, 
exchangeId) (only when chaos is on)") Map<String, Object> report) {
     }
 
+    @Metadata(label = "query",
+              description = "Whether to include the report; without it only 
the rules, as the timeline console reads them each second",
+              javaType = "java.lang.Boolean", defaultValue = "true")
+    public static final String REPORT = "report";
+
     public ChaosDevConsole() {
         super("camel", "chaos", "Chaos", "The chaos rules and their report");
     }
@@ -95,7 +100,9 @@ public class ChaosDevConsole extends AbstractDevConsole {
         // one report, so the rules and the report are of the same moment
         ChaosReport report = chaos.report();
         root.put("rules", report.rulesJson(true));
-        root.put("report", report.toJson());
+        if (optionBoolean(options, REPORT, true)) {
+            root.put("report", report.toJson());
+        }
         return root;
     }
 
diff --git 
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosDevConsoleTest.java
 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosDevConsoleTest.java
index d22af83dce32..f699be8818fb 100644
--- 
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosDevConsoleTest.java
+++ 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosDevConsoleTest.java
@@ -119,6 +119,18 @@ class ChaosDevConsoleTest {
                 .map(r -> r.getString("route") + " failed " + 
r.getLong("failed"))).containsExactly("start failed 1");
     }
 
+    @Test
+    void withoutTheReportOnlyTheRules() throws Exception {
+        DevConsole console = start(null, Map.of("camel.chaos.enabled", 
"true"));
+        ChaosManager.get(context).addRule("endpoint=mock:target error rate=1 
until=1m");
+
+        // as the timeline console reads the rules each second
+        JsonObject root = json(console, Map.of(ChaosDevConsole.REPORT, 
"false"));
+        assertThat(root.getBoolean("enabled")).isTrue();
+        assertThat(root.<JsonArray> getCollection("rules")).hasSize(1);
+        assertThat(root).doesNotContainKey("report");
+    }
+
     @Test
     void text() throws Exception {
         DevConsole console = start(null, Map.of("camel.chaos.enabled", "true", 
"camel.chaos.seed", "7"));
diff --git 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/timeline.json
 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/timeline.json
index b7455fb47488..fc9b5da8c4b2 100644
--- 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/timeline.json
+++ 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/timeline.json
@@ -150,6 +150,13 @@
                 "type": "integer"
               },
               "description": "Redelivery attempts per second in this route"
+            },
+            "down": {
+              "type": "array",
+              "items": {
+                "type": "integer"
+              },
+              "description": "Per second 1 when a camel-chaos rule had the 
route down (stall or a lifecycle fault), else 0 (only present with camel-chaos)"
             }
           }
         },
@@ -187,6 +194,30 @@
         },
         "description": "The errors by route and exception (only present when 
there are any)"
       },
+      "chaos": {
+        "type": "array",
+        "items": {
+          "type": "object",
+          "properties": {
+            "id": {
+              "type": "string",
+              "description": "The id of the camel-chaos rule"
+            },
+            "rule": {
+              "type": "string",
+              "description": "The rule in its one-line text form"
+            },
+            "faults": {
+              "type": "array",
+              "items": {
+                "type": "integer"
+              },
+              "description": "The faults the rule fired per second"
+            }
+          }
+        },
+        "description": "The faults per second of each camel-chaos rule that 
fired in the seconds (only present when one did)"
+      },
       "exchanges": {
         "type": "array",
         "items": {
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/TimelineDevConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/TimelineDevConsole.java
index 128206984786..cdff0369c659 100644
--- 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/TimelineDevConsole.java
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/TimelineDevConsole.java
@@ -21,8 +21,10 @@ import java.util.Arrays;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
@@ -35,26 +37,31 @@ import org.apache.camel.NonManagedService;
 import org.apache.camel.Route;
 import org.apache.camel.api.management.ManagedCamelContext;
 import org.apache.camel.api.management.mbean.ManagedRouteMBean;
+import org.apache.camel.console.DevConsole;
+import org.apache.camel.console.DevConsoleRegistry;
 import org.apache.camel.spi.CamelEvent;
 import org.apache.camel.spi.Configurer;
 import org.apache.camel.spi.Metadata;
-import org.apache.camel.spi.annotations.DevConsole;
 import org.apache.camel.support.EventNotifierSupport;
 import org.apache.camel.support.ExchangeHelper;
 import org.apache.camel.support.PatternHelper;
 import org.apache.camel.support.console.AbstractDevConsole;
 import org.apache.camel.util.json.JsonRecordSupport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * The timeline of the integration: per second, for the last minutes, what 
each route did, the errors by exception, and
  * a few exchanges of each second with the calls they made. It records from 
the start of the console, so the history is
  * there when a tool first looks.
  */
-@DevConsole(name = "timeline", displayName = "Camel Timeline",
-            description = "Per second, what each route did, the errors, and 
the failed and slowest exchanges")
[email protected](name = "timeline", displayName = 
"Camel Timeline",
+                                             description = "Per second, what 
each route did, the errors, and the failed and slowest exchanges")
 @Configurer(extended = true)
 public class TimelineDevConsole extends AbstractDevConsole {
 
+    private static final Logger LOG = 
LoggerFactory.getLogger(TimelineDevConsole.class);
+
     /** The exchange property camel-chaos marks a faulted exchange with, with 
the id of the rule. */
     static final String CHAOS_RULE = "CamelChaosRule";
 
@@ -74,7 +81,14 @@ public class TimelineDevConsole extends AbstractDevConsole {
             @Metadata(description = "Mean processing time per second in 
milliseconds") List<Long> meanMillis,
             @Metadata(description = "p95 processing time per second in 
milliseconds, of the exchanges that came into this route") List<Long> p95Millis,
             @Metadata(description = "Max processing time per second in 
milliseconds, of the exchanges that came into this route") List<Long> maxMillis,
-            @Metadata(description = "Redelivery attempts per second in this 
route") List<Long> redeliveryAttempts) {
+            @Metadata(description = "Redelivery attempts per second in this 
route") List<Long> redeliveryAttempts,
+            @Metadata(description = "Per second 1 when a camel-chaos rule had 
the route down (stall or a lifecycle fault), else 0 (only present with 
camel-chaos)") List<Long> down) {
+    }
+
+    public record ChaosSeries(
+            @Metadata(description = "The id of the camel-chaos rule") String 
id,
+            @Metadata(description = "The rule in its one-line text form") 
String rule,
+            @Metadata(description = "The faults the rule fired per second") 
List<Long> faults) {
     }
 
     public record ErrorSeries(
@@ -105,6 +119,7 @@ public class TimelineDevConsole extends AbstractDevConsole {
             @Metadata(description = "Exchanges done per second, in the routes 
they came in") List<Long> exchangesDone,
             @Metadata(description = "The routes") List<RouteSeries> routes,
             @Metadata(description = "The errors by route and exception (only 
present when there are any)") List<ErrorSeries> errors,
+            @Metadata(description = "The faults per second of each camel-chaos 
rule that fired in the seconds (only present when one did)") List<ChaosSeries> 
chaos,
             @Metadata(description = "Per second the failed, handled and 
recovered exchanges (up to 10) and the 3 slowest, oldest first (only present 
when asked for)") List<TimelineExchange> exchanges) {
     }
 
@@ -176,7 +191,52 @@ public class TimelineDevConsole extends AbstractDevConsole 
{
     }
 
     void sample() {
-        recorder.sample(System.currentTimeMillis(), counters());
+        long now = System.currentTimeMillis();
+        recorder.sample(now, counters());
+        sampleChaos(now);
+    }
+
+    /**
+     * With camel-chaos, the faults per second of its rules and the routes 
they have down, from its dev console (so this
+     * console does not depend on camel-chaos).
+     */
+    private void sampleChaos(long now) {
+        DevConsoleRegistry registry = 
getCamelContext().getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class);
+        DevConsole chaos = registry != null ? registry.resolveById("chaos") : 
null;
+        if (chaos == null) {
+            return;
+        }
+        Object json;
+        try {
+            json = chaos.call(DevConsole.MediaType.JSON, Map.of("report", 
"false"));
+        } catch (Exception e) {
+            LOG.debug("Failed to sample the chaos dev console", e);
+            return;
+        }
+        if (json instanceof Map<?, ?> root && root.get("rules") instanceof 
List<?> rules) {
+            Map<String, long[]> faults = new LinkedHashMap<>();
+            Map<String, String> texts = new LinkedHashMap<>();
+            Set<String> down = new LinkedHashSet<>();
+            for (Object o : rules) {
+                if (o instanceof Map<?, ?> r && r.get("id") instanceof String 
id) {
+                    if (r.get("rule") instanceof String text) {
+                        texts.put(id, text);
+                    }
+                    if (r.get("faultsPerSecond") instanceof List<?> perSecond) 
{
+                        long[] values = new long[perSecond.size()];
+                        for (int i = 0; i < values.length; i++) {
+                            values[i] = perSecond.get(i) instanceof Number n ? 
n.longValue() : 0;
+                        }
+                        faults.put(id, values);
+                    }
+                    if (r.get("lifecycle") instanceof Map<?, ?> lifecycle && 
"down".equals(lifecycle.get("state"))
+                            && lifecycle.get("route") instanceof String route) 
{
+                        down.add(route);
+                    }
+                }
+            }
+            recorder.chaos(now, faults, texts, down);
+        }
     }
 
     private Map<String, TimelineRecorder.Counters> counters() {
@@ -232,6 +292,7 @@ public class TimelineDevConsole extends AbstractDevConsole {
         List<ErrorSeries> errors = new ArrayList<>();
         List<TimelineExchange> kept = new ArrayList<>();
         List<Long> done = new ArrayList<>(count);
+        List<ChaosSeries> chaos = new ArrayList<>();
         // the from endpoint of each route, read before the recorder is 
locked: the routes have a lock of their own, and
         // the exchange events wait for the recorder
         Map<String, String> fromUris = new HashMap<>();
@@ -242,6 +303,7 @@ public class TimelineDevConsole extends AbstractDevConsole {
         }
         synchronized (rec) {
             secs = rec.seconds(end, count);
+            boolean chaosSeen = !rec.chaosRules().isEmpty();
             for (TimelineRecorder.Second s : secs) {
                 done.add(s != null ? s.done : 0L);
             }
@@ -253,8 +315,18 @@ public class TimelineDevConsole extends AbstractDevConsole 
{
                         series(secs, routeId, rs -> rs.handled), series(secs, 
routeId, rs -> rs.failed),
                         series(secs, routeId, rs -> rs.inflight), series(secs, 
routeId, TimelineRecorder.RouteSecond::mean),
                         series(secs, routeId, rs -> rs.p95), series(secs, 
routeId, rs -> rs.max),
-                        series(secs, routeId, rs -> rs.attempts)));
+                        series(secs, routeId, rs -> rs.attempts), chaosSeen ? 
downSeries(secs, routeId) : null));
+            }
+            Map<String, Long[]> faults = new LinkedHashMap<>();
+            for (int i = 0; i < secs.size(); i++) {
+                TimelineRecorder.Second s = secs.get(i);
+                if (s != null && s.faults != null) {
+                    for (Map.Entry<String, Long> e : s.faults.entrySet()) {
+                        faults.computeIfAbsent(e.getKey(), k -> 
zeros(secs.size()))[i] = e.getValue();
+                    }
+                }
             }
+            faults.forEach((id, v) -> chaos.add(new ChaosSeries(id, 
rec.chaosRules().get(id), List.of(v))));
             Map<TimelineRecorder.ErrorKind, Long[]> byKind = new 
LinkedHashMap<>();
             for (int i = 0; i < secs.size(); i++) {
                 TimelineRecorder.Second s = secs.get(i);
@@ -283,7 +355,16 @@ public class TimelineDevConsole extends AbstractDevConsole 
{
                     new ErrorSeries(k.routeId(), k.exception(), 
k.statusCode(), k.chaosRule(), List.of(v))));
         }
         return new Response(
-                end, count, done, routes, errors.isEmpty() ? null : errors, 
exchanges ? kept : null);
+                end, count, done, routes, errors.isEmpty() ? null : errors, 
chaos.isEmpty() ? null : chaos,
+                exchanges ? kept : null);
+    }
+
+    private static List<Long> downSeries(List<TimelineRecorder.Second> secs, 
String routeId) {
+        List<Long> answer = new ArrayList<>(secs.size());
+        for (TimelineRecorder.Second s : secs) {
+            answer.add(s != null && s.down != null && s.down.contains(routeId) 
? 1L : 0L);
+        }
+        return answer;
     }
 
     private static TimelineExchange exchange(TimelineRecorder.Kept k) {
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/TimelineRecorder.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/TimelineRecorder.java
index 843e14491939..72288f016cc9 100644
--- 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/TimelineRecorder.java
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/TimelineRecorder.java
@@ -122,6 +122,9 @@ final class TimelineRecorder {
         final Map<ErrorKind, long[]> errors = new LinkedHashMap<>();
         final List<Kept> failed = new ArrayList<>();
         final List<Kept> slowest = new ArrayList<>();
+        // with camel-chaos: the faults of each rule, and the routes a rule 
had down
+        Map<String, Long> faults;
+        Set<String> down;
         long done;
         boolean closed;
 
@@ -187,6 +190,8 @@ final class TimelineRecorder {
     private final Second[] ring;
     private final Map<String, Inflight> inflight = new ConcurrentHashMap<>();
     private final Map<String, Counters> lastCounters = new HashMap<>();
+    // the text of each chaos rule seen, by its id
+    private final Map<String, String> chaosRules = new LinkedHashMap<>();
     private long lastSampleSecond = -1;
 
     TimelineRecorder(int seconds) {
@@ -370,6 +375,51 @@ final class TimelineRecorder {
         inflight.values().removeIf(f -> f.created < cutoff);
     }
 
+    /**
+     * Takes the state of the chaos rules at the given time: the faults per 
second of each rule (the last seconds, the
+     * last one the current second), and the routes a rule has down now. The 
faults replace the ones of those seconds,
+     * as a rule counts the faults of a second until it is over; the routes 
down are the ones of the second before.
+     *
+     * @param faults the faults per second of each rule by its id, the oldest 
first and the current second last
+     * @param rules  the text of each rule by its id
+     * @param down   the routes a rule has down now
+     */
+    synchronized void chaos(long now, Map<String, long[]> faults, Map<String, 
String> rules, Set<String> down) {
+        long second = now / 1000;
+        chaosRules.putAll(rules);
+        for (Map.Entry<String, long[]> e : faults.entrySet()) {
+            long[] values = e.getValue();
+            // the seconds that are over, at most the window
+            int n = Math.min(values.length - 1, seconds);
+            for (int i = 1; i <= n; i++) {
+                long value = values[values.length - 1 - i];
+                Second s = value > 0 ? slot(second - i) : existing(second - i);
+                if (s == null) {
+                    continue;
+                }
+                if (value > 0) {
+                    if (s.faults == null) {
+                        s.faults = new LinkedHashMap<>();
+                    }
+                    s.faults.put(e.getKey(), value);
+                } else if (s.faults != null) {
+                    s.faults.remove(e.getKey());
+                }
+            }
+        }
+        if (!down.isEmpty()) {
+            Second s = slot(second - 1);
+            if (s.down == null) {
+                s.down = new LinkedHashSet<>();
+            }
+            s.down.addAll(down);
+        }
+    }
+
+    Map<String, String> chaosRules() {
+        return chaosRules;
+    }
+
     private static long rise(long before, long now) {
         return Math.max(0, now - before);
     }
diff --git 
a/core/camel-console/src/test/java/org/apache/camel/impl/console/TimelineDevConsoleTest.java
 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/TimelineDevConsoleTest.java
index d20ea8eefd9d..19a04583e729 100644
--- 
a/core/camel-console/src/test/java/org/apache/camel/impl/console/TimelineDevConsoleTest.java
+++ 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/TimelineDevConsoleTest.java
@@ -16,15 +16,20 @@
  */
 package org.apache.camel.impl.console;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.camel.ContextTestSupport;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.console.DevConsole;
+import org.apache.camel.console.DevConsoleRegistry;
 import org.apache.camel.support.PluginHelper;
+import org.apache.camel.support.console.AbstractDevConsole;
 import org.apache.camel.support.service.ServiceHelper;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
@@ -169,6 +174,50 @@ public class TimelineDevConsoleTest extends 
ContextTestSupport {
         });
     }
 
+    @Test
+    public void withChaosTheFaultsOfTheRulesAndTheRoutesTheyHaveDown() {
+        AtomicBoolean askedWithoutReport = new AtomicBoolean();
+        // camel-chaos is not on the classpath here: a console of its id that 
answers as it does
+        AbstractDevConsole chaos = new AbstractDevConsole("camel", "chaos", 
"Chaos", "fake") {
+            @Override
+            protected String doCallText(Map<String, Object> options) {
+                return "";
+            }
+
+            @Override
+            protected Map<String, Object> doCallJson(Map<String, Object> 
options) {
+                // the timeline asks without the report
+                askedWithoutReport.set("false".equals(options.get("report")));
+                JsonObject rule = new JsonObject();
+                rule.put("id", "r1");
+                rule.put("rule", "route=failing stall duration=20s");
+                List<Long> faults = new ArrayList<>(Collections.nCopies(60, 
1L));
+                rule.put("faultsPerSecond", faults);
+                rule.put("lifecycle", Map.of("route", "failing", "state", 
"down"));
+                JsonObject root = new JsonObject();
+                root.put("enabled", true);
+                root.put("rules", List.of(rule));
+                return root;
+            }
+        };
+        chaos.setCamelContext(context);
+        
context.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class).register(chaos);
+        startConsole();
+
+        await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
+            JsonObject out = (JsonObject) 
console.call(DevConsole.MediaType.JSON, Map.of(TimelineDevConsole.SECONDS, 
"10"));
+            JsonArray series = out.getCollection("chaos");
+            assertThat(series).isNotNull();
+            JsonObject r1 = (JsonObject) series.get(0);
+            assertThat(r1.getString("id")).isEqualTo("r1");
+            assertThat(r1.getString("rule")).isEqualTo("route=failing stall 
duration=20s");
+            assertThat(r1.<List<Number>> getCollection("faults")).contains(1L);
+            assertThat(sum(route(out, "failing"), "down")).isGreaterThan(0);
+            assertThat(sum(route(out, "orders"), "down")).isZero();
+        });
+        assertThat(askedWithoutReport.get()).isTrue();
+    }
+
     @Override
     protected RouteBuilder createRouteBuilder() {
         return new RouteBuilder() {
diff --git 
a/core/camel-console/src/test/java/org/apache/camel/impl/console/TimelineRecorderTest.java
 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/TimelineRecorderTest.java
index ac8e3762e53e..c775730063e9 100644
--- 
a/core/camel-console/src/test/java/org/apache/camel/impl/console/TimelineRecorderTest.java
+++ 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/TimelineRecorderTest.java
@@ -18,6 +18,7 @@ package org.apache.camel.impl.console;
 
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import org.apache.camel.impl.console.TimelineRecorder.Counters;
 import org.apache.camel.impl.console.TimelineRecorder.RouteSecond;
@@ -239,6 +240,25 @@ class TimelineRecorderTest {
         assertThat(recorder.inflightCount()).isZero();
     }
 
+    @Test
+    void theFaultsOfTheChaosRulesAndTheRoutesTheyHaveDown() {
+        // at T + 2s: r1 fired 3 faults two seconds ago, 1 a second ago, and 2 
in the second going on
+        recorder.chaos(T + 2000, Map.of("r1", new long[] { 0, 3, 1, 2 }), 
Map.of("r1", "endpoint=http:stock* error"),
+                Set.of("shipping"));
+
+        List<Second> secs = recorder.seconds(second(T) + 1, 2);
+        assertThat(secs.get(0).faults).containsEntry("r1", 3L);
+        assertThat(secs.get(1).faults).containsEntry("r1", 1L);
+        assertThat(secs.get(1).down).containsExactly("shipping");
+        assertThat(secs.get(0).down).isNull();
+        assertThat(recorder.chaosRules()).containsEntry("r1", 
"endpoint=http:stock* error");
+
+        // a second later the rule counts the second before differently: it 
replaces it
+        recorder.chaos(T + 3000, Map.of("r1", new long[] { 3, 1, 4, 0 }), 
Map.of(), Set.of());
+        assertThat(recorder.seconds(second(T) + 2, 
1).get(0).faults).containsEntry("r1", 4L);
+        assertThat(recorder.seconds(second(T) + 1, 
1).get(0).faults).containsEntry("r1", 1L);
+    }
+
     @Test
     void theRingKeepsTheRecordedSecondsOnly() {
         recorder.exchangeDone("OLD", "orders", null, T, 1, false, false, null, 
null, null, false);
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-diagnostics.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-diagnostics.adoc
index 8483a2fd3911..b040e5420964 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-diagnostics.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-diagnostics.adoc
@@ -61,6 +61,10 @@ camel get timeline --watch
 error. `--exchanges` lists the exchanges kept per second: the failed, handled 
and recovered ones and the 3 slowest, with their
 status (`OK`, `RECOVERED`, `HANDLED` or `FAILED`), elapsed time, and the 
number of calls to remote endpoints.
 
+With xref:next@components:others:chaos.adoc[camel-chaos], the timeline also 
has the faults of each chaos rule per
+second (listed by `--errors` as `chaos faults`), and the seconds a stall or 
lifecycle rule had a route down (the `DOWN`
+column).
+
 The integration records the timeline itself, for the last 5 minutes, with the 
`timeline` dev console. It is on when
 the dev consoles are, as with the `dev` profile that `camel run` uses by 
default. The number of seconds is set with
 `camel.devConsole.timeline.seconds` (60 to 3600).
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index 30cc6f294f8a..4692723d5c00 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -972,8 +972,9 @@ given, and a call that names its pid is refused.
 | `camel_runtime_timeline`
 | What happened per second in the last minutes (up to 5): per route the 
exchanges completed, recovered by a
   redelivery, handled by the error handler and failed, with their mean, p95 
and max time and the redelivery attempts;
-  the errors per second by route and exception; and per second the failed, 
handled and recovered exchanges and the 3 slowest
-  with the remote calls they made. It is recorded by the `timeline` dev 
console, which `camel run` (dev profile) turns on.
+  the errors per second by route and exception; with camel-chaos, the faults 
per second of each chaos rule and the
+  seconds a rule had a route down; and per second the failed, handled and 
recovered exchanges and the 3 slowest with the remote
+  calls they made. It is recorded by the `timeline` dev console, which `camel 
run` (dev profile) turns on.
 
 | `camel_runtime_memory`
 | Show JVM memory usage (heap/non-heap), garbage collection stats, and thread 
counts.
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
index c371b776248f..d482709ebd13 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
@@ -44,7 +44,8 @@ image::jbang/camel-tui-timeline-exchanges.png[The exchanges 
of a second in the o
 *Shift+F8* asks the AI to explain what happened, as a story of cause and 
effect.
 
 The integration records the timeline itself, for the last 5 minutes, with the 
`timeline` dev console, which is on in
-the `dev` profile of `camel run`. So the tab opens with the last minutes, also 
when the TUI started after the
+the `dev` profile of `camel run`. With camel-chaos it records the faults of 
the chaos rules and the seconds they had a
+route down too. So the tab opens with the last minutes, also when the TUI 
started after the
 integration, and `camel get timeline` shows the same from the command line
 (see xref:camel-jbang-diagnostics.adoc#_timeline[Diagnostics]).
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
index 582535fb06b0..dc491234c363 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
@@ -335,9 +335,10 @@ public final class ToolRegistry {
                                       + " recovered by a redelivery, handled 
by the error handler and failed, the mean, p95"
                                       + " and max time, the exchanges in 
flight and the redelivery attempts; the errors per"
                                       + " second by route and exception 
(chaosRule names the camel-chaos rule that made"
-                                      + " one); and per second the failed, 
handled and recovered exchanges and the 3 slowest with the remote"
-                                      + " calls they made. Each series is 
oldest first and ends at the epoch second end."
-                                      + " Use it to tell when something went 
wrong and what it did to which route.")
+                                      + " one); with camel-chaos the faults 
per second of each chaos rule (chaos) and the"
+                                      + " seconds a rule had a route down 
(down of the route); and per second the failed,"
+                                      + " handled and recovered exchanges and 
the 3 slowest with the remote calls they made. Each series is"
+                                      + " oldest first and ends at the epoch 
second end. Use it to tell when something went wrong and what it did to which 
route.")
                 .param("seconds", "integer", "The number of seconds (default 
60, at most what the integration records)",
                         false)
                 .param("routeId", "string", "Route ID or pattern such as 
orders* (default all routes)", false)
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListTimeline.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListTimeline.java
index 530c53b98b41..18c94ae9df3c 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListTimeline.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListTimeline.java
@@ -203,6 +203,8 @@ public class ListTimeline extends ProcessWatchCommand {
             row.handled = sum(handled);
             row.failed = sum(failed);
             row.attempts = sum(values(r, "redeliveryAttempts"));
+            // with camel-chaos: the seconds a rule had the route down
+            row.down = r.get("down") != null ? sum(values(r, "down")) : -1;
             row.total = row.completed + row.recovered + row.handled + 
row.failed;
             long[] done = new long[completed.length];
             long weighted = 0;
@@ -248,11 +250,26 @@ public class ListTimeline extends ProcessWatchCommand {
     }
 
     private static void addErrors(Row base, JsonObject timeline, 
List<ErrorRow> rows) {
+        long end = timeline.getLongOrDefault("end", 0);
+        JsonArray chaos = (JsonArray) timeline.get("chaos");
+        if (chaos != null) {
+            // the faults of the chaos rules, which made the errors marked 
with the rule
+            for (Object o : chaos) {
+                JsonObject c = (JsonObject) o;
+                ErrorRow row = new ErrorRow();
+                row.copy(base);
+                row.exception = "chaos faults: " + 
c.getStringOrDefault("rule", "");
+                row.statusCode = "";
+                row.chaosRule = c.getString("id");
+                long[] counts = values(c, "faults");
+                count(row, counts, end);
+                rows.add(row);
+            }
+        }
         JsonArray list = (JsonArray) timeline.get("errors");
         if (list == null) {
             return;
         }
-        long end = timeline.getLongOrDefault("end", 0);
         for (Object o : list) {
             JsonObject e = (JsonObject) o;
             ErrorRow row = new ErrorRow();
@@ -262,20 +279,23 @@ public class ListTimeline extends ProcessWatchCommand {
             Object status = e.get("statusCode");
             row.statusCode = status != null ? status.toString() : "";
             row.chaosRule = e.getString("chaosRule");
-            long[] counts = values(e, "counts");
-            row.count = sum(counts);
-            for (int i = counts.length - 1; i >= 0; i--) {
-                if (counts[i] > 0) {
-                    long second = end - (counts.length - 1 - i);
-                    row.last = TimeUtils.printSince(second * 1000);
-                    break;
-                }
-            }
-            row.spark = spark(counts, SPARK_WIDTH);
+            count(row, values(e, "counts"), end);
             rows.add(row);
         }
     }
 
+    private static void count(ErrorRow row, long[] counts, long end) {
+        row.count = sum(counts);
+        for (int i = counts.length - 1; i >= 0; i--) {
+            if (counts[i] > 0) {
+                long second = end - (counts.length - 1 - i);
+                row.last = TimeUtils.printSince(second * 1000);
+                break;
+            }
+        }
+        row.spark = spark(counts, SPARK_WIDTH);
+    }
+
     private static long[] values(JsonObject o, String key) {
         JsonArray arr = (JsonArray) o.get(key);
         if (arr == null) {
@@ -336,6 +356,9 @@ public class ListTimeline extends ProcessWatchCommand {
                 jo.put("handled", r.handled);
                 jo.put("failed", r.failed);
                 jo.put("redeliveryAttempts", r.attempts);
+                if (r.down >= 0) {
+                    jo.put("downSeconds", r.down);
+                }
                 jo.put("meanMillis", r.mean);
                 jo.put("p95Millis", r.p95);
                 jo.put("maxMillis", r.max);
@@ -357,6 +380,9 @@ public class ListTimeline extends ProcessWatchCommand {
                         .with(r -> count(r.handled)),
                 new 
Column().header("FAILED").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
                         .with(r -> count(r.failed)),
+                new Column().header("DOWN").visible(rows.stream().anyMatch(r 
-> r.down >= 0))
+                        
.headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
+                        .with(r -> r.down > 0 ? r.down + "s" : ""),
                 new 
Column().header("ATTEMPTS").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
                         .with(r -> count(r.attempts)),
                 new 
Column().header("MEAN").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
@@ -444,8 +470,8 @@ public class ListTimeline extends ProcessWatchCommand {
                         .with(r -> r.name),
                 new 
Column().header("ROUTE").dataAlign(HorizontalAlign.LEFT).maxWidth(25, 
OverflowBehaviour.ELLIPSIS_RIGHT)
                         .with(r -> r.routeId != null ? r.routeId : ""),
-                new 
Column().header("EXCEPTION").dataAlign(HorizontalAlign.LEFT).maxWidth(50, 
OverflowBehaviour.ELLIPSIS_LEFT)
-                        .with(r -> r.exception),
+                new 
Column().header("EXCEPTION").dataAlign(HorizontalAlign.LEFT).maxWidth(50, 
OverflowBehaviour.ELLIPSIS_RIGHT)
+                        .with(r -> simpleName(r.exception)),
                 new 
Column().header("STATUS").headerAlign(HorizontalAlign.CENTER).dataAlign(HorizontalAlign.CENTER)
                         .with(r -> r.statusCode),
                 new Column().header("CHAOS").dataAlign(HorizontalAlign.LEFT)
@@ -456,6 +482,14 @@ public class ListTimeline extends ProcessWatchCommand {
                 new 
Column().header("ERRORS/S").dataAlign(HorizontalAlign.LEFT).with(r -> 
r.spark))));
     }
 
+    /** The simple name of an exception class (the JSON output has the full 
name). */
+    private static String simpleName(String exception) {
+        if (exception == null || exception.contains(" ")) {
+            return exception != null ? exception : "";
+        }
+        return exception.substring(exception.lastIndexOf('.') + 1);
+    }
+
     private static String count(long n) {
         return n > 0 ? Long.toString(n) : "";
     }
@@ -506,6 +540,7 @@ public class ListTimeline extends ProcessWatchCommand {
         long handled;
         long failed;
         long attempts;
+        long down = -1;
         long mean;
         long p95;
         long max;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListTimelineTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListTimelineTest.java
index 26d9eb4f4ae7..bbfa74584b0c 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListTimelineTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListTimelineTest.java
@@ -99,6 +99,28 @@ class ListTimelineTest extends ProcessCommandTestSupport {
         assertThat(line).contains("orders", "503", "r1", " 3 ");
     }
 
+    @Test
+    void withChaosTheSecondsARouteWasDownAndTheFaultsOfTheRules() throws 
Exception {
+        String routes = run(command(timeline()));
+        assertThat(routes).contains("DOWN");
+        assertThat(routes.lines().filter(l -> 
l.contains("orders")).findFirst().orElseThrow()).contains("2s");
+
+        ListTimeline command = command(timeline());
+        command.errors = true;
+        String errors = run(command);
+        String line = errors.lines().filter(l -> l.contains("chaos 
faults")).findFirst().orElseThrow();
+        assertThat(line).contains("endpoint=http:stock*", "r1", " 3 ");
+    }
+
+    @Test
+    void withoutChaosNoDownColumn() throws Exception {
+        JsonObject timeline = timeline();
+        for (Object r : (List<?>) timeline.get("routes")) {
+            ((JsonObject) r).remove("down");
+        }
+        assertThat(run(command(timeline))).doesNotContain("DOWN");
+    }
+
     @Test
     void aSparklineTakesTheMaxOfItsSeconds() {
         assertThat(ListTimeline.spark(new long[] { 0, 0, 4, 8 }, 
4)).isEqualTo("  ▄█");
@@ -138,6 +160,8 @@ class ListTimelineTest extends ProcessCommandTestSupport {
         orders.put("maxMillis", List.of(20L, 900L, 50L));
         orders.put("redeliveryAttempts", List.of(0L, 4L, 0L));
         orders.put("from", "platform-http:/orders");
+        // with camel-chaos: a rule had orders down for 2 seconds
+        orders.put("down", List.of(0L, 1L, 1L));
         JsonObject dlq = route("dlq", List.of(0L, 0L, 1L), List.of(0L, 0L, 
0L), List.of(0L, 0L, 0L),
                 List.of(0L, 0L, 0L));
 
@@ -158,6 +182,11 @@ class ListTimelineTest extends ProcessCommandTestSupport {
         root.put("routes", List.of(orders, dlq));
         root.put("errors", List.of(error));
         root.put("exchanges", List.of(ex1, ex2));
+        JsonObject r1 = new JsonObject();
+        r1.put("id", "r1");
+        r1.put("rule", "endpoint=http:stock* error status=503");
+        r1.put("faults", List.of(0L, 3L, 0L));
+        root.put("chaos", List.of(r1));
         // as the integration answers it: parsed JSON
         try {
             return (JsonObject) Jsoner.deserialize(root.toJson());
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
index 996b4f9c3537..200b0d5fac33 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
@@ -576,9 +576,10 @@ public class RuntimeTools {
                   What happened per second in the last minutes (up to 5) of 
the running application: per route the \
                   exchanges completed, recovered by a redelivery, handled by 
the error handler and failed, the mean, \
                   p95 and max time, the exchanges in flight and the redelivery 
attempts; the errors per second by \
-                  route and exception (chaosRule names the camel-chaos rule 
that made one); and per second the failed, \
-                  handled and recovered exchanges (a recovered one with the 
failure it was redelivered for) and the 3 \
-                  slowest with the remote calls they made. Each series is 
oldest first and ends at \
+                  route and exception (chaosRule names the camel-chaos rule 
that made one); with camel-chaos the faults \
+                  per second of each chaos rule (chaos) and the seconds a rule 
had a route down (down of the route); \
+                  and per second the failed, handled and recovered exchanges 
(a recovered one with the failure it was \
+                  redelivered for) and the 3 slowest with the remote calls 
they made. Each series is oldest first and ends at \
                   the epoch second end. Use it to tell when something went 
wrong and what it did to which route; \
                   camel_runtime_chaos_report has the chaos rules.""")
     public JsonObject camel_runtime_timeline(
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Timeline.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Timeline.java
index e97443f121d4..11812c9ad103 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Timeline.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Timeline.java
@@ -125,6 +125,8 @@ final class Timeline {
         // the last second the integration reported, and whether all of its 
seconds were read
         long end = -1;
         boolean full;
+        // whether the integration records the chaos rules itself (else the 
TUI samples their state)
+        boolean chaosRecorded;
 
         void trim(long second) {
             long oldest = second - SECONDS;
@@ -185,6 +187,40 @@ final class Timeline {
                     }
                 }
             }
+            // with camel-chaos the routes have their down seconds: the 
integration records the chaos rules
+            boolean chaosRecorded = ((List<?>) timeline.get("routes")).stream()
+                    .anyMatch(o -> o instanceof JsonObject r && r.get("down") 
instanceof List<?>);
+            if (chaosRecorded) {
+                // the faults of the chaos rules and the routes they had down, 
as the integration recorded them
+                rec.faults.subMap(start, true, end, true).clear();
+                rec.down.subMap(start, true, end, true).clear();
+                List<?> chaos = timeline.get("chaos") instanceof List<?> l ? l 
: List.of();
+                for (Object o : chaos) {
+                    if (o instanceof JsonObject c && c.getString("id") != 
null) {
+                        String id = c.getString("id");
+                        if (c.getString("rule") != null) {
+                            rec.ruleLabels.putIfAbsent(id, ruleLabel(id, 
c.getString("rule")));
+                        }
+                        long[] faults = values(c, "faults", n);
+                        for (int i = 0; i < n; i++) {
+                            if (faults[i] > 0) {
+                                rec.faults.computeIfAbsent(start + i, s -> new 
LinkedHashMap<>()).put(id, faults[i]);
+                            }
+                        }
+                    }
+                }
+                for (Object o : (List<?>) timeline.get("routes")) {
+                    if (o instanceof JsonObject r && r.get("down") instanceof 
List<?>) {
+                        long[] down = values(r, "down", n);
+                        for (int i = 0; i < n; i++) {
+                            if (down[i] > 0) {
+                                rec.down.computeIfAbsent(start + i, s -> new 
LinkedHashSet<>()).add(r.getString("routeId"));
+                            }
+                        }
+                    }
+                }
+                rec.chaosRecorded = true;
+            }
             long[] done = values(timeline, "exchangesDone", n);
             for (int i = 0; i < n; i++) {
                 rec.done.put(start + i, Math.max(0, done[i]));
@@ -238,6 +274,15 @@ final class Timeline {
         long second = now / 1000;
         Recording rec = byPid.computeIfAbsent(info.pid, k -> new Recording());
         synchronized (rec) {
+            if (rec.chaosRecorded) {
+                // the integration records the faults and the routes down: 
only the labels of the rules from here
+                for (ChaosInfo.Rule r : info.chaos.rules) {
+                    if (r.id != null) {
+                        rec.ruleLabels.put(r.id, ruleLabel(r));
+                    }
+                }
+                return;
+            }
             recordChaos(rec, info.chaos, second);
             rec.trim(Math.max(second, rec.end));
         }
@@ -283,6 +328,31 @@ final class Timeline {
         }
     }
 
+    /** A short label of a chaos rule from its text, when the status no longer 
has the rule: r1 error (stock). */
+    static String ruleLabel(String id, String rule) {
+        String fault = "";
+        String where = "";
+        for (String part : rule.split(" ")) {
+            int eq = part.indexOf('=');
+            if (eq < 0 && fault.isEmpty()) {
+                fault = part;
+            } else if (where.isEmpty() && eq > 0
+                    && List.of("endpoint", "route", "step", "component", 
"consumer").contains(part.substring(0, eq))) {
+                where = part.substring(eq + 1);
+            }
+        }
+        int colon = where.lastIndexOf(':');
+        if (colon >= 0 && colon < where.length() - 1) {
+            where = where.substring(colon + 1);
+        }
+        where = where.replace("*", "").replaceAll("^/+", "");
+        int slash = where.lastIndexOf('/');
+        if (slash >= 0 && slash < where.length() - 1) {
+            where = where.substring(slash + 1);
+        }
+        return id + (fault.isEmpty() ? "" : " " + fault) + (where.isEmpty() ? 
"" : " (" + where + ")");
+    }
+
     /** A short label of a chaos rule for the legend: r1 error 503 (stock). */
     static String ruleLabel(ChaosInfo.Rule r) {
         String fault = r.faultShort();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/timeline.md
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/timeline.md
index 1d114f502f4d..1ef06e712bfe 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/timeline.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/timeline.md
@@ -20,7 +20,9 @@ handled. A failure that a redelivery recovered is not an 
error of the
 exchange: look for it in the yellow of the routes.
 
 With camel-chaos on, the faults of each chaos rule are a layer of their
-own, marked 🎲 with the id of the rule (`🎲 r1 error 503 (stock)`). An
+own, marked 🎲 with the id of the rule (`🎲 r1 error 503 (stock)`). The
+integration records them too, with the seconds a rule had a route down,
+so they are there also for an outage before the TUI started. An
 error that a rule made is not counted again as an error, so an injected
 fault and a real one stay apart.
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTest.java
index c0f8d3cf15bc..56bf811ff37a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTest.java
@@ -116,6 +116,46 @@ class TimelineTest {
         assertThat(b.calls()).containsExactly(new 
Timeline.Call("http://stock";, 850), new Timeline.Call("kafka:orders", 5));
     }
 
+    @Test
+    void 
theIntegrationRecordsTheFaultsOfTheChaosRulesAndTheRoutesTheyHadDown() throws 
Exception {
+        JsonObject json = new TimelineJson(S0 + 2, 3).route("orders", 
"completed", 0, 1)
+                .route("orders", "down", 1, 1).route("orders", "down", 2, 
1).json();
+        JsonObject r2 = new JsonObject();
+        r2.put("id", "r2");
+        r2.put("rule", "route=orders stall duration=20s");
+        r2.put("faults", List.of(0L, 1L, 0L));
+        json.put("chaos", List.of(r2));
+        timeline.merge("1234", json, true);
+
+        assertThat(orders(S0 + 2, 3).down()).containsExactly(0, 1, 1);
+        List<Timeline.CauseSeries> causes = timeline.causes("1234", S0 + 2, 3);
+        assertThat(causes).singleElement().satisfies(c -> {
+            assertThat(c.rule()).isEqualTo("r2");
+            assertThat(c.label()).isEqualTo("r2 stall (orders)");
+            assertThat(c.values()).containsExactly(0, 1, 0);
+        });
+
+        // the status then only gives the labels: the TUI no longer samples 
the state of the rules
+        IntegrationInfo info = info();
+        info.chaos = ChaosInfo.parse((JsonObject) Jsoner.deserialize("""
+                {"enabled": true,
+                 "rules": [{"id": "r2", "version": 1, "state": "active", 
"rule": "route=orders stall duration=20s",
+                            "matches": 1, "fired": {"lifecycle": 1}, 
"faultsPerSecond": [9, 9, 9],
+                            "lifecycle": {"action": "suspend", "route": 
"orders", "state": "down"}}],
+                 "report": {"verdict": "PASS", "routes": [], "recentFaults": 
[]}}
+                """));
+        timeline.record(info, (S0 + 2) * 1000);
+        assertThat(timeline.causes("1234", S0 + 2, 
3).get(0).values()).containsExactly(0, 1, 0);
+        assertThat(timeline.causes("1234", S0 + 2, 
3).get(0).label()).isEqualTo("r2 stall 20s (route orders)");
+    }
+
+    @Test
+    void theLabelOfARuleFromItsText() {
+        assertThat(Timeline.ruleLabel("r1", 
"endpoint=http://localhost:8765/stock* error status=503 rate=1"))
+                .isEqualTo("r1 error (stock)");
+        assertThat(Timeline.ruleLabel("r2", "route=shipping stall 
duration=15s")).isEqualTo("r2 stall (shipping)");
+    }
+
     @Test
     void notATimelineIsIgnored() {
         timeline.merge("1234", new JsonObject(), true);

Reply via email to