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 20535aae4d74 CAMEL-26727: camel-chaos - what to try: chaos rules
suggested per route from what the route does (#29397)
20535aae4d74 is described below
commit 20535aae4d744cc0adb1479852483b4bdf16c816
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Nov 8 09:13:04 2026 +0100
CAMEL-26727: camel-chaos - what to try: chaos rules suggested per route
from what the route does (#29397)
ChaosAdvisor suggests rules per route from the running routes (their remote
sends, circuit breakers, dead letter channel, input, and hand-offs between
routes), each a known rule of the chaos model with its target filled in and why
it is worth trying, the most worth trying first. camel cmd chaos-suggest prints
them with the command that adds each; w on the chaos screen lists them and
types the picked one in. The camel-chaos page has a new section, What to try.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../org/apache/camel/catalog/docs/chaos.adoc | 44 ++++
components/camel-chaos/src/main/docs/chaos.adoc | 44 ++++
.../apache/camel/component/chaos/ChaosAdvisor.java | 245 +++++++++++++++++++++
.../apache/camel/component/chaos/ChaosManager.java | 27 +++
.../camel/component/chaos/ChaosAdvisorTest.java | 117 ++++++++++
design/chaos.adoc | 4 +
.../modules/ROOT/pages/camel-jbang-managing.adoc | 3 +
.../ROOT/pages/camel-jbang-tui-observe.adoc | 3 +
.../camel-jbang-cmd-chaos-suggest.adoc | 26 +++
.../ROOT/pages/jbang-commands/camel-jbang-cmd.adoc | 1 +
.../apache/camel/cli/connector/ChaosAction.java | 8 +-
.../LocalCliConnectorChaosActionTest.java | 11 +
.../META-INF/camel-jbang-commands-metadata.json | 2 +-
.../dsl/jbang/core/commands/CamelJBangMain.java | 1 +
.../commands/action/CamelChaosSuggestAction.java | 79 +++++++
.../core/commands/action/CamelChaosActionTest.java | 31 ++-
.../src/test/resources/golden/cli-help.txt | 30 +++
.../src/test/resources/golden/cli-parse.txt | 7 +
.../dsl/jbang/core/commands/tui/ChaosControl.java | 91 +++++++-
.../jbang/core/commands/tui/ChaosSuggestList.java | 163 ++++++++++++++
.../src/main/resources/tui/help/chaos.md | 5 +
.../jbang/core/commands/tui/ChaosControlTest.java | 38 +++-
22 files changed, 974 insertions(+), 6 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 743afbc5e3a6..bfc599944098 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
@@ -124,6 +124,7 @@ camel cmd chaos-remove myapp --id=r1
camel cmd chaos-clear
camel cmd chaos-clear myapp --reset
camel cmd chaos-load chaos/outage.chaos
+camel cmd chaos-suggest
camel get chaos
camel get chaos myapp --report
----
@@ -179,6 +180,49 @@ switched on in the integration (see
<<_switching_it_on,Switching it on>>): they
The `chaos` developer console (`/q/dev/chaos` with `camel run --console`)
shows the same rules and report, as text or
JSON. It only reads: a rule cannot be added, changed or removed over HTTP.
+=== What to try
+
+Where to start with a route you did not write? `camel cmd chaos-suggest` asks
the running integration, and it suggests
+rules per route from what the routes do, the most worth trying first. Each is
a known rule (see
+<<_the_model_of_the_rules,The model of the rules>>) with the endpoint or route
filled in, why it is worth trying, and
+the command that adds it. Nothing is added.
+
+[source]
+----
+$ camel cmd chaos-suggest
+Chaos rules to try, the most worth trying first:
+
+1. route orders: Returns an error (HTTP status) (errors)
+ an HTTP call to http://stock/check without a circuit breaker: what does the
route do when the service answers 503?
+ camel cmd chaos-add --rule='endpoint=http://stock/check error status=503
rate=0.25 until=10m'
+
+2. route orders: The route after a hand-off fails (lost)
+ route orders hands the message to route shipping over seda and does not
wait: when shipping fails, is the message lost?
+ camel cmd chaos-add --rule='route=shipping error rate=0.2 until=2m
expect=orders->shipping:lost<=0 within=3m'
+----
+
+[cols="2,3"]
+|===
+|What a route does |What is suggested
+
+|An HTTP call, without a circuit breaker |The service answers with an error
(`status=503`), and does not answer in time
+(`timeout`)
+|An HTTP call, in a route with a circuit breaker |The service is down for a
while (`outage`): does the breaker open, and
+does the fallback answer?
+|A call over another protocol |The connection is refused, and the call is slow
+|An input that can deliver a message again (Kafka, JMS, AMQP, SQS and more, or
a consumer that reads a failed message
+again, such as a file without `moveFailed`) |The call out is made twice
(`duplicate`): is it safe to repeat?
+|A dead letter channel |It fails too (`at=dlq`): is the message then lost? Add
it with a rule that fails the route.
+|A hand-off to another route over `seda`, `vm` or `disruptor` |The next route
fails, with an expectation that no
+message is lost between the two routes (see
<<_expectations_on_time_and_loss,Expectations on time and loss>>)
+|An input from outside (not `direct` or `seda`) |The route fails at its entry
(what does the sender see: a rollback, a
+retry, an error?), or the polls fail for a polling consumer; and the route
stalls for a while
+|===
+
+The rules leave the query of the endpoint out: its options do not tell which
endpoint it is, and may hold a secret.
+`ChaosManager.suggest()` gives the same as JSON (`suggestions`: `route`,
`kind`, `recipe`, `title`, `rule`, `why`), and
+the chaos screen of the Camel TUI lists them (key `w`), where Enter types the
rule in to change or add.
+
=== From an AI agent (MCP)
The Camel MCP server (`camel mcp`, see xref:manual::camel-jbang-mcp.adoc[Camel
MCP server]) has the same operations
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc
b/components/camel-chaos/src/main/docs/chaos.adoc
index 743afbc5e3a6..bfc599944098 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -124,6 +124,7 @@ camel cmd chaos-remove myapp --id=r1
camel cmd chaos-clear
camel cmd chaos-clear myapp --reset
camel cmd chaos-load chaos/outage.chaos
+camel cmd chaos-suggest
camel get chaos
camel get chaos myapp --report
----
@@ -179,6 +180,49 @@ switched on in the integration (see
<<_switching_it_on,Switching it on>>): they
The `chaos` developer console (`/q/dev/chaos` with `camel run --console`)
shows the same rules and report, as text or
JSON. It only reads: a rule cannot be added, changed or removed over HTTP.
+=== What to try
+
+Where to start with a route you did not write? `camel cmd chaos-suggest` asks
the running integration, and it suggests
+rules per route from what the routes do, the most worth trying first. Each is
a known rule (see
+<<_the_model_of_the_rules,The model of the rules>>) with the endpoint or route
filled in, why it is worth trying, and
+the command that adds it. Nothing is added.
+
+[source]
+----
+$ camel cmd chaos-suggest
+Chaos rules to try, the most worth trying first:
+
+1. route orders: Returns an error (HTTP status) (errors)
+ an HTTP call to http://stock/check without a circuit breaker: what does the
route do when the service answers 503?
+ camel cmd chaos-add --rule='endpoint=http://stock/check error status=503
rate=0.25 until=10m'
+
+2. route orders: The route after a hand-off fails (lost)
+ route orders hands the message to route shipping over seda and does not
wait: when shipping fails, is the message lost?
+ camel cmd chaos-add --rule='route=shipping error rate=0.2 until=2m
expect=orders->shipping:lost<=0 within=3m'
+----
+
+[cols="2,3"]
+|===
+|What a route does |What is suggested
+
+|An HTTP call, without a circuit breaker |The service answers with an error
(`status=503`), and does not answer in time
+(`timeout`)
+|An HTTP call, in a route with a circuit breaker |The service is down for a
while (`outage`): does the breaker open, and
+does the fallback answer?
+|A call over another protocol |The connection is refused, and the call is slow
+|An input that can deliver a message again (Kafka, JMS, AMQP, SQS and more, or
a consumer that reads a failed message
+again, such as a file without `moveFailed`) |The call out is made twice
(`duplicate`): is it safe to repeat?
+|A dead letter channel |It fails too (`at=dlq`): is the message then lost? Add
it with a rule that fails the route.
+|A hand-off to another route over `seda`, `vm` or `disruptor` |The next route
fails, with an expectation that no
+message is lost between the two routes (see
<<_expectations_on_time_and_loss,Expectations on time and loss>>)
+|An input from outside (not `direct` or `seda`) |The route fails at its entry
(what does the sender see: a rollback, a
+retry, an error?), or the polls fail for a polling consumer; and the route
stalls for a while
+|===
+
+The rules leave the query of the endpoint out: its options do not tell which
endpoint it is, and may hold a secret.
+`ChaosManager.suggest()` gives the same as JSON (`suggestions`: `route`,
`kind`, `recipe`, `title`, `rule`, `why`), and
+the chaos screen of the Camel TUI lists them (key `w`), where Enter types the
rule in to change or add.
+
=== From an AI agent (MCP)
The Camel MCP server (`camel mcp`, see xref:manual::camel-jbang-mcp.adoc[Camel
MCP server]) has the same operations
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java
new file mode 100644
index 000000000000..0decbcc9fdca
--- /dev/null
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java
@@ -0,0 +1,245 @@
+/*
+ * 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.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Endpoint;
+import org.apache.camel.EndpointAware;
+import org.apache.camel.Processor;
+import org.apache.camel.Route;
+import org.apache.camel.model.CircuitBreakerDefinition;
+import org.apache.camel.model.ProcessorDefinitionHelper;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.model.errorhandler.DeadLetterChannelDefinition;
+import org.apache.camel.support.ScheduledPollConsumer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * What to try (CAMEL-26727): chaos rules suggested per route from what the
route does, ranked, each a known rule of the
+ * chaos model ({@link ChaosModel#recipeList()}) with its target filled in and
a line that says why. The facts come from
+ * the running routes: their sends, their circuit breakers, their dead letter
channel, their input, and the hand-offs
+ * between them.
+ */
+final class ChaosAdvisor {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(ChaosAdvisor.class);
+
+ /** The most suggestions in all, so a big integration does not flood a
tool. */
+ static final int MAX = 40;
+
+ // the inputs that can deliver a message again (at least once), so a call
out may be made twice
+ private static final Set<String> AT_LEAST_ONCE = Set.of(
+ "kafka", "jms", "activemq", "activemq6", "amqp", "sjms", "sjms2",
"aws2-sqs", "google-pubsub",
+ "spring-rabbitmq", "rabbitmq", "nats", "paho", "paho-mqtt5",
"azure-servicebus", "ibm-mq");
+ // a hand-off to another route of the same application, which does not
wait for it
+ private static final Set<String> HAND_OFF = Set.of("seda", "vm",
"disruptor", "disruptor-vm");
+
+ /** The kinds of faults, which the report also uses for what has been
tried. */
+ enum Kind {
+ ERRORS("errors"),
+ SLOW("slow"),
+ LOST("lost"),
+ CORRUPT("corrupt"),
+ DUPLICATE("duplicate"),
+ CONSUMER("consumer"),
+ LIFECYCLE("lifecycle"),
+ DEAD_LETTER("dead letter channel");
+
+ final String key;
+
+ Kind(String key) {
+ this.key = key;
+ }
+ }
+
+ /**
+ * A rule to try.
+ *
+ * @param route the route it is about
+ * @param kind the kind of fault
+ * @param recipe the id of the known rule it comes from (null for one of
its own)
+ * @param title the known rule in plain words, such as "Returns an error
(HTTP status)"
+ * @param rule the rule, ready to add
+ * @param why why it is worth trying for this route
+ * @param rank higher is tried first
+ */
+ record Suggestion(String route, Kind kind, String recipe, String title,
String rule, String why, int rank) {
+ }
+
+ private ChaosAdvisor() {
+ }
+
+ /** The rules to try for the routes of the context, the most worth trying
first, at most {@link #MAX}. */
+ static List<Suggestion> suggest(CamelContext camelContext) {
+ Map<String, ChaosModel.Recipe> recipes = new LinkedHashMap<>();
+ for (ChaosModel.Recipe recipe : ChaosModel.recipeList()) {
+ recipes.put(recipe.id(), recipe);
+ }
+ List<Route> routes = camelContext.getRoutes();
+ // the routes that consume an endpoint, for the hand-offs
+ Map<String, String> consumers = new LinkedHashMap<>();
+ for (Route route : routes) {
+ if (route.getEndpoint() != null) {
+ consumers.put(uri(route.getEndpoint()), route.getRouteId());
+ }
+ }
+ List<Suggestion> answer = new ArrayList<>();
+ for (Route route : routes) {
+ try {
+ suggest(route, recipes, consumers, answer);
+ } catch (RuntimeException e) {
+ // the route changed while it was read (such as a reload):
nothing to say for it now
+ LOG.debug("No chaos suggestions for route {}: {}",
route.getRouteId(), e.getMessage(), e);
+ }
+ }
+ answer.sort(Comparator.comparingInt(Suggestion::rank).reversed());
+ return answer.size() > MAX ? List.copyOf(answer.subList(0, MAX)) :
answer;
+ }
+
+ private static void suggest(
+ Route route, Map<String, ChaosModel.Recipe> recipes, Map<String,
String> consumers, List<Suggestion> answer) {
+ String id = route.getRouteId();
+ Endpoint from = route.getEndpoint();
+ boolean breaker = route.getRoute() instanceof RouteDefinition
definition
+ &&
!ProcessorDefinitionHelper.filterTypeInOutputs(definition.getOutputs(),
CircuitBreakerDefinition.class)
+ .isEmpty();
+ boolean atLeastOnce = from != null
+ &&
(AT_LEAST_ONCE.contains(ChaosPatterns.scheme(from.getEndpointUri())) ||
ChaosConsumers.keepsFailed(from));
+
+ // the sends: each endpoint once
+ Set<String> seen = new LinkedHashSet<>();
+ for (Processor processor : route.filter("*")) {
+ if (!(processor instanceof EndpointAware aware) ||
aware.getEndpoint() == null) {
+ continue;
+ }
+ Endpoint to = aware.getEndpoint();
+ String uri = uri(to);
+ if (!seen.add(uri)) {
+ continue;
+ }
+ String scheme = ChaosPatterns.scheme(uri);
+ if (HAND_OFF.contains(scheme) && consumers.containsKey(uri) &&
!id.equals(consumers.get(uri))) {
+ String next = consumers.get(uri);
+ answer.add(new Suggestion(
+ id, Kind.LOST, "route-error", "The route after a
hand-off fails",
+ "route=" + next + " error rate=0.2 until=2m expect=" +
id + "->" + next + ":lost<=0 within=3m",
+ "route " + id + " hands the message to route " + next
+ " over " + scheme + " and does not wait:"
+
+ " when "
+
+ next
+
+ " fails, is the message lost?",
+ 85));
+ continue;
+ }
+ if (!to.isRemote()) {
+ continue;
+ }
+ if (ChaosRuleParser.HTTP_SCHEMES.contains(scheme)) {
+ if (breaker) {
+ add(answer, recipes, "outage", id, Kind.ERRORS,
Map.of("endpoint", uri),
+ "a call to " + uri + " in a route with a circuit
breaker: when the service is down for a"
+
+ " while, does the breaker open, and does the fallback answer?",
+ 70);
+ } else {
+ add(answer, recipes, "http-error", id, Kind.ERRORS,
Map.of("endpoint", uri),
+ "an HTTP call to " + uri + " without a circuit
breaker: what does the route do when the"
+
+ " service answers 503?",
+ 90);
+ }
+ add(answer, recipes, "timeout", id, Kind.SLOW,
Map.of("endpoint", uri),
+ "a call to " + uri + " that may not answer in time:
does the route give up, and retry?", 65);
+ } else {
+ add(answer, recipes, "connection-refused", id, Kind.ERRORS,
Map.of("endpoint", uri),
+ "a call to " + uri + ": what does the route do when
the system is not there?", 75);
+ add(answer, recipes, "slow", id, Kind.SLOW, Map.of("endpoint",
uri),
+ "a call to " + uri + ": what does a slow answer do to
the route and its input?", 50);
+ }
+ if (atLeastOnce) {
+ add(answer, recipes, "duplicate", id, Kind.DUPLICATE,
Map.of("endpoint", uri),
+ "the input of route " + id + " can deliver a message
again: is the call to " + uri
+
+ " safe to make twice?",
+ 80);
+ }
+ }
+
+ // the dead letter channel of the route
+ if (route.getErrorHandlerFactory() instanceof
DeadLetterChannelDefinition dlc && dlc.getDeadLetterUri() != null) {
+ String dead = dlc.getDeadLetterUri().indexOf('?') > 0
+ ? dlc.getDeadLetterUri().substring(0,
dlc.getDeadLetterUri().indexOf('?')) : dlc.getDeadLetterUri();
+ add(answer, recipes, "dlq-unreachable", id, Kind.DEAD_LETTER,
Map.of("endpoint", dead),
+ "route " + id + " sends what fails to the dead letter
channel " + dead
+
+ ": when that fails too, is the message lost? Add it with
a rule that fails the route",
+ 78);
+ }
+
+ // the input of the route
+ if (from != null && from.isRemote()) {
+ if (route.getConsumer() instanceof ScheduledPollConsumer) {
+ add(answer, recipes, "poll-error", id, Kind.CONSUMER,
Map.of("endpoint", uri(from)),
+ "route " + id + " polls " + uri(from)
+
+ ": does it poll again after a failed poll?",
+ 55);
+ } else {
+ add(answer, recipes, "route-error", id, Kind.CONSUMER,
Map.of("route", id),
+ "route " + id + " is called from outside (" +
ChaosPatterns.scheme(from.getEndpointUri())
+
+ "): what does the sender see when it fails, a rollback, a retry"
+
+ " or an error?",
+ 60);
+ }
+ add(answer, recipes, "stall", id, Kind.LIFECYCLE, Map.of("route",
id),
+ "route " + id + " stops taking messages for a while: does
the sender wait, retry or lose them?", 30);
+ }
+ }
+
+ /**
+ * The URI of an endpoint as a rule targets it, without its query: the
options say nothing of which endpoint it is,
+ * and may hold a secret.
+ */
+ private static String uri(Endpoint endpoint) {
+ String uri = ChaosPatterns.endpointUri(endpoint);
+ int query = uri.indexOf('?');
+ return query > 0 ? uri.substring(0, query) : uri;
+ }
+
+ /** A suggestion from a known rule, with its placeholders filled in: the
given values, else their defaults. */
+ private static void add(
+ List<Suggestion> answer, Map<String, ChaosModel.Recipe> recipes,
String recipeId, String route, Kind kind,
+ Map<String, String> values, String why, int rank) {
+ ChaosModel.Recipe recipe = recipes.get(recipeId);
+ if (recipe == null) {
+ return;
+ }
+ String rule = recipe.rule();
+ for (ChaosModel.Placeholder p : recipe.placeholders()) {
+ String value = values.getOrDefault(p.name(), p.defaultValue());
+ if (value == null) {
+ // a placeholder without a value, such as the header to
remove: not a rule to add as it is
+ return;
+ }
+ rule = rule.replace("${" + p.name() + "}", value);
+ }
+ answer.add(new Suggestion(route, kind, recipe.id(), recipe.title(),
rule, why, rank));
+ }
+}
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 44834b350280..e258ff01ddd4 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
@@ -461,6 +461,33 @@ public final class ChaosManager {
}
}
+ /**
+ * What to try (CAMEL-26727): rules suggested per route from what the
routes do, the most worth trying first, each
+ * with the known rule it comes from and why, such as an HTTP call without
a circuit breaker, a dead letter channel,
+ * or a hand-off to another route over seda.
+ *
+ * @return {@code suggestions}: a list of {@code route}, {@code kind},
{@code recipe}, {@code title}, {@code rule}
+ * and {@code why}
+ */
+ public Map<String, Object> suggest() {
+ JsonArray list = new JsonArray();
+ for (ChaosAdvisor.Suggestion s : ChaosAdvisor.suggest(camelContext)) {
+ JsonObject jo = new JsonObject();
+ jo.put("route", s.route());
+ jo.put("kind", s.kind().key);
+ if (s.recipe() != null) {
+ jo.put("recipe", s.recipe());
+ }
+ jo.put("title", s.title());
+ jo.put("rule", s.rule());
+ jo.put("why", s.why());
+ list.add(jo);
+ }
+ JsonObject answer = new JsonObject();
+ answer.put("suggestions", list);
+ return answer;
+ }
+
/**
* Removes a rule. It stays in the report, and its expectation, if not
decided yet, is cancelled.
*
diff --git
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosAdvisorTest.java
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosAdvisorTest.java
new file mode 100644
index 000000000000..9939da651eb3
--- /dev/null
+++
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosAdvisorTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-26727: what to try, the rules suggested per route from what the
routes do.
+ */
+class ChaosAdvisorTest extends ChaosTestSupport {
+
+ @BindToRegistry("http")
+ private final TestHttpComponent http = new TestHttpComponent();
+
+ @BindToRegistry("testpoll")
+ private final TestPollComponent poll = new TestPollComponent();
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:orders").routeId("orders")
+ .to("http://stock/check?throwExceptionOnFailure=true")
+ .to("seda:shipping");
+ from("seda:shipping?pollTimeout=10").routeId("shipping")
+ .errorHandler(deadLetterChannel("seda:dlq"))
+ .to("mock:ship");
+ from("seda:dlq?pollTimeout=10").routeId("dlq").to("mock:dead");
+ from("direct:pay").routeId("pay")
+ .circuitBreaker()
+ .to("http://pay/charge")
+ .onFallback().setBody(constant("later"))
+ .end();
+
from("testpoll:inbox?delay=100000&initialDelay=100000").routeId("inbox").to("mock:inbox");
+ }
+ };
+ }
+
+ private static Map<String, String> rules(List<ChaosAdvisor.Suggestion>
suggestions) {
+ Map<String, String> answer = new LinkedHashMap<>();
+ suggestions.forEach(s -> answer.put(s.route() + " " + s.recipe() + " "
+ s.kind().key, s.rule()));
+ return answer;
+ }
+
+ @Test
+ void whatToTryPerRouteFromWhatTheRoutesDo() {
+ List<ChaosAdvisor.Suggestion> suggestions =
ChaosAdvisor.suggest(context);
+ Map<String, String> rules = rules(suggestions);
+
+ assertThat(rules)
+ .containsEntry("orders http-error errors",
"endpoint=http://stock/check error status=503 rate=0.25 until=10m")
+ .containsEntry("orders timeout slow",
+ "endpoint=http://stock/check error after=5s
exception=java.net.SocketTimeoutException rate=0.1 until=10m")
+ // a hand-off over seda: the next route fails, and nothing may
be lost between the two
+ .containsEntry("orders route-error lost",
+ "route=shipping error rate=0.2 until=2m
expect=orders->shipping:lost<=0 within=3m")
+ .containsEntry("shipping dlq-unreachable dead letter channel",
"endpoint=seda:dlq error at=dlq")
+ // a circuit breaker: the service is down for a while
+ .containsEntry("pay outage errors",
"endpoint=http://pay/charge error rate=burst:10s/1m until=10m")
+ .containsEntry("inbox poll-error consumer",
"endpoint=testpoll://inbox error at=poll rate=0.5 until=10m")
+ .containsEntry("inbox stall lifecycle", "route=inbox stall
duration=20s");
+ // no HTTP error without a breaker for the call inside the breaker,
and the query is left out
+ assertThat(rules).doesNotContainKey("pay http-error errors");
+ assertThat(rules.values()).noneMatch(r ->
r.contains("throwExceptionOnFailure"));
+ // an internal input (direct) is not a consumer to fail
+ assertThat(rules).doesNotContainKey("orders route-error consumer");
+
+ // the most worth trying first
+ assertThat(suggestions.get(0).recipe()).isEqualTo("http-error");
+ assertThat(suggestions.get(0).why()).contains("without a circuit
breaker");
+ }
+
+ @Test
+ void everySuggestedRuleIsValid() {
+ for (ChaosAdvisor.Suggestion s : ChaosAdvisor.suggest(context)) {
+ String id = chaos().addRule(s.rule());
+ chaos().removeRule(id);
+ }
+ }
+
+ @Test
+ void theManagerGivesThemAsJson() {
+ JsonArray list = (JsonArray) chaos().suggest().get("suggestions");
+ JsonObject first = (JsonObject) list.get(0);
+ assertThat(first.getString("route")).isEqualTo("orders");
+ assertThat(first.getString("kind")).isEqualTo("errors");
+ assertThat(first.getString("recipe")).isEqualTo("http-error");
+ assertThat(first.getString("title")).isEqualTo("Returns an error (HTTP
status)");
+
assertThat(first.getString("rule")).startsWith("endpoint=http://stock/check
error status=503");
+ assertThat(first.getString("why")).isNotBlank();
+ }
+}
diff --git a/design/chaos.adoc b/design/chaos.adoc
index c49fb05054ae..e8640c4ec29d 100644
--- a/design/chaos.adoc
+++ b/design/chaos.adoc
@@ -397,6 +397,10 @@ framework, so camel-test does not depend on camel-chaos;
the rules come from the
- A `chaos` dev console lists the rules and shows the report, so it is
`@DevConsole(readOnly = true)`. It does not add,
change or remove: the flag is metadata only, and `/q/dev/*`
(`ManagementHttpServer`) takes GET and POST with the
options in the query string, without checking it. So over HTTP chaos only
lists and reports.
+- What to try (stage 2, CAMEL-26727): `ChaosManager.suggest()` and `camel cmd
chaos-suggest` suggest rules per route
+ from the running routes (their sends, circuit breakers, dead letter channel,
input and hand-offs), each a recipe of
+ the chaos model with its target filled in and why. The About screen hints
(CAMEL-26716) are meant to be one shared
+ rule set; these facts come from the running context, and can become the
chaos part of that set.
- A `chaos` action of the CLI connector (`LocalCliConnector`) adds, changes
and removes rules through the
`ChaosManager`. The CLI has the flat verb-noun shape of every `camel cmd`
today (`stop-route`, `stub`), with the
reads under `camel get` (as `camel get circuit-breaker`): `camel cmd
chaos-add`, `chaos-change`, `chaos-remove`,
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
index f7f162c01096..4a22dbc61675 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
@@ -905,6 +905,9 @@ only integration that runs, or the only one with chaos
switched on, and otherwis
integrations. `chaos-add` prints the rule in plain words too (`fail 30% of the
sends to kafka:orders*, for 1 minute`),
and with `--dry-run` it adds nothing, and prints what the rule would hit now:
the sends, polls, routes or steps, with
their route.
+`camel cmd chaos-suggest` suggests rules to try per route from what the routes
do (an HTTP call without a circuit
+breaker, a dead letter channel, a hand-off over seda, an input from outside),
the most worth trying first, each with
+why and the `chaos-add` command that adds it.
`camel get chaos` lists the current rules with their id, version, state,
matches and the faults that fired, and
`--report` shows the report: the verdict, the seed, every version of every
rule and the counts per route. Chaos is for
development only: it stays off with profile `prod`, and the commands cannot
switch it on.
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
index 18ac3e5c79c4..76ddebd9f680 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
@@ -328,6 +328,9 @@ values start with a default, and the rule is shown as it is
built. The last entr
(`endpoint=kafka:orders* error rate=0.1 until=5m`).
+
image::jbang/camel-tui-chaos-recipes.png[Adding a chaos rule from the known
rules: the HTTP endpoint of the route is picked]
+* *w* shows what to try: the integration suggests rules per route from what
its routes do, the most worth trying
+first, each with why (see the camel-chaos documentation, "What to try"). Enter
types the selected rule in, to change
+or add.
* *e* changes the selected rule, starting from the rule as it is.
* *!* on the Endpoint tab, the Consumers tab or a node of the Diagram opens
the known rules for what is selected, with
it filled in: for a `to` or an endpoint the routes send to, the faults of that
endpoint (an HTTP status only for an
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-suggest.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-suggest.adoc
new file mode 100644
index 000000000000..10589a3cd196
--- /dev/null
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-suggest.adoc
@@ -0,0 +1,26 @@
+
+// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
+= camel cmd chaos-suggest
+
+Suggest chaos rules to try in a running integration, per route (camel-chaos,
for development only)
+
+
+== Usage
+
+[source,bash]
+----
+camel cmd chaos-suggest [options]
+----
+
+
+
+== Options
+
+[cols="2,5,1,2",options="header"]
+|===
+| Option | Description | Default | Type
+| `--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 e4902c686875..f6a8cd0288cb 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
@@ -24,6 +24,7 @@ camel cmd [options]
| 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-load.adoc[chaos-load] | Load a
chaos scenario into a running integration (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)
+| xref:jbang-commands/camel-jbang-cmd-chaos-suggest.adoc[chaos-suggest] |
Suggest chaos rules to try in a running integration, per route (camel-chaos,
for development only)
|
xref:jbang-commands/camel-jbang-cmd-disable-processor.adoc[disable-processor] |
Disable Camel processor
| xref:jbang-commands/camel-jbang-cmd-enable-processor.adoc[enable-processor]
| Enable Camel processor
| xref:jbang-commands/camel-jbang-cmd-gc.adoc[gc] | Trigger Java Memory
Garbage Collector
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 78c2dca86370..07e1fc7fcb2b 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
@@ -82,6 +82,12 @@ final class ChaosAction {
answer.put("version", 1);
descriptions(type, chaos, ids, answer);
warnings(type, chaos, ids, answer);
+ } else if ("suggest".equals(command)) {
+ // what to try: rules suggested per route from what the routes
do (read only)
+ Method suggest = type.getMethod("suggest");
+ @SuppressWarnings("unchecked")
+ Map<String, Object> suggested = (Map<String, Object>)
suggest.invoke(chaos);
+ answer.putAll(suggested);
} else if ("load".equals(command)) {
String text = root.getString("text");
if (isBlank(text)) {
@@ -136,7 +142,7 @@ final class ChaosAction {
answer.put("reset", true);
}
} else {
- output.error("Unknown chaos command: " + command + " (add,
check, change, remove, clear or load)");
+ output.error("Unknown chaos command: " + command + " (add,
check, change, remove, clear, load or suggest)");
return;
}
} catch (IllegalArgumentException | IllegalStateException e) {
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 f2d09f45975c..5e5c827e7a8b 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
@@ -211,6 +211,17 @@ class LocalCliConnectorChaosActionTest {
assertThat(reportRules()).hasSize(2);
}
+ @Test
+ void suggestWhatToTry() throws Exception {
+ start(null, Map.of("camel.chaos.enabled", "true"));
+
+ CliActionOutcome suggested = chaos("suggest", null, null);
+ assertThat(suggested.error()).isNull();
+ // the route of the test only calls local endpoints: nothing to
suggest, and nothing was added
+ assertThat((JsonArray)
suggested.result().get("suggestions")).isNotNull();
+ assertThat(reportRules()).isEmpty();
+ }
+
@Test
void rulesAreAddedTogetherWithWarningsForTheOnesThatMatchNothing() 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 172347fb8b8b..ce4287ac149b 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/CamelJBangMain.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
index 48391d19f438..fb056fd2da4e 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
@@ -264,6 +264,7 @@ public class CamelJBangMain implements Callable<Integer> {
.addSubcommand("chaos-clear", new CommandLine(new
CamelChaosClearAction(this)))
.addSubcommand("chaos-load", new CommandLine(new
CamelChaosLoadAction(this)))
.addSubcommand("chaos-remove", new CommandLine(new
CamelChaosRemoveAction(this)))
+ .addSubcommand("chaos-suggest", new CommandLine(new
CamelChaosSuggestAction(this)))
.addSubcommand("disable-processor", new CommandLine(new
CamelProcessorDisableAction(this)))
.addSubcommand("enable-processor", new CommandLine(new
CamelProcessorEnableAction(this)))
.addSubcommand("gc", new CommandLine(new
CamelGCAction(this)))
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosSuggestAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosSuggestAction.java
new file mode 100644
index 000000000000..f8378239071a
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosSuggestAction.java
@@ -0,0 +1,79 @@
+/*
+ * 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.dsl.jbang.core.commands.action;
+
+import java.util.List;
+
+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;
+
[email protected](name = "chaos-suggest",
+ description = "Suggest chaos rules to try in a running
integration, per route (camel-chaos, for development only)",
+ sortOptions = false, showDefaultValues = true,
+ footer = {
+ "%nThe rules come from what the routes do: an
HTTP call without a circuit breaker",
+ "(the service answers with an error, or does not
answer in time), a circuit",
+ "breaker (the service is down for a while), a
call that the input may repeat",
+ "(Kafka, JMS: the call is made twice), a dead
letter channel (it fails too), a",
+ "hand-off to another route over seda (the next
route fails, and no message may",
+ "be lost between the two), an input from outside
(the route fails at its entry,",
+ "the polls fail, the route stalls). The most
worth trying come first, each with",
+ "why and the command that adds it. Nothing is
added.",
+ "%nWithout a name or pid, the only integration
that runs, or the only one with",
+ "chaos switched on.",
+ "%nExamples:",
+ " camel cmd chaos-suggest",
+ " camel cmd chaos-suggest myApp" })
+public class CamelChaosSuggestAction extends CamelChaosAction {
+
+ @CommandLine.Parameters(description = "Name or pid of the running Camel
integration (must match one); without it,"
+ + " the only one running, or the
only one with chaos switched on",
+ arity = "0..1")
+ String name;
+
+ public CamelChaosSuggestAction(CamelJBangMain main) {
+ super(main);
+ }
+
+ @Override
+ public Integer doCall() throws Exception {
+ CamelProcess process = findChaosTarget(name);
+ printer().println(describe(chaos(process, "suggest", null)));
+ return 0;
+ }
+
+ @Override
+ protected String describe(JsonObject answer) {
+ List<Object> suggestions = answer.getCollection("suggestions");
+ if (suggestions == null || suggestions.isEmpty()) {
+ return "Nothing to suggest: the routes call nothing remote, and
have no input from outside, dead letter"
+ + " channel or hand-off to another route";
+ }
+ StringBuilder sb = new StringBuilder("Chaos rules to try, the most
worth trying first:");
+ int n = 0;
+ for (Object o : suggestions) {
+ JsonObject s = (JsonObject) o;
+ sb.append("\n\n").append(++n).append(". route
").append(s.getString("route")).append(": ")
+ .append(s.getString("title")).append("
(").append(s.getString("kind")).append(')');
+ sb.append("\n ").append(s.getString("why"));
+ sb.append("\n camel cmd chaos-add
--rule='").append(s.getString("rule")).append('\'');
+ }
+ return sb.toString();
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
index b5e9d6af07ec..612dbeadb814 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
@@ -36,8 +36,8 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
- * camel cmd chaos-add, chaos-change, chaos-remove, chaos-clear and
chaos-load: the chaos action they send, what they
- * print, and that a refused rule is an error with the reason of the
integration.
+ * camel cmd chaos-add, chaos-change, chaos-remove, chaos-clear, chaos-load
and chaos-suggest: the chaos action they
+ * send, what they print, and that a refused rule is an error with the reason
of the integration.
*/
@ExtendWith(MockitoExtension.class)
class CamelChaosActionTest extends ActionCommandTestSupport {
@@ -99,6 +99,33 @@ class CamelChaosActionTest extends ActionCommandTestSupport {
.hasMessageStartingWith("Chaos is off in this integration");
}
+ @Test
+ void suggestPrintsWhatToTryWithTheCommandThatAddsIt() throws Exception {
+ writeStatusFile(TEST_PID, "orders");
+ JsonObject suggestion = new JsonObject(
+ Map.of("route", "orders", "kind", "errors", "recipe",
"http-error",
+ "title", "Returns an error (HTTP status)",
+ "rule", "endpoint=http://stock/check error status=503
rate=0.25 until=10m",
+ "why", "an HTTP call to http://stock/check without a
circuit breaker: what does the route do when the"
+ + " service answers 503?"));
+ JsonObject answer = new JsonObject(Map.of("suggestions", new
JsonArray(List.of(suggestion))));
+
+ assertThat(callWithResponse(command(new
CamelChaosSuggestAction(main()), "orders"), answer)).isZero();
+
+
assertThat(readActionFile(TEST_PID).getString("command")).isEqualTo("suggest");
+ assertThat(printer.getLines()).contains("Chaos rules to try, the most
worth trying first:",
+ "1. route orders: Returns an error (HTTP status) (errors)",
+ "camel cmd chaos-add --rule='endpoint=http://stock/check error
status=503 rate=0.25 until=10m'");
+ }
+
+ @Test
+ void suggestSaysWhenThereIsNothingToTry() throws Exception {
+ writeStatusFile(TEST_PID, "orders");
+ assertThat(callWithResponse(command(new
CamelChaosSuggestAction(main()), "orders"),
+ new JsonObject(Map.of("suggestions", new
JsonArray())))).isZero();
+ assertThat(printer.getOutput()).startsWith("Nothing to suggest");
+ }
+
@Test
void loadSendsTheScenario(@TempDir Path dir) throws Exception {
writeStatusFile(TEST_PID, "orders");
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 77e2357f62b9..51a4c91f3dc1 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
@@ -562,6 +562,8 @@ Commands:
(camel-chaos, for development only)
chaos-remove Remove a chaos rule of a running integration (camel-chaos,
for development only)
+ chaos-suggest Suggest chaos rules to try in a running integration, per
+ route (camel-chaos, for development only)
disable-processor Disable Camel processor
enable-processor Enable Camel processor
gc Trigger Java Memory Garbage Collector
@@ -790,6 +792,34 @@ Examples:
camel cmd chaos-remove --id=r1
camel cmd chaos-remove myApp --id=r1
+== camel cmd chaos-suggest --help
+Usage: camel cmd chaos-suggest [-h] [--timeout=<timeout>] [<name>]
+Suggest chaos rules to try in a running integration, per route (camel-chaos,
+for development only)
+ [<name>] Name or pid of the running Camel integration (must
+ match one); without it, the only one running, or
+ the only one with chaos switched on
+ -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
+
+The rules come from what the routes do: an HTTP call without a circuit breaker
+(the service answers with an error, or does not answer in time), a circuit
+breaker (the service is down for a while), a call that the input may repeat
+(Kafka, JMS: the call is made twice), a dead letter channel (it fails too), a
+hand-off to another route over seda (the next route fails, and no message may
+be lost between the two), an input from outside (the route fails at its entry,
+the polls fail, the route stalls). The most worth trying come first, each with
+why and the command that adds it. Nothing is added.
+
+Without a name or pid, the only integration that runs, or the only one with
+chaos switched on.
+
+Examples:
+ camel cmd chaos-suggest
+ camel cmd chaos-suggest myApp
+
== camel cmd disable-processor --help
Usage: camel cmd disable-processor [-h] [--id=<id>] [--timeout=<timeout>]
[<name>]
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-parse.txt
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-parse.txt
index eb2dc03c90b6..dbae4927d33f 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-parse.txt
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-parse.txt
@@ -567,6 +567,13 @@
timeoutOption.timeout=10000
> cmd chaos-remove --id v --timeout 7
timeoutOption.timeout=7
+> cmd chaos-suggest
+ cmd:helpRequested=false
+ helpRequested=false
+ name=null
+ timeoutOption.timeout=10000
+> cmd chaos-suggest --timeout 7
+ timeoutOption.timeout=7
> cmd disable-processor
cmd:helpRequested=false
helpRequested=false
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
index 0b4e76b4bcf4..e951863834b7 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
@@ -59,6 +59,8 @@ final class ChaosControl {
NONE,
// the known rules to pick from, and the form of the one picked
RECIPES,
+ // what to try, the rules the integration suggests (null while it is
asked)
+ SUGGEST,
INPUT,
CONFIRM,
RESULT
@@ -86,6 +88,7 @@ final class ChaosControl {
private ChaosRecipes recipes;
private boolean recipesLoaded;
private ChaosRecipeForm form;
+ private volatile ChaosSuggestList suggestions;
// set by the background task that sends the action, read when the screen
is drawn
private volatile Step step = Step.NONE;
private Command command;
@@ -127,7 +130,7 @@ final class ChaosControl {
boolean isTextInputActive() {
// the list of known rules takes the keys too, so a digit or a letter
does not leave the screen
- return step == Step.INPUT || step == Step.RECIPES;
+ return step == Step.INPUT || step == Step.RECIPES || step ==
Step.SUGGEST;
}
boolean isConfirming() {
@@ -150,6 +153,9 @@ final class ChaosControl {
case RECIPES -> {
return handleRecipeKey(ke);
}
+ case SUGGEST -> {
+ return handleSuggestKey(ke);
+ }
case INPUT -> {
return handleInputKey(ke);
}
@@ -215,6 +221,11 @@ final class ChaosControl {
ask(Command.RESET);
return true;
}
+ if (ke.isChar('w')) {
+ // what to try: the rules the integration suggests from what its
routes do (CAMEL-26727)
+ askWhatToTry();
+ return true;
+ }
if (ke.isChar('s') && !info.chaos.rules.isEmpty()) {
// the rules as a scenario of the project, for chaos-load and
camel run --chaos=<file>
describing = info;
@@ -252,6 +263,61 @@ final class ChaosControl {
}
}
+ /** Asks the integration what to try, in the background; the list shows
when it answers. */
+ private void askWhatToTry() {
+ suggestions = null;
+ step = Step.SUGGEST;
+ JsonObject request = new JsonObject();
+ request.put("action", "chaos");
+ request.put("command", "suggest");
+ String target = pid;
+ String of = name;
+ Runnable task = () -> {
+ JsonObject reply = sender != null ? sender.apply(request) :
ctx.executeAction(target, request, 5000);
+ if (step != Step.SUGGEST) {
+ // closed while it was asked
+ return;
+ }
+ if (reply == null || reply.getString("error") != null) {
+ step = Step.NONE;
+ resultLines.clear();
+ rule = null;
+ command = Command.ADD;
+ result("What to try", true, reply == null
+ ? "The integration " + of + " did not answer"
+ : reply.getString("error"));
+ return;
+ }
+ suggestions = new ChaosSuggestList(of, reply);
+ };
+ if (sender != null) {
+ task.run();
+ } else {
+ ctx.backgroundExecutor.execute(task);
+ }
+ }
+
+ private boolean handleSuggestKey(KeyEvent ke) {
+ ChaosSuggestList list = suggestions;
+ if (list == null) {
+ if (ke.isCancel()) {
+ close();
+ }
+ return true;
+ }
+ switch (list.handleKey(ke)) {
+ case CLOSED -> close();
+ case PICKED -> {
+ id = null;
+ openInput(Command.ADD, list.selected().rule());
+ }
+ default -> {
+ // the list used the key
+ }
+ }
+ return true;
+ }
+
/** Opens a rule typed as one line, to change before Enter: a rule that
makes a real error again (CAMEL-26723). */
void openRule(IntegrationInfo info, String text) {
if (!ChaosHere.available(info)) {
@@ -391,6 +457,10 @@ final class ChaosControl {
// the form goes back to the list, the list closes
return handleRecipeKey(KeyEvent.ofKey(KeyCode.ESCAPE));
}
+ if (step == Step.SUGGEST) {
+ close();
+ return true;
+ }
close();
return true;
}
@@ -399,6 +469,7 @@ final class ChaosControl {
step = Step.NONE;
input = null;
form = null;
+ suggestions = null;
describing = null;
}
@@ -521,6 +592,15 @@ final class ChaosControl {
void render(Frame frame, Rect area) {
switch (step) {
case RECIPES -> form.render(frame, area);
+ case SUGGEST -> {
+ ChaosSuggestList list = suggestions;
+ if (list != null) {
+ list.render(frame, area);
+ } else {
+ DialogHelper.renderConfirm(frame, area, "What to Try",
"Asking " + name + " what to try...", null,
+ "Esc", "cancel", null, null);
+ }
+ }
case INPUT -> {
if (command == Command.DESCRIBE) {
DialogHelper.renderInputDialog(frame, area, "Describe the
chaos you want in " + name, input,
@@ -611,6 +691,14 @@ final class ChaosControl {
void renderFooter(List<Span> spans, boolean hasRules) {
switch (step) {
case RECIPES -> form.renderFooter(spans);
+ case SUGGEST -> {
+ ChaosSuggestList list = suggestions;
+ if (list != null) {
+ list.renderFooter(spans);
+ } else {
+ TuiHelper.hint(spans, "Esc", "cancel");
+ }
+ }
case INPUT -> {
TuiHelper.hint(spans, "Enter",
command == Command.DESCRIBE ? "ask the AI" : command
== Command.SAVE ? "save" : "confirm");
@@ -624,6 +712,7 @@ final class ChaosControl {
default -> {
TuiHelper.hint(spans, "Esc", "back");
TuiHelper.hint(spans, "a", "add");
+ TuiHelper.hint(spans, "w", "what to try");
if (hasRules) {
TuiHelper.hint(spans, "e", "change");
TuiHelper.hint(spans, "x", "remove");
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosSuggestList.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosSuggestList.java
new file mode 100644
index 000000000000..8915cff7450b
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosSuggestList.java
@@ -0,0 +1,163 @@
+/*
+ * 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.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Overflow;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * What to try ({@code w} on the Chaos screen, CAMEL-26727): the rules the
integration suggests per route from what its
+ * routes do, the most worth trying first, each with why. Enter types the
selected rule in, to change or add.
+ */
+final class ChaosSuggestList {
+
+ /** What a key did to the list. */
+ enum Outcome {
+ NONE,
+ CLOSED,
+ PICKED
+ }
+
+ /** A suggestion, as the chaos action of the integration gives it. */
+ record Item(String route, String kind, String title, String rule, String
why) {
+ }
+
+ private final String name;
+ private final List<Item> items = new ArrayList<>();
+ private int selected;
+
+ ChaosSuggestList(String name, JsonObject reply) {
+ this.name = name;
+ if (reply != null && reply.getCollection("suggestions") != null) {
+ for (Object o : reply.getCollection("suggestions")) {
+ if (o instanceof JsonObject jo && jo.getString("rule") !=
null) {
+ items.add(new Item(
+ jo.getString("route"), jo.getString("kind"),
jo.getString("title"),
+ jo.getString("rule"), jo.getString("why")));
+ }
+ }
+ }
+ }
+
+ List<Item> items() {
+ return items;
+ }
+
+ /** The selected suggestion, or null when there are none. */
+ Item selected() {
+ return items.isEmpty() ? null : items.get(selected);
+ }
+
+ Outcome handleKey(KeyEvent ke) {
+ if (ke.isCancel()) {
+ return Outcome.CLOSED;
+ }
+ if (ke.isUp() && selected > 0) {
+ selected--;
+ } else if (ke.isDown() && selected < items.size() - 1) {
+ selected++;
+ } else if (ke.isConfirm() && !items.isEmpty()) {
+ return Outcome.PICKED;
+ }
+ return Outcome.NONE;
+ }
+
+ void render(Frame frame, Rect area) {
+ int width = Math.min(area.width() - 4, 150);
+ int height = Math.min(area.height() - 2, 30);
+ Rect popup = DialogHelper.centered(area, width, height);
+ frame.renderWidget(Clear.INSTANCE, popup);
+ Block outer =
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(Theme.warning())
+ .title(" What to try in " + name + " ").build();
+ frame.renderWidget(outer, popup);
+ Rect inner = outer.inner(popup);
+ if (items.isEmpty()) {
+
frame.renderWidget(Paragraph.builder().overflow(Overflow.WRAP_WORD).text(Text.from(List.of(Line.empty(),
+ Line.from(Span.styled(" Nothing to suggest: the routes
call nothing remote, and have no input from"
+ + " outside, dead letter channel or
hand-off to another route",
+ Theme.muted())))))
+ .build(),
+ inner);
+ return;
+ }
+ List<Rect> cols = Layout.horizontal()
+ .constraints(Constraint.length(Math.min(56, inner.width() /
2)), Constraint.fill())
+ .split(inner);
+ renderList(frame, cols.get(0));
+ renderDetail(frame, cols.get(1));
+ }
+
+ private void renderList(Frame frame, Rect area) {
+ List<Line> lines = new ArrayList<>();
+ lines.add(Line.from(Span.styled(" The most worth trying first",
Style.EMPTY.bold())));
+ int selectedLine = 0;
+ for (int i = 0; i < items.size(); i++) {
+ Item item = items.get(i);
+ boolean sel = i == selected;
+ if (sel) {
+ selectedLine = lines.size();
+ }
+ Style style = sel ? Theme.selectionBg().bold() : Style.EMPTY;
+ List<Span> spans = new ArrayList<>();
+ spans.add(Span.styled((sel ? " > " : " ") + item.title(),
style));
+ spans.add(Span.styled(" " + item.route(), Theme.muted()));
+ lines.add(Line.from(spans));
+ }
+ // keep the selected one in sight
+ int scroll = Math.max(0, selectedLine - (area.height() - 2));
+
frame.renderWidget(Paragraph.builder().text(Text.from(lines)).scroll(scroll)
+
.block(Block.builder().borders(EnumSet.of(Borders.RIGHT)).borderStyle(Theme.border()).build()).build(),
area);
+ }
+
+ private void renderDetail(Frame frame, Rect area) {
+ Item item = selected();
+ List<Line> lines = new ArrayList<>();
+ lines.add(Line.from(Span.styled(" " + item.title(),
Style.EMPTY.bold()),
+ Span.styled(" route " + item.route() + " · " + item.kind(),
Theme.muted())));
+ lines.add(Line.empty());
+ lines.add(Line.from(Span.raw(" " + item.why())));
+ lines.add(Line.empty());
+ lines.add(Line.from(Span.styled(" " + item.rule(), Theme.warning())));
+
frame.renderWidget(Paragraph.builder().overflow(Overflow.WRAP_WORD).text(Text.from(lines)).build(),
area);
+ }
+
+ void renderFooter(List<Span> spans) {
+ if (!items.isEmpty()) {
+ TuiHelper.hint(spans, "↑↓", "choose");
+ TuiHelper.hint(spans, "Enter", "type it in");
+ }
+ TuiHelper.hint(spans, "Esc", "close");
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md
index 7e7be29d76be..3ee695aad9a6 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md
@@ -73,6 +73,11 @@ error handler (HAND) and the redeliveries (REDEL).
the rule is shown as it is built. The last entry, "Write the rule
yourself", types it as one line, such as
`endpoint=kafka:orders* error rate=0.1 until=5m`
+- **w** — What to try: the integration suggests rules per route from what
+ its routes do (an HTTP call without a circuit breaker, a dead letter
+ channel, a hand-off over seda, an input from outside), the most worth
+ trying first, each with why. Enter types the selected rule in, to change
+ it or add it after you confirm
- **e** — Change the selected rule: the line starts with the rule as it is
- **x** — Remove the selected rule
- **c** — Clear: remove all rules (the report keeps what they did)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControlTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControlTest.java
index dbca4cfb092a..6f6daeacbc72 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControlTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControlTest.java
@@ -225,6 +225,41 @@ class ChaosControlTest {
assertThat(footer.stream().map(Span::content).reduce("",
String::concat)).doesNotContain("add");
}
+ @Test
+ void whatToTryListsTheSuggestionsAndTypesThePickedOneIn() throws Exception
{
+ reply = (JsonObject) Jsoner.deserialize("""
+ {"suggestions": [
+ {"route": "orders", "kind": "errors", "recipe":
"http-error", "title": "Returns an error (HTTP status)",
+ "rule": "endpoint=http://stock/check error status=503
rate=0.25 until=10m",
+ "why": "an HTTP call to http://stock/check without a
circuit breaker"},
+ {"route": "shipping", "kind": "dead letter channel",
"recipe": "dlq-unreachable",
+ "title": "The dead letter channel is unreachable", "rule":
"endpoint=seda:dlq error at=dlq",
+ "why": "route shipping sends what fails to the dead letter
channel seda:dlq"}]}
+ """);
+ key('w');
+ assertThat(requests).singleElement().satisfies(r ->
assertThat(r.getString("command")).isEqualTo("suggest"));
+ String screen = TuiTestHelper.renderToString(tab, 160, 40);
+ assertThat(screen).contains("What to try in
chaosdemo").contains("Returns an error (HTTP status)")
+ .contains("without a circuit breaker").contains("The dead
letter channel is unreachable");
+
+ tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, KeyModifiers.NONE));
+ enter();
+ assertThat(tab.isTextInputActive()).isTrue();
+ assertThat(TuiTestHelper.renderToString(tab, 160, 40)).contains("Add a
chaos rule to chaosdemo")
+ .contains("endpoint=seda:dlq error at=dlq");
+ // only added after Enter and the confirmation
+ assertThat(requests).hasSize(1);
+ }
+
+ @Test
+ void whatToTryWithNothingToSuggest() throws Exception {
+ reply = (JsonObject) Jsoner.deserialize("{\"suggestions\": []}");
+ key('w');
+ assertThat(TuiTestHelper.renderToString(tab, 160,
40)).contains("Nothing to suggest");
+ tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.ESCAPE, KeyModifiers.NONE));
+ assertThat(tab.control().isOpen()).isFalse();
+ }
+
@Test
void saveTheRulesAsAScenarioOfTheProject(@TempDir Path dir) throws
Exception {
info.directory = dir.toString();
@@ -292,7 +327,8 @@ class ChaosControlTest {
List<Span> footer = new ArrayList<>();
tab.renderFooter(footer);
assertThat(footer.stream().map(Span::content).reduce("",
String::concat))
-
.contains("add").contains("change").contains("remove").contains("clear").contains("save
as scenario")
+ .contains("add").contains("what to
try").contains("change").contains("remove").contains("clear")
+ .contains("save as scenario")
.contains("new experiment");
key('x');
footer.clear();