This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch CAMEL-26713-timeline-chaos
in repository https://gitbox.apache.org/repos/asf/camel.git

commit b6864435f250a4fb3ace6200dd74533e7133513a
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Nov 7 10:43:57 2026 +0100

    CAMEL-26713: camel-console - the timeline records the faults of the chaos 
rules and the routes they had down
    
    With camel-chaos, the timeline dev console asks the chaos dev console each 
second for its rules (with the new option
    report=false, so without the report) and records per second the faults of 
each rule and the routes a stall or
    lifecycle rule had down. It returns them as chaos and as the down series of 
each route. camel-console does not depend
    on camel-chaos.
    
    The Timeline screen takes the chaos layers and the shaded seconds from 
there, so they are there also for an outage
    before the TUI started; camel get timeline shows a DOWN column and the 
faults of the rules with --errors, and the MCP
    tool has them.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[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     | 90 ++++++++++++++++++++--
 .../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  |  5 +-
 .../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, 567 insertions(+), 30 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 d560168f463c..f3c56af21045 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 57bbb3fdc305..550132204118 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 57bbb3fdc305..550132204118 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 e7bc0b615408..635caeeccb39 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,10 +37,11 @@ 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;
@@ -50,8 +53,8 @@ import org.apache.camel.util.json.JsonRecordSupport;
  * 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 {
 
@@ -74,7 +77,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 +115,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 and handled 
exchanges (up to 10) and the 3 slowest, oldest first (only present when asked 
for)") List<TimelineExchange> exchanges) {
     }
 
@@ -176,7 +187,51 @@ 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) {
+            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,8 +287,10 @@ 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<>();
         synchronized (rec) {
             secs = rec.seconds(end, count);
+            boolean chaosSeen = !rec.chaosRules().isEmpty();
             for (TimelineRecorder.Second s : secs) {
                 done.add(s != null ? s.done : 0L);
             }
@@ -247,8 +304,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);
@@ -277,7 +344,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 cfc18163d437..65a478908177 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
@@ -121,6 +121,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;
 
@@ -184,6 +187,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) {
@@ -352,6 +357,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 cc96da7974f4..e3baded8c6c6 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;
@@ -166,6 +171,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 c8f62995a3da..5f17427b1655 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;
@@ -216,6 +217,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 2bda457023df..cb5d376ab808 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 and 
handled 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 e49c3a06911c..fb49a622a7f8 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 and 
handled 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 and handled 
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 50e09b22097d..171ab2cf1f95 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 and 
handled 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 and"
+                                      + " handled 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 83c36e6439d8..8c9243ef174b 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 487e035d28f7..26f9b99b9ed2 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,8 +576,9 @@ 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 \
-                  and handled exchanges 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 and handled 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; \
                   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