This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 0b1e41c5c42c CAMEL-26676: camel-chaos - feedback while adding rules
(#29311)
0b1e41c5c42c is described below
commit 0b1e41c5c42c8950ece37b572c83ad2bd06eb9cd
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Nov 6 10:41:11 2026 +0100
CAMEL-26676: camel-chaos - feedback while adding rules (#29311)
* CAMEL-26675: camel-chaos - report accuracy
- The send of the dead letter channel is a hook of its own (at=dlq): an
endpoint= or component= rule no longer fails it by accident, and a rule
with at=dlq that keeps a message from the dead letter channel counts it
as lost, with a FAIL verdict.
- A route counts an exchange once (the last pass), not once per pass:
direct routes called in a loop or again by a redelivery, expectations
and the entry faults.
- An expectation of a removed or changed rule, or of a stopped context, is
cancelled, so the report can be final again.
- chaos-clear --reset (also reset=true of the MCP and camel ask tool, and
ChaosManager.reset()) starts a new report.
- The rules of camel.chaos.rules start their clock when the context has
started.
- The report lists the rules by id, and the versions of an id from the
latest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* 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]>
* CAMEL-26675: camel-chaos - update the CLI help golden file for
chaos-clear --reset
Also wraps the chaos-clear footer within 80 columns.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-26676: camel-chaos - update the CLI help golden file for chaos-add
Also wraps the chaos-add footer within 80 columns.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---------
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Opus 5.5 <[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 | 32 +++--
.../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 +++++++-
.../src/test/resources/golden/cli-help.txt | 24 ++--
.../dsl/jbang/core/commands/mcp/RuntimeTools.java | 12 +-
24 files changed, 723 insertions(+), 92 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 dc901e118881..bb20880b7ede 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
@@ -273,7 +291,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>`
@@ -401,7 +419,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 dc901e118881..bb20880b7ede 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
@@ -273,7 +291,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>`
@@ -401,7 +419,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..215597837395 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,38 @@
*/
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.",
+ "%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. 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 +57,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-core/src/test/resources/golden/cli-help.txt
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
index 2f88918f942f..99ea7b26cafa 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
@@ -552,8 +552,8 @@ or change logging levels. (use cmd --help to see sub
commands)
-h, --help Display the help and sub-commands
Commands:
browse Browse pending messages on endpoints
- chaos-add Add a chaos rule to a running integration (camel-chaos,
- for development only)
+ chaos-add Add chaos rules to a running integration (camel-chaos, for
+ development only)
chaos-change Change a chaos rule of a running integration (camel-chaos,
for development only)
chaos-clear Remove all chaos rules of running integrations
@@ -640,22 +640,24 @@ Examples:
camel cmd browse --endpoint=seda:foo --limit=10
== camel cmd chaos-add --help
-Usage: camel cmd chaos-add [-h] --rule=<rule> [--timeout=<timeout>] <name>
-Add a chaos rule to a running integration (camel-chaos, for development only)
+Usage: camel cmd chaos-add [-h] [--timeout=<timeout>] --rule=<rules>
+ [--rule=<rules>]... <name>
+Add chaos rules to a running integration (camel-chaos, for development only)
<name> Name or pid of the running Camel integration (must
match one)
-h, --help Display the help and sub-commands
--timeout=<timeout> How long to wait for the integration to answer, in
millis or as a duration such as 30s
Default: 10000
- --rule=<rule> One rule in its one-line text form, as one entry of
- camel.chaos.rules
+ --rule=<rules> A rule in its one-line text form, as an entry of
+ camel.chaos.rules; can be given more than once
The 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.
+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. 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.
Examples:
camel cmd chaos-add myApp --rule='endpoint=kafka:orders* delay duration=2s
@@ -664,6 +666,8 @@ rate=0.2 until=5m'
until=2m'
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'
== camel cmd chaos-change --help
Usage: camel cmd chaos-change [-h] --id=<id> --rule=<rule>
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,