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