This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26676-chaos-feedback in repository https://gitbox.apache.org/repos/asf/camel.git
commit d12c03d58db47036fea8de5f6d11050c16879043 Author: Claus Ibsen <[email protected]> AuthorDate: Fri Nov 6 09:53:14 2026 +0100 CAMEL-26676: camel-chaos - feedback while adding rules - A rule that matches nothing in the context now (no endpoint, route or step id, at=poll without a scheduled poll consumer, an expectation of a route that does not exist) is added with a warning that names some of what is there: logged, returned by ChaosManager.checkRule, printed by chaos-add and chaos-change, and passed through the MCP and camel ask tools. - ChaosManager.addRules adds rules separated by ; together, all or none; chaos-add takes --rule more than once, and the add tool several rules. - A skipped rule counts why (skippedReasons), in the report and the CLI. - camel get chaos has a NOTE column (what a lifecycle rule does to its route, why a rule was skipped), and says on stderr when there is nothing to show. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../org/apache/camel/catalog/docs/chaos.adoc | 29 ++++- components/camel-chaos/src/main/docs/chaos.adoc | 29 ++++- .../apache/camel/component/chaos/ChaosFaults.java | 43 ++++--- .../camel/component/chaos/ChaosLifecycle.java | 2 +- .../apache/camel/component/chaos/ChaosManager.java | 125 +++++++++++++++--- .../camel/component/chaos/ChaosPollStrategy.java | 2 +- .../apache/camel/component/chaos/ChaosReport.java | 13 ++ .../apache/camel/component/chaos/ChaosRule.java | 22 +++- .../apache/camel/component/chaos/ChaosTargets.java | 137 ++++++++++++++++++++ .../component/chaos/ChaosRuleFeedbackTest.java | 140 +++++++++++++++++++++ .../modules/ROOT/pages/camel-jbang-mcp.adoc | 6 +- .../jbang-commands/camel-jbang-cmd-chaos-add.adoc | 4 +- .../ROOT/pages/jbang-commands/camel-jbang-cmd.adoc | 2 +- .../apache/camel/cli/connector/ChaosAction.java | 27 +++- .../LocalCliConnectorChaosActionTest.java | 21 ++++ .../META-INF/camel-jbang-commands-metadata.json | 2 +- .../core/commands/action/CamelChaosAction.java | 12 ++ .../core/commands/action/CamelChaosAddAction.java | 27 ++-- .../commands/action/CamelChaosChangeAction.java | 3 +- .../dsl/jbang/core/commands/ai/ToolRegistry.java | 14 ++- .../dsl/jbang/core/commands/process/ListChaos.java | 62 +++++++-- .../jbang/core/commands/process/ListChaosTest.java | 52 +++++++- .../dsl/jbang/core/commands/mcp/RuntimeTools.java | 12 +- 23 files changed, 706 insertions(+), 80 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc index 3c2360541ef8..d1974b66c4cb 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc @@ -94,10 +94,15 @@ Rules can also be added, changed and removed from Java while the application run ChaosManager chaos = ChaosManager.get(camelContext); String id = chaos.addRule("endpoint=file:inbox* drop at=poll rate=0.5 until=2m"); chaos.changeRule(id, "endpoint=file:inbox* drop at=poll rate=0.2 until=2m"); +List<String> warnings = chaos.checkRule(id); chaos.removeRule(id); +List<String> ids = chaos.addRules("endpoint=seda:dlq error at=dlq; endpoint=https:inventory* error status=503"); String text = chaos.report().toText(); ---- +`addRules` adds rules separated by `;`, as in `camel.chaos.rules`, all of them or none when one is not valid, so they +are in place at the same time. `checkRule` says what a rule matches nothing of in the context now, see below. + == Control while it runs With the Camel CLI, against a running integration (`myapp` is its name or pid): @@ -105,6 +110,7 @@ With the Camel CLI, against a running integration (`myapp` is its name or pid): [source,bash] ---- camel cmd chaos-add myapp --rule='endpoint=kafka:orders* delay duration=2s rate=0.2 until=5m' +camel cmd chaos-add myapp --rule='endpoint=seda:dlq error at=dlq' --rule='endpoint=https:inventory* error status=503' camel cmd chaos-change myapp --id=r1 --rule='endpoint=kafka:orders* delay duration=5s rate=0.5 until=5m' camel cmd chaos-remove myapp --id=r1 camel cmd chaos-clear @@ -114,12 +120,23 @@ camel get chaos myapp --report ---- `chaos-add` prints the id of the new rule (`r1`, `r2`, ...), and `chaos-change` the version of the rule that replaces -it. The ids are per integration, so `chaos-add`, `chaos-change` and `chaos-remove` need the name or pid of one +it. `--rule` can be given more than once (or a rule can hold several separated by `;`): the rules are added together, +all or none, which matters when one must be in place before the other fires, as each `chaos-add` takes a moment. The ids are per integration, so `chaos-add`, `chaos-change` and `chaos-remove` need the name or pid of one integration; `chaos-clear` without one acts on every running integration that has chaos switched on. The rules that `chaos-clear` removes stay in the report; with `--reset` the report starts again, for a new experiment: no rules, route counts or verdict, as for an application that just started (with the same seed, and the ids of new rules going on from the last one). An id that does not exist is "not found" (exit code 3). `camel get chaos` lists the current rules with their id, version, state, -matches and faults, and `--report` shows the report (also as JSON with `--json`). +matches and faults, and `--report` shows the report (also as JSON with `--json`). Their `NOTE` column says what a +lifecycle rule does to its route (`route poller down, back in 12s`) and why a rule did nothing when it was skipped. +Without a rule to show, `camel get chaos` says so on stderr (no rules, chaos off, or no integration with camel-chaos). + +A rule that matches nothing in the integration now is added with a warning, which `chaos-add` and `chaos-change` print, +and which is logged: an `endpoint=` or `component=` pattern that matches no endpoint of the context, an `at=poll` for +endpoints that no route consumes with a scheduled poll, a `route=` or `step=` pattern that matches no route or step +id, and an expectation of a route that does not exist. Such a rule fires 0 times, and is often a typo; the warning +names some of the endpoints, routes or steps there are. It is not an error, as the routes may still change (a `toD` +creates its endpoints when it sends, and a route can be added later). The rules of `camel.chaos.rules` are checked when +the context has started. The commands go through the `chaos` action of the CLI connector, which calls `ChaosManager`: a rule goes through the same parser as `camel.chaos.rules`, and one that is not valid is refused with the reason. They work only when chaos is @@ -137,7 +154,8 @@ as tools, for an AI agent that tries a route against failures: `camel_runtime_ch They take the same one-line rules: adding, changing, removing and clearing go through the same `chaos` action of the CLI connector, and the list and the report read the console from the status of the application. So chaos must be switched on, and a rule that is not valid is refused with the reason; `camel_runtime_chaos_clear` takes `reset=true` as -`chaos-clear --reset` does. Adding and changing a rule are marked destructive, so a +`chaos-clear --reset` does. `camel_runtime_chaos_add` takes several rules separated by `;`, and the add and change +tools return the warnings of a rule that matches nothing, so an agent can fix a typo with a change. Adding and changing a rule are marked destructive, so a client that honours `destructiveHint` asks before it runs them, and the `read-write` access level of the MCP security layer does not offer them; the list and the report are read-only. `camel ask` has the same tools, and asks before it adds or changes a rule. The development MCP tools that `camel run --mcp` (`camel.jbang.mcp=true`) serves over HTTP have @@ -268,7 +286,7 @@ which is read into memory; not a stream or a reader, which reading would consume the bytes of that text in UTF-16, while the exchange keeps its charset, so read as text again it has characters outside US-ASCII. `truncate`: an `InputStream` or stream cache body ends after the first half of its bytes (the stream is read into memory; a stream cache stays a stream cache). On a message the mode cannot change, the message is left as it is, -and the report counts the rule as skipped. +and the report counts the rule as skipped, with the reason. |`lifecycle` |`action=suspend\|stop\|remove`, `duration=<time>` @@ -396,7 +414,8 @@ removed while the route was down, and the `reason` when it did not go as planned `ChaosManager.report()` gives the report, as plain text (`toText()`) for a person and as JSON (`toJson()`) for a tool, and it is logged when the context stops (`camel.chaos.reportOnShutdown`), if any rule was added. It states the seed, -each rule with its id and version, its matches, the faults that fired at each hook and the ones that were skipped, its +each rule with its id and version, its matches, the faults that fired at each hook and the ones that were skipped (with +the reasons, such as `a stream body`, counted in `skippedReasons`), its state (`active`, `ended`, `removed` or `replaced`), and per route. The rules are in the order of their ids, and the versions of an id from the latest. Per route: diff --git a/components/camel-chaos/src/main/docs/chaos.adoc b/components/camel-chaos/src/main/docs/chaos.adoc index 3c2360541ef8..d1974b66c4cb 100644 --- a/components/camel-chaos/src/main/docs/chaos.adoc +++ b/components/camel-chaos/src/main/docs/chaos.adoc @@ -94,10 +94,15 @@ Rules can also be added, changed and removed from Java while the application run ChaosManager chaos = ChaosManager.get(camelContext); String id = chaos.addRule("endpoint=file:inbox* drop at=poll rate=0.5 until=2m"); chaos.changeRule(id, "endpoint=file:inbox* drop at=poll rate=0.2 until=2m"); +List<String> warnings = chaos.checkRule(id); chaos.removeRule(id); +List<String> ids = chaos.addRules("endpoint=seda:dlq error at=dlq; endpoint=https:inventory* error status=503"); String text = chaos.report().toText(); ---- +`addRules` adds rules separated by `;`, as in `camel.chaos.rules`, all of them or none when one is not valid, so they +are in place at the same time. `checkRule` says what a rule matches nothing of in the context now, see below. + == Control while it runs With the Camel CLI, against a running integration (`myapp` is its name or pid): @@ -105,6 +110,7 @@ With the Camel CLI, against a running integration (`myapp` is its name or pid): [source,bash] ---- camel cmd chaos-add myapp --rule='endpoint=kafka:orders* delay duration=2s rate=0.2 until=5m' +camel cmd chaos-add myapp --rule='endpoint=seda:dlq error at=dlq' --rule='endpoint=https:inventory* error status=503' camel cmd chaos-change myapp --id=r1 --rule='endpoint=kafka:orders* delay duration=5s rate=0.5 until=5m' camel cmd chaos-remove myapp --id=r1 camel cmd chaos-clear @@ -114,12 +120,23 @@ camel get chaos myapp --report ---- `chaos-add` prints the id of the new rule (`r1`, `r2`, ...), and `chaos-change` the version of the rule that replaces -it. The ids are per integration, so `chaos-add`, `chaos-change` and `chaos-remove` need the name or pid of one +it. `--rule` can be given more than once (or a rule can hold several separated by `;`): the rules are added together, +all or none, which matters when one must be in place before the other fires, as each `chaos-add` takes a moment. The ids are per integration, so `chaos-add`, `chaos-change` and `chaos-remove` need the name or pid of one integration; `chaos-clear` without one acts on every running integration that has chaos switched on. The rules that `chaos-clear` removes stay in the report; with `--reset` the report starts again, for a new experiment: no rules, route counts or verdict, as for an application that just started (with the same seed, and the ids of new rules going on from the last one). An id that does not exist is "not found" (exit code 3). `camel get chaos` lists the current rules with their id, version, state, -matches and faults, and `--report` shows the report (also as JSON with `--json`). +matches and faults, and `--report` shows the report (also as JSON with `--json`). Their `NOTE` column says what a +lifecycle rule does to its route (`route poller down, back in 12s`) and why a rule did nothing when it was skipped. +Without a rule to show, `camel get chaos` says so on stderr (no rules, chaos off, or no integration with camel-chaos). + +A rule that matches nothing in the integration now is added with a warning, which `chaos-add` and `chaos-change` print, +and which is logged: an `endpoint=` or `component=` pattern that matches no endpoint of the context, an `at=poll` for +endpoints that no route consumes with a scheduled poll, a `route=` or `step=` pattern that matches no route or step +id, and an expectation of a route that does not exist. Such a rule fires 0 times, and is often a typo; the warning +names some of the endpoints, routes or steps there are. It is not an error, as the routes may still change (a `toD` +creates its endpoints when it sends, and a route can be added later). The rules of `camel.chaos.rules` are checked when +the context has started. The commands go through the `chaos` action of the CLI connector, which calls `ChaosManager`: a rule goes through the same parser as `camel.chaos.rules`, and one that is not valid is refused with the reason. They work only when chaos is @@ -137,7 +154,8 @@ as tools, for an AI agent that tries a route against failures: `camel_runtime_ch They take the same one-line rules: adding, changing, removing and clearing go through the same `chaos` action of the CLI connector, and the list and the report read the console from the status of the application. So chaos must be switched on, and a rule that is not valid is refused with the reason; `camel_runtime_chaos_clear` takes `reset=true` as -`chaos-clear --reset` does. Adding and changing a rule are marked destructive, so a +`chaos-clear --reset` does. `camel_runtime_chaos_add` takes several rules separated by `;`, and the add and change +tools return the warnings of a rule that matches nothing, so an agent can fix a typo with a change. Adding and changing a rule are marked destructive, so a client that honours `destructiveHint` asks before it runs them, and the `read-write` access level of the MCP security layer does not offer them; the list and the report are read-only. `camel ask` has the same tools, and asks before it adds or changes a rule. The development MCP tools that `camel run --mcp` (`camel.jbang.mcp=true`) serves over HTTP have @@ -268,7 +286,7 @@ which is read into memory; not a stream or a reader, which reading would consume the bytes of that text in UTF-16, while the exchange keeps its charset, so read as text again it has characters outside US-ASCII. `truncate`: an `InputStream` or stream cache body ends after the first half of its bytes (the stream is read into memory; a stream cache stays a stream cache). On a message the mode cannot change, the message is left as it is, -and the report counts the rule as skipped. +and the report counts the rule as skipped, with the reason. |`lifecycle` |`action=suspend\|stop\|remove`, `duration=<time>` @@ -396,7 +414,8 @@ removed while the route was down, and the `reason` when it did not go as planned `ChaosManager.report()` gives the report, as plain text (`toText()`) for a person and as JSON (`toJson()`) for a tool, and it is logged when the context stops (`camel.chaos.reportOnShutdown`), if any rule was added. It states the seed, -each rule with its id and version, its matches, the faults that fired at each hook and the ones that were skipped, its +each rule with its id and version, its matches, the faults that fired at each hook and the ones that were skipped (with +the reasons, such as `a stream body`, counted in `skippedReasons`), its state (`active`, `ended`, `removed` or `replaced`), and per route. The rules are in the order of their ids, and the versions of an id from the latest. Per route: diff --git a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosFaults.java b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosFaults.java index 27fe753d172a..9a78d96b0a8b 100644 --- a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosFaults.java +++ b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosFaults.java @@ -117,7 +117,7 @@ final class ChaosFaults { String scheme = ChaosPatterns.scheme(ChaosPatterns.endpointUri(endpoint)); if (!ChaosRuleParser.HTTP_SCHEMES.contains(scheme)) { // the rule matched an endpoint that is not an HTTP one (with a pattern such as endpoint=*inventory*) - rule.skipped(); + rule.skipped("not an HTTP endpoint (status= is for the HTTP components)"); return SendHook.PROCEED; } Message message = exchange.getMessage(); @@ -348,21 +348,22 @@ final class ChaosFaults { * skipped when the message has nothing the mode can change. */ static void corrupt(ChaosRule rule, Exchange exchange, Hook hook) { - boolean changed; + // why the message was left as it is, or null when it was changed + String skip; try { - changed = switch (rule.mode) { + skip = switch (rule.mode) { case HEADERS -> corruptHeaders(rule, exchange.getMessage()); case CHARSET -> corruptCharset(exchange); case TRUNCATE -> truncate(exchange.getMessage()); }; } catch (RuntimeException e) { // a body that cannot be read as the mode needs (such as a failing type conversion) is left as it is - changed = false; + skip = "the body could not be read (" + e.getClass().getSimpleName() + ")"; } - if (changed) { + if (skip == null) { rule.fired(hook); } else { - rule.skipped(); + rule.skipped(skip); } } @@ -370,7 +371,7 @@ final class ChaosFaults { * Gives every header that is not one of Camel's own ({@code Camel*}) a random value of letters and digits, from the * random sequence of the rule. */ - private static boolean corruptHeaders(ChaosRule rule, Message message) { + private static String corruptHeaders(ChaosRule rule, Message message) { List<String> names = new ArrayList<>(); for (String name : message.getHeaders().keySet()) { if (!name.regionMatches(true, 0, "Camel", 0, 5)) { @@ -384,7 +385,7 @@ final class ChaosFaults { } message.setHeader(name, new String(value)); } - return !names.isEmpty(); + return names.isEmpty() ? "no headers to change (Camel* headers are left as they are)" : null; } /** @@ -392,15 +393,18 @@ final class ChaosFaults { * stream, which reading would consume) with the bytes of that text in UTF-16, while the exchange keeps its charset: * read as text again, the body has characters outside US-ASCII. */ - private static boolean corruptCharset(Exchange exchange) { + private static String corruptCharset(Exchange exchange) { Message message = exchange.getMessage(); Object body = message.getBody(); - if (body == null || body instanceof InputStream || body instanceof Reader || body instanceof StreamCache) { - return false; + if (body == null) { + return "no body"; + } + if (body instanceof InputStream || body instanceof Reader || body instanceof StreamCache) { + return "a stream body (charset changes a text body; mode=truncate changes a stream)"; } if (body instanceof WrappedFile<?> wrapped && !(wrapped.getFile() instanceof File)) { // a remote file (such as FTP), whose content may be a stream - return false; + return "a remote file body (charset changes a text body)"; } String text; if (body instanceof CharSequence cs) { @@ -410,22 +414,25 @@ final class ChaosFaults { } else { text = exchange.getContext().getTypeConverter().tryConvertTo(String.class, exchange, body); } - if (text == null || text.isEmpty()) { - return false; + if (text == null) { + return "a body that is not text (charset changes a text body)"; + } + if (text.isEmpty()) { + return "an empty body"; } message.setBody(text.getBytes(StandardCharsets.UTF_16)); - return true; + return null; } /** * Replaces an {@link InputStream} or stream cache body with one that ends after the first half of its bytes. The * stream is read into memory and closed; a stream cache stays a stream cache. */ - private static boolean truncate(Message message) { + private static String truncate(Message message) { Object body = message.getBody(); boolean cache = body instanceof StreamCache; if (!(body instanceof InputStream) && !cache) { - return false; + return body == null ? "no body" : "not a stream body (truncate changes a stream or a stream cache)"; } ByteArrayOutputStream bytes = new ByteArrayOutputStream(); try { @@ -446,6 +453,6 @@ final class ChaosFaults { byte[] half = new byte[all.length / 2]; System.arraycopy(all, 0, half, 0, half.length); message.setBody(cache ? new InputStreamCache(half) : new ByteArrayInputStream(half)); - return all.length > 0; + return all.length > 0 ? null : "an empty body"; } } diff --git a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosLifecycle.java b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosLifecycle.java index e64d3e90ade8..c60b0a200735 100644 --- a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosLifecycle.java +++ b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosLifecycle.java @@ -301,7 +301,7 @@ final class ChaosLifecycle { } synchronized (fault) { if (skip != null) { - rule.skipped(); + rule.skipped(skip); LOG.info("Chaos rule {} did nothing: {}", rule.id, skip); fault.finish(FaultState.SKIPPED, null, skip); return; diff --git a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java index 9af42102d310..8882016fb92b 100644 --- a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java +++ b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java @@ -131,27 +131,67 @@ public final class ChaosManager { public String addRule(String rule) { ChaosRule.Builder definition = ChaosRuleParser.parse(rule, camelContext.getClassResolver(), maxDuration); synchronized (lock) { + return add(List.of(definition)).get(0); + } + } + + /** + * Adds rules together: all of them, or none when one is not valid. The rules are in the form of + * {@code camel.chaos.rules}, separated by {@code ;}, so they are in place at the same time, such as a rule for the + * dead letter channel and the rule that sends a message there. + * + * @param rules the rules in their one-line text form, separated by {@code ;} + * @return the ids of the rules, in their order + * @throws IllegalArgumentException when a rule is not valid, with the reason; no rule is added then + * @throws IllegalStateException when the rules would be more than {@link #MAX_RULES} + */ + public List<String> addRules(String rules) { + List<String> texts = ChaosRuleParser.splitRules(rules); + if (texts.isEmpty()) { + throw new IllegalArgumentException("The chaos rule is empty"); + } + List<ChaosRule.Builder> definitions = new ArrayList<>(); + for (String text : texts) { + definitions.add(ChaosRuleParser.parse(text, camelContext.getClassResolver(), maxDuration)); + } + synchronized (lock) { + return add(definitions); + } + } + + /** Adds parsed rules, all or none. Called with the lock held. */ + private List<String> add(List<ChaosRule.Builder> definitions) { + for (ChaosRule.Builder definition : definitions) { checkRoute(definition); - long now = TimeSource.currentTimeMillis(); - if (rules.size() >= MAX_RULES) { - retireEnded(now); - } - if (rules.size() >= MAX_RULES) { - throw new IllegalStateException( - "There are " + MAX_RULES + " chaos rules already; remove some before adding more"); - } - String id = "r" + nextId++; - ChaosRule added = new ChaosRule(definition, id, seed, now, maxDuration); + } + long now = TimeSource.currentTimeMillis(); + if (rules.size() + definitions.size() > MAX_RULES) { + retireEnded(now); + } + if (rules.size() + definitions.size() > MAX_RULES) { + throw new IllegalStateException( + "There are " + rules.size() + " chaos rules already, and at most " + MAX_RULES + + "; remove some before adding more"); + } + List<ChaosRule> added = new ArrayList<>(); + for (ChaosRule.Builder definition : definitions) { + ChaosRule rule = new ChaosRule(definition, "r" + nextId++, seed, now, maxDuration); // a rule added before the context started starts its clock when it has started - added.clockStarted = camelContext.isStarted(); - rules.add(added); - update(); - LOG.info("Chaos rule {} added: {}", id, added.text); + rule.clockStarted = camelContext.isStarted(); + rules.add(rule); + added.add(rule); + } + update(); + List<String> ids = new ArrayList<>(); + for (ChaosRule rule : added) { + LOG.info("Chaos rule {} added: {}", rule.id, rule.text); if (camelContext.isStarted()) { - warnIfNoRoute(added); + warnIfNoRoute(rule); + warnIfNoTarget(rule); } - return id; + ids.add(rule.id); } + return ids; } /** @@ -183,11 +223,46 @@ public final class ChaosManager { LOG.info("Chaos rule {} changed (version {}): {}", id, changed.version, changed.text); if (camelContext.isStarted()) { warnIfNoRoute(changed); + warnIfNoTarget(changed); } return changed.version; } } + /** + * What a rule matches nothing of in the context now: its pattern matches no endpoint, route or step id, its + * {@code at=poll} finds no consumer that polls on a schedule, or its expectation names a route that does not exist. + * Such a rule fires 0 times, often because of a typo. The routes may still change (a {@code toD} creates its + * endpoints when it sends, a route can be added later), so it is a warning, not an error. Empty before the context + * has started, as its routes are not there yet. + * + * @param id the id of the rule + * @return the warnings, empty when the rule finds something to fire at + * @throws IllegalArgumentException when there is no rule with that id + */ + public List<String> checkRule(String id) { + ChaosRule rule; + synchronized (lock) { + int index = indexOf(id); + if (index < 0) { + throw new IllegalArgumentException("There is no chaos rule " + id); + } + rule = rules.get(index); + } + List<String> answer = new ArrayList<>(); + if (!camelContext.isStarted()) { + return answer; + } + if (rule.expectation != null && camelContext.getRoute(rule.expectation.route) == null) { + answer.add("expects route " + rule.expectation.route + ", but there is no route with that id (yet)"); + } + String target = ChaosTargets.check(camelContext, rule); + if (target != null) { + answer.add(target); + } + return answer; + } + /** * Whether there is a rule with that id that can be changed or removed: one that was added and is not removed, nor * moved to the report history. @@ -463,9 +538,27 @@ public final class ChaosManager { warned.add(rule.id); } } + List<ChaosRule> current; + synchronized (lock) { + current = new ArrayList<>(rules); + } + for (ChaosRule rule : current) { + warnIfNoTarget(rule); + } return warned; } + // the WARNs for a rule that matches nothing, for the tests + final AtomicInteger noTargetWarnings = new AtomicInteger(); + + private void warnIfNoTarget(ChaosRule rule) { + String warning = ChaosTargets.check(camelContext, rule); + if (warning != null) { + noTargetWarnings.incrementAndGet(); + LOG.warn("Chaos rule {}: {}", rule.id, warning); + } + } + // the WARNs for an expectation of a route that does not exist, for the tests final AtomicInteger unknownRouteWarnings = new AtomicInteger(); diff --git a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosPollStrategy.java b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosPollStrategy.java index 343c208752d1..44fbbe4891d5 100644 --- a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosPollStrategy.java +++ b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosPollStrategy.java @@ -106,7 +106,7 @@ final class ChaosPollStrategy implements PollingConsumerPollStrategy { case ERROR -> { if (!beginDelegate(consumer, endpoint)) { // the strategy of the consumer skips this poll anyway - rule.skipped(); + rule.skipped("the poll strategy of the consumer skipped the poll"); return false; } rule.fired(Hook.POLL); diff --git a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java index 260065d199b0..1cef8c19cbef 100644 --- a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java +++ b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java @@ -193,6 +193,11 @@ public final class ChaosReport { } } sb.append(at).append(", skipped ").append(r.skipped); + if (!r.skipReasons.isEmpty()) { + StringJoiner why = new StringJoiner("; ", " (", ")"); + r.skipReasons.forEach((reason, count) -> why.add(reason + ": " + count)); + sb.append(why); + } if (r.lostAtDlq > 0) { sb.append(", lost at the dead letter channel ").append(r.lostAtDlq); } @@ -287,6 +292,12 @@ public final class ChaosReport { } jo.put("fired", fired); jo.put("skipped", r.skipped); + if (!r.skipReasons.isEmpty()) { + // why the rule did nothing, per reason + JsonObject why = new JsonObject(); + r.skipReasons.forEach(why::put); + jo.put("skippedReasons", why); + } if (r.hooks.contains(Hook.DLQ)) { jo.put("lostAtDlq", r.lostAtDlq); } @@ -393,6 +404,7 @@ public final class ChaosReport { final long[] fired = new long[Hook.values().length]; final long firedTotal; final long skipped; + final Map<String, Long> skipReasons; final long lostAtDlq; final long endsIn; final String expectRoute; @@ -428,6 +440,7 @@ public final class ChaosReport { // a lifecycle rule fired when it took its route down firedTotal = total + (lifecycle != null && lifecycle.acted() ? 1 : 0); skipped = rule.skippedCount(); + skipReasons = rule.skipReasons(); lostAtDlq = rule.lostAtDlqCount(); endsIn = Math.max(0, rule.endsAt - now); within = rule.within; diff --git a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java index 0ddadf31ecec..45115b5c2bc7 100644 --- a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java +++ b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java @@ -19,6 +19,9 @@ package org.apache.camel.component.chaos; import java.lang.reflect.Constructor; import java.util.Map; import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.LongAdder; @@ -168,6 +171,8 @@ final class ChaosRule { private final AtomicLong matches = new AtomicLong(); private final LongAdder[] fired = new LongAdder[Hook.values().length]; private final LongAdder skipped = new LongAdder(); + // why the rule did nothing when it fired: a reason per kind of message it could not change (a few fixed texts) + private final ConcurrentMap<String, LongAdder> skipReasons = new ConcurrentHashMap<>(); // the messages lost because the rule failed or dropped the send of the dead letter channel, which then handled them private final LongAdder lostAtDlq = new LongAdder(); private final AtomicLong random; @@ -280,9 +285,22 @@ final class ChaosRule { return lostAtDlq.sum(); } - /** Counts that the rule fired at a message it could not change, so it did nothing. */ - void skipped() { + /** + * Counts that the rule fired at a message it could not change, so it did nothing. + * + * @param reason why, such as {@code a stream body (charset changes a text body)}: one of a few fixed texts, so the + * report can count them + */ + void skipped(String reason) { skipped.increment(); + skipReasons.computeIfAbsent(reason, k -> new LongAdder()).increment(); + } + + /** How often the rule did nothing, per reason, in the order of the reasons. */ + Map<String, Long> skipReasons() { + Map<String, Long> answer = new TreeMap<>(); + skipReasons.forEach((reason, count) -> answer.put(reason, count.sum())); + return answer; } long matches() { diff --git a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosTargets.java b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosTargets.java new file mode 100644 index 000000000000..8dead19bb74a --- /dev/null +++ b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosTargets.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.chaos; + +import java.util.Collection; +import java.util.Set; +import java.util.StringJoiner; +import java.util.TreeSet; + +import org.apache.camel.CamelContext; +import org.apache.camel.Endpoint; +import org.apache.camel.Processor; +import org.apache.camel.Route; +import org.apache.camel.component.chaos.ChaosRule.Hook; +import org.apache.camel.spi.IdAware; +import org.apache.camel.support.ScheduledPollConsumer; +import org.apache.camel.util.URISupport; + +/** + * Checks what the target of a rule finds in the context now, for the warning about a rule that matches nothing (often a + * typo). The warning names some of what is there, so a person or a tool can fix the pattern. + */ +final class ChaosTargets { + + // the most endpoints, routes or steps a warning names + private static final int NAMED = 5; + + private ChaosTargets() { + } + + /** + * Why the rule finds nothing to fire at in the context now, or null when it finds something. A lifecycle rule is + * not checked: its route was checked when it was added. + */ + static String check(CamelContext camelContext, ChaosRule rule) { + if (rule.lifecycle != null) { + return null; + } + return switch (rule.target) { + case ENDPOINT, COMPONENT -> checkEndpoints(camelContext, rule); + case ROUTE -> checkRoutes(camelContext, rule); + case STEP -> checkSteps(camelContext, rule); + }; + } + + private static String checkEndpoints(CamelContext camelContext, ChaosRule rule) { + Collection<Endpoint> endpoints = camelContext.getEndpoints(); + boolean any = false; + boolean polled = false; + TreeSet<String> uris = new TreeSet<>(); + for (Endpoint endpoint : endpoints) { + String uri = ChaosPatterns.endpointUri(endpoint); + uris.add(withoutQuery(uri)); + if (rule.matchesUri(uri)) { + any = true; + } + } + if (!any) { + return rule.target.key + "=" + rule.pattern + " matches no endpoint of the context now, so the rule fires 0" + + " times (a toD creates its endpoints when it sends); the endpoints are: " + some(uris); + } + if (rule.hooks.equals(Set.of(Hook.POLL))) { + for (Route route : camelContext.getRoutes()) { + if (route.getConsumer() instanceof ScheduledPollConsumer + && rule.matchesUri(ChaosPatterns.endpointUri(route.getEndpoint()))) { + polled = true; + } + } + if (!polled) { + return "at=poll: no route consumes an endpoint that matches " + rule.target.key + "=" + rule.pattern + + " with a scheduled poll (consumers such as Kafka and JMS do not poll on a schedule), so the rule" + + " fires 0 times"; + } + } + return null; + } + + private static String checkRoutes(CamelContext camelContext, ChaosRule rule) { + TreeSet<String> ids = new TreeSet<>(); + for (Route route : camelContext.getRoutes()) { + if (rule.matchesId(route.getRouteId())) { + return null; + } + ids.add(route.getRouteId()); + } + return "route=" + rule.pattern + " matches no route of the context now, so the rule fires 0 times; the routes are: " + + some(ids); + } + + private static String checkSteps(CamelContext camelContext, ChaosRule rule) { + TreeSet<String> ids = new TreeSet<>(); + for (Route route : camelContext.getRoutes()) { + for (Processor processor : route.filter("*")) { + String id = processor instanceof IdAware idAware ? idAware.getId() : null; + if (id != null) { + if (rule.matchesId(id)) { + return null; + } + ids.add(id); + } + } + } + return "step=" + rule.pattern + " matches no step of the context now, so the rule fires 0 times (a step id is" + + " such as to1, or the id set with id(...)); the steps are: " + some(ids); + } + + // the endpoints are named as Camel shows them elsewhere (a pattern matches them with or without the // after the + // scheme), and without their query, which can hold secrets + private static String withoutQuery(String uri) { + int query = uri.indexOf('?'); + return URISupport.sanitizeUri(query > 0 ? uri.substring(0, query) : uri); + } + + private static String some(TreeSet<String> names) { + if (names.isEmpty()) { + return "none"; + } + StringJoiner sj = new StringJoiner(", "); + names.stream().limit(NAMED).forEach(sj::add); + int more = names.size() - NAMED; + return more > 0 ? sj + " and " + more + " more" : sj.toString(); + } +} diff --git a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleFeedbackTest.java b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleFeedbackTest.java new file mode 100644 index 000000000000..e1223f956f1c --- /dev/null +++ b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleFeedbackTest.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.chaos; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.util.json.JsonObject; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * What a developer (or a tool) learns when adding rules: a warning for a rule that matches nothing in the context now + * (often a typo), rules added together (all or none), and why a rule did nothing when it was skipped. + */ +class ChaosRuleFeedbackTest extends ChaosTestSupport { + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:orders").routeId("orders") + .setHeader("country", constant("DK")).id("enrich") + .to("mock:stock?retainFirst=1").id("callStock"); + // a consumer that polls on a schedule, which never polls in the test + from("file:target/chaos-feedback?initialDelay=3600000").routeId("poller").to("mock:polled"); + } + }; + } + + @Test + void aRuleThatFindsSomethingHasNoWarning() { + for (String rule : List.of("endpoint=mock:stock* error", "endpoint=direct:orders delay duration=1ms", + "component=mock error", "step=enrich error", "step=call* delay duration=1ms", + "route=orders corrupt mode=headers", "endpoint=file:target* drop at=poll", + "endpoint=mock:stock* error expect=orders:1", "route=orders stall duration=1s")) { + assertThat(chaos().checkRule(chaos().addRule(rule))).as(rule).isEmpty(); + chaos().reset(); + } + } + + @Test + void anEndpointTypoNamesTheEndpoints() { + String id = chaos().addRule("endpoint=mock:stok* error"); + + assertThat(chaos().checkRule(id)).singleElement().asString() + .startsWith("endpoint=mock:stok* matches no endpoint of the context now, so the rule fires 0 times") + .contains("the endpoints are: ") + .contains("mock://stock") + // without the query of the endpoint + .doesNotContain("retainFirst"); + assertThat(chaos().noTargetWarnings.get()).isEqualTo(1); + } + + @Test + void aStepRouteAndExpectationTypoAreNamed() { + assertThat(chaos().checkRule(chaos().addRule("step=callStok error"))).singleElement().asString() + .startsWith("step=callStok matches no step of the context now") + .contains("callStock", "enrich"); + assertThat(chaos().checkRule(chaos().addRule("route=order corrupt mode=headers"))).singleElement().asString() + .startsWith("route=order matches no route of the context now") + .endsWith("the routes are: orders, poller"); + assertThat(chaos().checkRule(chaos().addRule("endpoint=mock:stock* error expect=shiping:1"))) + .containsExactly("expects route shiping, but there is no route with that id (yet)"); + } + + @Test + void aPollRuleForAnEndpointWithoutAScheduledPollIsNamed() { + assertThat(chaos().checkRule(chaos().addRule("endpoint=direct:orders drop at=poll"))).singleElement() + .asString().startsWith("at=poll: no route consumes an endpoint that matches endpoint=direct:orders"); + // without at=poll the sends to it are reached + assertThat(chaos().checkRule(chaos().addRule("endpoint=direct:orders drop"))).isEmpty(); + } + + @Test + void aChangedRuleIsCheckedToo() { + String id = chaos().addRule("step=enrich error"); + chaos().changeRule(id, "step=enrihc error"); + + assertThat(chaos().checkRule(id)).singleElement().asString().startsWith("step=enrihc matches no step"); + assertThatThrownBy(() -> chaos().checkRule("r99")).hasMessage("There is no chaos rule r99"); + } + + @Test + void rulesAreAddedTogether() { + List<String> ids = chaos().addRules( + "endpoint=mock:stock* error at=dlq; step=enrich delay duration=1ms ;endpoint=mock:stock* error message='a; b'"); + + assertThat(ids).containsExactly("r1", "r2", "r3"); + assertThat(ruleReport("r3").getString("rule")).isEqualTo("endpoint=mock:stock* error message='a; b'"); + } + + @Test + void rulesAreAddedAllOrNone() { + assertThatThrownBy(() -> chaos().addRules("step=enrich error; step=enrich drop")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("Invalid chaos rule [step=enrich drop]"); + assertThat(chaos().hasRule("r1")).isFalse(); + assertThatThrownBy(() -> chaos().addRules(" ; ")).hasMessage("The chaos rule is empty"); + // the ids go on from the rules that were added + assertThat(chaos().addRule("step=enrich error")).isEqualTo("r1"); + } + + @Test + void theReasonOfASkipIsCounted() { + String charset = chaos().addRule("route=orders corrupt mode=charset until=2"); + template.sendBody("direct:orders", new ByteArrayInputStream("abc".getBytes(StandardCharsets.UTF_8))); + template.sendBody("direct:orders", null); + String truncate = chaos().addRule("endpoint=mock:stock* corrupt mode=truncate until=1"); + template.sendBody("direct:orders", "text"); + + JsonObject reasons = ruleReport(charset).getJsonObject("skippedReasons"); + assertThat(reasons).containsEntry("a stream body (charset changes a text body; mode=truncate changes a stream)", + 1L).containsEntry("no body", 1L); + assertThat(ruleReport(truncate).getJsonObject("skippedReasons")) + .containsEntry("not a stream body (truncate changes a stream or a stream cache)", 1L); + assertThat(chaos().report().toText()) + .contains("skipped 2 (a stream body (charset changes a text body; mode=truncate changes a stream): 1;" + + " no body: 1)"); + } +} 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 93b29b868044..de06851ac263 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc @@ -1079,11 +1079,13 @@ that changes rules is refused. The list and the report read the chaos console fr | `camel_runtime_chaos_add` | `false` | `true` -| Adds a rule (`rule`) and returns its id (`r1`, `r2`, ...). +| Adds a rule (`rule`), or several separated by `;` (all or none), and returns the ids (`r1`, `r2`, ...), with + `warnings` for a rule that matches nothing in the application now (often a typo). | `camel_runtime_chaos_change` | `false` | `true` -| Gives the rule with the `id` the new `rule` and one more version; the report keeps the version it replaces. +| Gives the rule with the `id` the new `rule` and one more version; the report keeps the version it replaces. Returns + `warnings` when the new rule matches nothing in the application now. | `camel_runtime_chaos_remove` | `false` | `false` diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-add.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-add.adoc index 538caa9f8dd9..fff9e87e943e 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-add.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-add.adoc @@ -2,7 +2,7 @@ // AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE = camel cmd chaos-add -Add a chaos rule to a running integration (camel-chaos, for development only) +Add chaos rules to a running integration (camel-chaos, for development only) == Usage @@ -19,7 +19,7 @@ camel cmd chaos-add [options] [cols="2,5,1,2",options="header"] |=== | Option | Description | Default | Type -| `--rule` | One rule in its one-line text form, as one entry of camel.chaos.rules | | String +| `--rule` | A rule in its one-line text form, as an entry of camel.chaos.rules; can be given more than once | | List | `--timeout` | How long to wait for the integration to answer, in millis or as a duration such as 30s | 10000 | long | `-h,--help` | Display the help and sub-commands | | boolean |=== diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc index 6ab676025a36..6dbf3a04dd98 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc @@ -19,7 +19,7 @@ camel cmd [options] |=== | Subcommand | Description | xref:jbang-commands/camel-jbang-cmd-browse.adoc[browse] | Browse pending messages on endpoints -| xref:jbang-commands/camel-jbang-cmd-chaos-add.adoc[chaos-add] | Add a chaos rule to a running integration (camel-chaos, for development only) +| xref:jbang-commands/camel-jbang-cmd-chaos-add.adoc[chaos-add] | Add chaos rules to a running integration (camel-chaos, for development only) | xref:jbang-commands/camel-jbang-cmd-chaos-change.adoc[chaos-change] | Change a chaos rule of a running integration (camel-chaos, for development only) | xref:jbang-commands/camel-jbang-cmd-chaos-clear.adoc[chaos-clear] | Remove all chaos rules of running integrations (camel-chaos, for development only) | xref:jbang-commands/camel-jbang-cmd-chaos-remove.adoc[chaos-remove] | Remove a chaos rule of a running integration (camel-chaos, for development only) diff --git a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/ChaosAction.java b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/ChaosAction.java index 9eb8dba9b8e9..af8e5afe5ede 100644 --- a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/ChaosAction.java +++ b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/ChaosAction.java @@ -19,8 +19,10 @@ package org.apache.camel.cli.connector; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.List; import org.apache.camel.CamelContext; +import org.apache.camel.util.json.JsonArray; import org.apache.camel.util.json.JsonObject; /** @@ -69,8 +71,14 @@ final class ChaosAction { output.error("A rule is required"); return; } - answer.put("id", call(type, chaos, "addRule", rule)); + // one rule or several separated by ; (as camel.chaos.rules), added together + List<?> ids = (List<?>) call(type, chaos, "addRules", rule); + if (ids.size() == 1) { + answer.put("id", ids.get(0)); + } + answer.put("ids", new JsonArray(ids)); answer.put("version", 1); + warnings(type, chaos, ids, answer); } else if ("change".equals(command)) { if (isBlank(id) || isBlank(rule)) { output.error("The id of the rule and the new rule are required"); @@ -82,6 +90,7 @@ final class ChaosAction { } answer.put("id", id); answer.put("version", call(type, chaos, "changeRule", id, rule)); + warnings(type, chaos, List.of(id), answer); } else if ("remove".equals(command)) { if (isBlank(id)) { output.error("The id of the rule is required"); @@ -117,6 +126,22 @@ final class ChaosAction { output.write(answer); } + /** + * The warnings of the rules that match nothing in the integration now (often a typo), each with the id of its rule, + * for the tool to show: the rule is added anyway, as the routes may still change. + */ + private static void warnings(Class<?> type, Object chaos, List<?> ids, JsonObject answer) throws Exception { + JsonArray warnings = new JsonArray(); + for (Object id : ids) { + for (Object warning : (List<?>) call(type, chaos, "checkRule", (String) id)) { + warnings.add(id + ": " + warning); + } + } + if (!warnings.isEmpty()) { + answer.put("warnings", warnings); + } + } + private static boolean isBlank(String text) { return text == null || text.isBlank(); } diff --git a/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/LocalCliConnectorChaosActionTest.java b/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/LocalCliConnectorChaosActionTest.java index 45c480d5814e..46a71e95e65b 100644 --- a/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/LocalCliConnectorChaosActionTest.java +++ b/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/LocalCliConnectorChaosActionTest.java @@ -152,6 +152,27 @@ class LocalCliConnectorChaosActionTest { assertThat(reportRules()).isEmpty(); } + @Test + void rulesAreAddedTogetherWithWarningsForTheOnesThatMatchNothing() throws Exception { + start(null, Map.of("camel.chaos.enabled", "true")); + + CliActionOutcome added = chaos("add", null, "endpoint=mock:target error at=dlq; step=tpyo delay duration=1ms"); + assertThat(added.error()).isNull(); + assertThat(added.result().<List<Object>> getCollection("ids")).containsExactly("r1", "r2"); + // the id of a single rule only + assertThat(added.result().containsKey("id")).isFalse(); + assertThat(added.result().<List<Object>> getCollection("warnings")).singleElement().asString() + .startsWith("r2: step=tpyo matches no step of the context now"); + + CliActionOutcome one = chaos("add", null, "endpoint=mock:target delay duration=1ms"); + assertThat(one.result().getString("id")).isEqualTo("r3"); + assertThat(one.result().containsKey("warnings")).isFalse(); + + CliActionOutcome changed = chaos("change", "r3", "endpoint=mock:taget delay duration=1ms"); + assertThat(changed.result().<List<Object>> getCollection("warnings")).singleElement().asString() + .startsWith("r3: endpoint=mock:taget matches no endpoint of the context now"); + } + @Test void aRuleThatIsNotValidIsRefusedWithTheReason() throws Exception { start(null, Map.of("camel.chaos.enabled", "true")); diff --git a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json index b771e638345a..88361384e3ea 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json +++ b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json @@ -3,7 +3,7 @@ { "name": "ask", "fullName": "ask", "description": "Ask a question about a running Camel application using AI", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": "--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars", "javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", "description": "API type: 'ollama', 'openai', 'anthropic', or 'watsonx'", "javaType": [...] { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind source and sink Kamelets as a new Camel integration", "deprecated": true, "sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": [ { "names": "--error-handler", "description": "Add error handler (none|log|sink:<endpoint>). Sink endpoints are expected in the format \"[[apigroup\/]version:]kind:[namespace\/]name\", plain Camel URIs or Kamelet name.", "javaType": "java.lang.String", "type": " [...] { "name": "catalog", "fullName": "catalog", "description": "List artifacts from Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "component", "fullName": "catalog component", "description": "List components from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...] - { "name": "cmd", "fullName": "cmd", "description": "Performs commands in the running Camel integrations, such as start\/stop route, or change logging levels.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "browse", "fullName": "cmd browse", "description": "Browse pending messages on endpoints [...] + { "name": "cmd", "fullName": "cmd", "description": "Performs commands in the running Camel integrations, such as start\/stop route, or change logging levels.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "browse", "fullName": "cmd browse", "description": "Browse pending messages on endpoints [...] { "name": "completion", "fullName": "completion", "description": "Generate completion script for bash\/zsh", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "config", "fullName": "config", "description": "Get and set user configuration values", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", "fullName": "config get", "description": "Display user configuration value", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...] { "name": "debug", "fullName": "debug", "description": "Debug local Camel integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", "options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": "--background", "description": "Run in the background", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--background-wait", "description": "How [...] diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosAction.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosAction.java index fd615100286b..5c265e4fed2e 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosAction.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosAction.java @@ -115,4 +115,16 @@ abstract class CamelChaosAction extends ActionBaseCommand { * The line printed for the answer of an integration that took the command. */ protected abstract String describe(JsonObject answer); + + /** + * The warnings of the answer, one per line after the line of the answer: the rules that match nothing in the + * integration now (often a typo), which were added anyway. + */ + protected static String warnings(JsonObject answer) { + List<Object> warnings = answer.getCollection("warnings"); + if (warnings == null || warnings.isEmpty()) { + return ""; + } + return warnings.stream().map(w -> "\n Warning: " + w).collect(Collectors.joining()); + } } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosAddAction.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosAddAction.java index d08740569a6e..8f4b4a89ed1d 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosAddAction.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosAddAction.java @@ -16,30 +16,37 @@ */ package org.apache.camel.dsl.jbang.core.commands.action; +import java.util.List; +import java.util.stream.Collectors; + import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; import org.apache.camel.dsl.jbang.core.common.ProcessFinder.CamelProcess; import org.apache.camel.util.json.JsonObject; import picocli.CommandLine; @CommandLine.Command(name = "chaos-add", - description = "Add a chaos rule to a running integration (camel-chaos, for development only)", + description = "Add chaos rules to a running integration (camel-chaos, for development only)", sortOptions = false, showDefaultValues = true, footer = { "%nThe integration must have camel-chaos switched on (camel run --chaos, profile dev). The rule is", "checked as camel.chaos.rules is; a rule that is not valid is refused with the reason. Its id", - "(r1, r2, ...) is printed, for chaos-change and chaos-remove.", + "(r1, r2, ...) is printed, for chaos-change and chaos-remove. Several --rule options (or rules", + "separated by ;) are added together, all or none, so they are in place at the same time. A rule", + "that matches nothing in the integration now (often a typo) is added with a warning.", "%nExamples:", " camel cmd chaos-add myApp --rule='endpoint=kafka:orders* delay duration=2s rate=0.2 until=5m'", " camel cmd chaos-add myApp --rule='endpoint=file:inbox* drop at=poll rate=0.5 until=2m'", - " camel cmd chaos-add myApp --rule='step=enrich* error exception=java.sql.SQLException rate=0.1'" }) + " camel cmd chaos-add myApp --rule='step=enrich* error exception=java.sql.SQLException rate=0.1'", + " camel cmd chaos-add myApp --rule='endpoint=seda:dlq error at=dlq' --rule='endpoint=http:stock* error'" }) public class CamelChaosAddAction extends CamelChaosAction { @CommandLine.Parameters(description = "Name or pid of the running Camel integration (must match one)", arity = "1") String name; @CommandLine.Option(names = { "--rule" }, required = true, - description = "One rule in its one-line text form, as one entry of camel.chaos.rules") - String rule; + description = "A rule in its one-line text form, as an entry of camel.chaos.rules; can be given more" + + " than once") + List<String> rules; public CamelChaosAddAction(CamelJBangMain main) { super(main); @@ -49,12 +56,18 @@ public class CamelChaosAddAction extends CamelChaosAction { public Integer doCall() throws Exception { // a rule goes to one integration: its id is only known there CamelProcess process = findOne(name); - printer().println(describe(chaos(process, "add", root -> root.put("rule", rule)))); + // the rules go together, separated by ; as in camel.chaos.rules (a ; inside a rule is quoted) + String text = String.join(";", rules); + printer().println(describe(chaos(process, "add", root -> root.put("rule", text)))); return 0; } @Override protected String describe(JsonObject answer) { - return "Added chaos rule " + answer.getString("id"); + List<Object> ids = answer.getCollection("ids"); + String added = ids != null && ids.size() > 1 + ? "Added chaos rules " + ids.stream().map(String::valueOf).collect(Collectors.joining(", ")) + : "Added chaos rule " + answer.getString("id"); + return added + warnings(answer); } } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosChangeAction.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosChangeAction.java index d23f7e917d8c..d76a77e21811 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosChangeAction.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosChangeAction.java @@ -57,6 +57,7 @@ public class CamelChaosChangeAction extends CamelChaosAction { @Override protected String describe(JsonObject answer) { - return "Changed chaos rule " + answer.getString("id") + " (version " + answer.get("version") + ")"; + return "Changed chaos rule " + answer.getString("id") + " (version " + answer.get("version") + ")" + + warnings(answer); } } 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 3c1d72163917..541cc2a1c106 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 @@ -732,10 +732,14 @@ public final class ToolRegistry { register(tool("chaos_add", "Add a chaos rule to the integration, in the one-line form of camel.chaos.rules, e.g. " - + "'endpoint=kafka:orders* error rate=0.1 until=5m'. Returns the id of the rule. " - + "Chaos must be on in the integration; a rule that is not valid is refused with " - + "the reason.") - .param("rule", "string", "The rule in its one-line text form, as one entry of camel.chaos.rules", true) + + "'endpoint=kafka:orders* error rate=0.1 until=5m'; several rules separated by ; " + + "are added together (all or none). Returns the ids of the rules, and warnings for " + + "a rule that matches nothing in the integration now (often a typo: fix it with " + + "chaos_change). Chaos must be on in the integration; a rule that is not valid is " + + "refused with the reason.") + .param("rule", "string", + "The rule in its one-line text form, as an entry of camel.chaos.rules; several rules separated by ;", + true) .param("name", "string", CHAOS_NAME, false) .readOnly(false).destructive(true) .executor((ctx, args) -> { @@ -747,7 +751,7 @@ public final class ToolRegistry { register(tool("chaos_change", "Change a chaos rule of the integration: the rule with the id gets the new one-line rule and one more " + "version; the report keeps the version it replaces. Returns the id and the new " - + "version.") + + "version, and warnings when the new rule matches nothing in the integration now.") .param("id", "string", "The id of the rule, such as r1 (chaos_list shows them)", true) .param("rule", "string", "The new rule in its one-line text form", true) .param("name", "string", CHAOS_NAME, false) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java index aa189f0f782a..46456d806aee 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java @@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.process; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.StringJoiner; import com.github.freva.asciitable.AsciiTable; import com.github.freva.asciitable.Column; @@ -129,14 +130,18 @@ public class ListChaos extends ProcessWatchCommand { if (r.rule.get("lifecycle") != null) { jo.put("lifecycle", r.rule.get("lifecycle")); } + if (r.rule.get("skippedReasons") != null) { + jo.put("skippedReasons", r.rule.get("skippedReasons")); + } return jo; }).toList())); return; } if (rows.isEmpty()) { - printOff(integrations); + printNothing(integrations, false); return; } + boolean notes = rows.stream().anyMatch(r -> !note(r.rule).isEmpty()); printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, List.of( new Column().header("PID").headerAlign(HorizontalAlign.CENTER).with(r -> r.integration.pid), new Column().header("NAME").dataAlign(HorizontalAlign.LEFT).maxWidth(30, OverflowBehaviour.ELLIPSIS_RIGHT) @@ -153,6 +158,8 @@ public class ListChaos extends ProcessWatchCommand { .with(r -> String.valueOf(r.rule.get("skipped"))), new Column().header("ENDS IN").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT) .with(r -> endsIn(r.rule)), + new Column().header("NOTE").visible(notes).dataAlign(HorizontalAlign.LEFT) + .maxWidth(40, OverflowBehaviour.NEWLINE).with(r -> note(r.rule)), new Column().header("RULE").dataAlign(HorizontalAlign.LEFT).maxWidth(80, OverflowBehaviour.NEWLINE) .with(r -> r.rule.getString("rule"))))); } @@ -185,7 +192,7 @@ public class ListChaos extends ProcessWatchCommand { printReport(integration, report); } if (!any) { - printOff(integrations); + printNothing(integrations, true); } } @@ -209,6 +216,7 @@ public class ListChaos extends ProcessWatchCommand { JsonArray rules = report.getCollection("rules"); if (rules != null && !rules.isEmpty()) { List<JsonObject> list = rules.stream().map(JsonObject.class::cast).toList(); + boolean notes = list.stream().anyMatch(r -> !note(r).isEmpty()); printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, list, List.of( new Column().header("ID").dataAlign(HorizontalAlign.LEFT).with(r -> r.getString("id")), new Column().header("VER").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT) @@ -221,6 +229,8 @@ public class ListChaos extends ProcessWatchCommand { new Column().header("SKIPPED").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT) .with(r -> String.valueOf(r.get("skipped"))), new Column().header("EXPECT").dataAlign(HorizontalAlign.LEFT).with(ListChaos::expectation), + new Column().header("NOTE").visible(notes).dataAlign(HorizontalAlign.LEFT) + .maxWidth(40, OverflowBehaviour.NEWLINE).with(ListChaos::note), new Column().header("RULE").dataAlign(HorizontalAlign.LEFT).maxWidth(80, OverflowBehaviour.NEWLINE) .with(r -> r.getString("rule"))))); } @@ -244,15 +254,53 @@ public class ListChaos extends ProcessWatchCommand { } /** - * Without a rule to show, says why for each integration where chaos is off, on stderr, so the output stays empty. + * Without a rule to show, says why on stderr, so the output stays empty: for each integration where chaos is off + * the reason, for each one where it is on that it has no rules, and when no integration has camel-chaos at all, + * that. */ - private void printOff(List<Row> integrations) { + private void printNothing(List<Row> integrations, boolean report) { + boolean any = false; for (Row integration : integrations) { - if (integration.chaos != null && !integration.isOn()) { - printer().printDiagnostic(integration.name + " (pid " + integration.pid + "): " - + integration.chaos.getStringOrDefault("reason", "chaos is off")); + if (integration.chaos == null) { + continue; + } + any = true; + String prefix = integration.name + " (pid " + integration.pid + "): "; + if (!integration.isOn()) { + printer().printDiagnostic(prefix + integration.chaos.getStringOrDefault("reason", "chaos is off")); + } else if (!report) { + printer().printDiagnostic(prefix + "no chaos rules (add one with camel cmd chaos-add)"); } } + if (!any && !integrations.isEmpty()) { + printer().printDiagnostic("No running integration has camel-chaos: run it with camel run --chaos"); + } + } + + /** + * What a person should know about a rule, besides its counts: what a lifecycle rule did to its route, and why the + * rule did nothing when it was skipped. + */ + static String note(JsonObject rule) { + StringJoiner sj = new StringJoiner("; "); + if (rule.get("lifecycle") instanceof JsonObject l) { + String state = l.getString("state"); + StringBuilder sb = new StringBuilder("route ").append(l.getString("route")).append(' ').append(state); + if (l.get("backIn") instanceof Number n) { + sb.append(", back in ").append(TimeUtils.printDuration(n.longValue())); + } else if ("back".equals(state) && l.get("downAt") instanceof Number down + && l.get("backAt") instanceof Number back) { + sb.append(" after ").append(TimeUtils.printDuration(back.longValue() - down.longValue())); + } + if (l.getString("reason") != null) { + sb.append(": ").append(l.getString("reason")); + } + sj.add(sb); + } + if (rule.get("skippedReasons") instanceof Map<?, ?> reasons && rule.get("lifecycle") == null) { + reasons.forEach((reason, count) -> sj.add("skipped " + count + ": " + reason)); + } + return sj.toString(); } private static long fired(JsonObject rule) { diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java index 51949221bef9..04c7c5abe6ef 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java @@ -159,6 +159,54 @@ class ListChaosTest extends ProcessCommandTestSupport { assertThat(out).contains("orders:100 pending (2 completed, 30s left)"); } + @Test + void theNoteSaysWhatALifecycleRuleDidAndWhyARuleWasSkipped() throws Exception { + JsonObject chaos = chaosOn(); + JsonObject stall = rule(1, "active", "route=poller stall duration=20s"); + stall.put("id", "r2"); + JsonObject lifecycle = new JsonObject(); + lifecycle.put("action", "suspend"); + lifecycle.put("route", "poller"); + lifecycle.put("state", "down"); + lifecycle.put("backIn", 12000L); + stall.put("lifecycle", lifecycle); + JsonObject corrupt = rule(1, "active", "route=shipping corrupt mode=charset"); + corrupt.put("id", "r3"); + corrupt.put("skippedReasons", new JsonObject(Map.of("a stream body", 79L))); + JsonArray rules = chaos.getCollection("rules"); + rules.add(stall); + rules.add(corrupt); + writeStatus(chaos); + + assertThat(run()).isZero(); + + String out = printer.getStdout(); + assertThat(out).contains("NOTE"); + assertThat(lineOf(out, "r2")).contains("route poller down, back in 12s"); + assertThat(lineOf(out, "r3")).contains("skipped 79: a stream body"); + } + + @Test + void withoutANoteThereIsNoNoteColumn() throws Exception { + writeStatus(chaosOn()); + + assertThat(run()).isZero(); + + assertThat(printer.getStdout()).doesNotContain("NOTE"); + } + + @Test + void noRulesSaysSo() throws Exception { + JsonObject chaos = chaosOn(); + chaos.put("rules", new JsonArray()); + writeStatus(chaos); + + assertThat(run()).isZero(); + + assertThat(printer.getStdout()).isEmpty(); + assertThat(printer.getStderr()).contains("orders (pid " + TEST_PID + "): no chaos rules"); + } + @Test void chaosOff() throws Exception { writeStatus(new JsonObject(Map.of("enabled", false, "reason", "Chaos is off: it stays off with profile prod"))); @@ -186,7 +234,9 @@ class ListChaosTest extends ProcessCommandTestSupport { assertThat(run()).isZero(); - assertThat(printer.getOutput()).isEmpty(); + // nothing on stdout, and why on stderr + assertThat(printer.getStdout()).isEmpty(); + assertThat(printer.getStderr()).contains("No running integration has camel-chaos: run it with camel run --chaos"); } private int run(String... args) throws Exception { 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 339dcf2e26eb..e5b3c737a0a2 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 @@ -1094,11 +1094,14 @@ public class RuntimeTools { @Tool(annotations = @Tool.Annotations(readOnlyHint = false, destructiveHint = true, openWorldHint = false), description = """ Add a chaos rule to the Camel application, in the one-line form of camel.chaos.rules, e.g. \ - 'endpoint=kafka:orders* error rate=0.1 until=5m'. Returns the id of the rule. Chaos must be on in \ - the application; a rule that is not valid is refused with the reason.""") + 'endpoint=kafka:orders* error rate=0.1 until=5m'; several rules separated by ; are added together \ + (all or none). Returns the ids of the rules, and warnings for a rule that matches nothing in the \ + application now (often a typo: fix it with camel_runtime_chaos_change). Chaos must be on in the \ + application; a rule that is not valid is refused with the reason.""") public JsonObject camel_runtime_chaos_add( @ToolArg(description = NAME_OR_PID_DESC, required = false) String nameOrPid, - @ToolArg(description = "The rule in its one-line text form, as one entry of camel.chaos.rules") String rule) { + @ToolArg(description = "The rule in its one-line text form, as an entry of camel.chaos.rules; several" + + " rules separated by ;") String rule) { requireArg("rule", rule); return delegateToRegistry("chaos_add", nameOrPid, Map.of("rule", rule)); } @@ -1107,7 +1110,8 @@ public class RuntimeTools { @Tool(annotations = @Tool.Annotations(readOnlyHint = false, destructiveHint = true, openWorldHint = false), description = """ Change a chaos rule of the Camel application: the rule with the id gets the new one-line rule and \ - one more version; the report keeps the version it replaces. Returns the id and the new version.""") + one more version; the report keeps the version it replaces. Returns the id and the new version, \ + and warnings when the new rule matches nothing in the application now.""") public JsonObject camel_runtime_chaos_change( @ToolArg(description = NAME_OR_PID_DESC, required = false) String nameOrPid, @ToolArg(description = "The id of the rule, such as r1 (camel_runtime_chaos_list shows them)") String id,
