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);