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 18058a29443f CAMEL-26689: camel-chaos - a model of the chaos rules in
camel-catalog: targets, hooks, faults, parameters and known rules (#29331)
18058a29443f is described below
commit 18058a29443f5509fa0855efa1676b87553a8f19
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Nov 6 16:09:47 2026 +0100
CAMEL-26689: camel-chaos - a model of the chaos rules in camel-catalog:
targets, hooks, faults, parameters and known rules (#29331)
* CAMEL-26689: camel-chaos - a model of the chaos rules in camel-catalog:
targets, hooks, faults, parameters and known rules
* CAMEL-26689: camel-chaos - the build writes the chaos model
(generate-chaos-model goal of camel-package-maven-plugin)
* CAMEL-26689: camel-chaos - escape the rule placeholders in the docs (doc
validation)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../org/apache/camel/catalog/chaos-model.json | 1078 ++++++++++++++++++++
.../org/apache/camel/catalog/docs/chaos.adoc | 23 +
.../org/apache/camel/catalog/CamelCatalog.java | 10 +
.../apache/camel/catalog/DefaultCamelCatalog.java | 5 +
.../org/apache/camel/catalog/CamelCatalogTest.java | 11 +
components/camel-chaos/pom.xml | 15 +
.../org/apache/camel/chaos/chaos-model.json | 1078 ++++++++++++++++++++
components/camel-chaos/src/main/docs/chaos.adoc | 23 +
.../apache/camel/component/chaos/ChaosModel.java | 602 +++++++++++
.../camel/component/chaos/ChaosRuleParser.java | 61 +-
.../camel/component/chaos/ChaosModelTest.java | 188 ++++
.../modules/ROOT/pages/camel-catalog.adoc | 9 +
.../maven/packaging/GenerateChaosModelMojo.java | 76 ++
.../camel/maven/packaging/PrepareCatalogMojo.java | 17 +
14 files changed, 3179 insertions(+), 17 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/chaos-model.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/chaos-model.json
new file mode 100644
index 000000000000..7505fb834508
--- /dev/null
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/chaos-model.json
@@ -0,0 +1,1078 @@
+{
+ "chaos": {
+ "description": "The chaos rules of camel-chaos: a rule is one line, the
target, the fault and its parameters as name=value, then the options (such as
at, rate, from, until) and optionally an expectation",
+ "syntax": "<target>=<pattern> <fault> [<parameter>=<value> ...]
[<option>=<value> ...]",
+ "targets": [
+ {
+ "name": "route",
+ "description": "A route id, with * for any characters; one route id
(no pattern) for stall and lifecycle",
+ "pattern": true,
+ "pick": "routes"
+ },
+ {
+ "name": "endpoint",
+ "description": "An endpoint URI pattern, with * for any characters;
matched with or without the query parameters, and the \/\/ after the scheme
does not count (kafka:orders* matches kafka:\/\/orders?brokers=b1)",
+ "pattern": true,
+ "pick": "endpoints"
+ },
+ {
+ "name": "component",
+ "description": "The scheme of the endpoint URIs, with * for any
characters (no ':')",
+ "pattern": true,
+ "pick": "components"
+ },
+ {
+ "name": "step",
+ "description": "A step id (to1, or the id set with id(\"enrich\")),
with * for any characters",
+ "pattern": true,
+ "pick": "steps"
+ }
+ ],
+ "hooks": [
+ {
+ "name": "send",
+ "description": "The sends to a matching endpoint (to, toD, wireTap,
enrich, recipientList, a ProducerTemplate...), before the producer is called,
inside the error handler",
+ "targets": [
+ "endpoint",
+ "component"
+ ],
+ "faults": [
+ "error",
+ "delay",
+ "drop",
+ "corrupt"
+ ],
+ "onlyWithAt": false
+ },
+ {
+ "name": "reply",
+ "description": "The reply of such a send, after the producer
succeeded: its side effect happened (only with at=reply)",
+ "targets": [
+ "endpoint",
+ "component"
+ ],
+ "faults": [
+ "error",
+ "corrupt"
+ ],
+ "onlyWithAt": true
+ },
+ {
+ "name": "dlq",
+ "description": "The send of the dead letter channel to a matching
endpoint (only with at=dlq)",
+ "targets": [
+ "endpoint",
+ "component"
+ ],
+ "faults": [
+ "error",
+ "delay",
+ "drop",
+ "corrupt"
+ ],
+ "onlyWithAt": true
+ },
+ {
+ "name": "poll",
+ "description": "The polls of the scheduled poll consumers of the
routes",
+ "targets": [
+ "endpoint",
+ "component"
+ ],
+ "faults": [
+ "error",
+ "delay",
+ "drop"
+ ],
+ "onlyWithAt": false
+ },
+ {
+ "name": "entry",
+ "description": "Each exchange that enters a matching route, before the
first step",
+ "targets": [
+ "route"
+ ],
+ "faults": [
+ "corrupt"
+ ],
+ "onlyWithAt": false
+ },
+ {
+ "name": "step",
+ "description": "Each matching step of a route, right before its
processor is called",
+ "targets": [
+ "step"
+ ],
+ "faults": [
+ "error",
+ "delay"
+ ],
+ "onlyWithAt": false
+ }
+ ],
+ "faults": [
+ {
+ "name": "error",
+ "description": "Fails the send, the poll or the step with an exception
(a ChaosException without exception=), or the failure of an HTTP component for
status=",
+ "targets": [
+ "component",
+ "endpoint",
+ "step"
+ ],
+ "hooks": [
+ "send",
+ "reply",
+ "dlq",
+ "poll",
+ "step"
+ ],
+ "parameters": [
+ {
+ "name": "after",
+ "type": "duration",
+ "required": false,
+ "description": "Waits that long first, then fails, as a call that
times out; the producer is not called",
+ "example": "5s",
+ "notAt": [
+ "reply"
+ ]
+ },
+ {
+ "name": "body",
+ "type": "string",
+ "required": false,
+ "description": "The body of the HTTP response (needs status=)",
+ "example": "no stock"
+ },
+ {
+ "name": "exception",
+ "type": "class",
+ "required": false,
+ "description": "The class of the exception: a public Throwable
with a public constructor taking a String or none (not an
InterruptedException)",
+ "example": "java.net.ConnectException"
+ },
+ {
+ "name": "header.<name>",
+ "type": "string",
+ "required": false,
+ "description": "A header of the HTTP response, such as
header.Retry-After=5 (needs status=)",
+ "example": "5",
+ "prefix": "header."
+ },
+ {
+ "name": "message",
+ "type": "string",
+ "required": false,
+ "description": "The message of the exception (the class needs a
constructor taking a String)",
+ "example": "broker down"
+ },
+ {
+ "name": "status",
+ "type": "integer",
+ "required": false,
+ "description": "The failure of an HTTP component for this status
(not with exception= or message=)",
+ "example": "503",
+ "min": 300,
+ "max": 599,
+ "schemes": [
+ "http",
+ "https",
+ "netty-http",
+ "undertow",
+ "vertx-http"
+ ]
+ }
+ ],
+ "options": [
+ "at",
+ "expect",
+ "from",
+ "rate",
+ "until",
+ "within"
+ ]
+ },
+ {
+ "name": "delay",
+ "description": "Waits before the send, the poll or the step, as for a
slow endpoint",
+ "targets": [
+ "component",
+ "endpoint",
+ "step"
+ ],
+ "hooks": [
+ "send",
+ "dlq",
+ "poll",
+ "step"
+ ],
+ "parameters": [
+ {
+ "name": "duration",
+ "type": "duration",
+ "required": true,
+ "description": "How long to wait, or a range such as 100ms..2s for
a draw between the two times (from the seeded sequence of the rule)",
+ "example": "500ms",
+ "range": true
+ }
+ ],
+ "options": [
+ "at",
+ "expect",
+ "from",
+ "rate",
+ "until",
+ "within"
+ ]
+ },
+ {
+ "name": "drop",
+ "description": "A send that reports success without calling the
producer, or a poll that is skipped",
+ "targets": [
+ "component",
+ "endpoint"
+ ],
+ "hooks": [
+ "send",
+ "dlq",
+ "poll"
+ ],
+ "parameters": [
+
+ ],
+ "options": [
+ "at",
+ "expect",
+ "from",
+ "rate",
+ "until",
+ "within"
+ ]
+ },
+ {
+ "name": "corrupt",
+ "description": "Changes the message: its headers, the charset of its
body, or its body",
+ "targets": [
+ "component",
+ "endpoint",
+ "route"
+ ],
+ "hooks": [
+ "send",
+ "reply",
+ "dlq",
+ "entry"
+ ],
+ "parameters": [
+ {
+ "name": "header",
+ "type": "string",
+ "required": false,
+ "description": "The headers mode=remove-header removes: a name or
a pattern with *, ignoring case (without it every header that is not Camel*)",
+ "example": "X-Trace-*"
+ },
+ {
+ "name": "mode",
+ "type": "enum",
+ "required": true,
+ "description": "headers: the headers that are not Camel* get
random values; charset: a text body becomes UTF-16 bytes; truncate: a String,
byte[] or stream body ends after half of it; empty: the body becomes empty of
its kind (or null); remove-header: removes headers",
+ "example": "headers",
+ "enum": [
+ "headers",
+ "charset",
+ "truncate",
+ "empty",
+ "remove-header"
+ ]
+ }
+ ],
+ "options": [
+ "at",
+ "expect",
+ "from",
+ "rate",
+ "until",
+ "within"
+ ]
+ },
+ {
+ "name": "lifecycle",
+ "description": "Suspends, stops or removes the route of a route=<id>
target, and brings it back after the duration; repeat= takes it down more than
once",
+ "targets": [
+ "route"
+ ],
+ "hooks": [
+
+ ],
+ "parameters": [
+ {
+ "name": "action",
+ "type": "enum",
+ "required": true,
+ "description": "What to do to the route",
+ "example": "stop",
+ "enum": [
+ "suspend",
+ "stop",
+ "remove"
+ ]
+ },
+ {
+ "name": "duration",
+ "type": "duration",
+ "required": true,
+ "description": "How long the route stays down (at most
camel.chaos.maxDuration)",
+ "example": "20s"
+ },
+ {
+ "name": "every",
+ "type": "duration",
+ "required": false,
+ "description": "The time from one time the route goes down to the
next, longer than duration=",
+ "example": "1m"
+ },
+ {
+ "name": "repeat",
+ "type": "integer",
+ "required": false,
+ "description": "How many times to take the route down (needs
every= when more than 1)",
+ "example": "3",
+ "min": 1,
+ "max": 100
+ }
+ ],
+ "options": [
+ "expect",
+ "from",
+ "within"
+ ]
+ },
+ {
+ "name": "stall",
+ "description": "Short for lifecycle action=suspend: the route stalls
for the duration",
+ "targets": [
+ "route"
+ ],
+ "hooks": [
+
+ ],
+ "shortFor": "lifecycle action=suspend",
+ "parameters": [
+ {
+ "name": "duration",
+ "type": "duration",
+ "required": true,
+ "description": "How long the route stays down (at most
camel.chaos.maxDuration)",
+ "example": "20s"
+ },
+ {
+ "name": "every",
+ "type": "duration",
+ "required": false,
+ "description": "The time from one time the route goes down to the
next, longer than duration=",
+ "example": "1m"
+ },
+ {
+ "name": "repeat",
+ "type": "integer",
+ "required": false,
+ "description": "How many times to take the route down (needs
every= when more than 1)",
+ "example": "3",
+ "min": 1,
+ "max": 100
+ }
+ ],
+ "options": [
+ "expect",
+ "from",
+ "within"
+ ]
+ }
+ ],
+ "options": [
+ {
+ "name": "at",
+ "type": "enum",
+ "required": false,
+ "description": "Narrows the rule to one hook of its target (reply and
dlq are reached only with at=)",
+ "example": "send",
+ "enum": [
+ "send",
+ "reply",
+ "dlq",
+ "poll",
+ "entry",
+ "step"
+ ]
+ },
+ {
+ "name": "expect",
+ "type": "expectation",
+ "required": false,
+ "description": "An expectation of a route after the rule is added:
<route>:<count> (at least that many completed) or <route>:<kind><op><count>",
+ "example": "orders:handled<=0"
+ },
+ {
+ "name": "from",
+ "type": "duration",
+ "required": false,
+ "description": "When the rule starts after it is added, such as after
the warm-up (+30s or 30s)",
+ "example": "+30s"
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "required": false,
+ "description": "How often the rule fires: a probability above 0 and at
most 1, every Nth match, or bursts (every match during the first part of each
period); without it every match fires",
+ "example": "0.25",
+ "forms": [
+ "0.25",
+ "every:4",
+ "burst:5s\/30s"
+ ]
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "required": false,
+ "description": "When the rule ends: a number of matches or a time from
its start (at most camel.chaos.maxDuration, its default)",
+ "example": "10m"
+ },
+ {
+ "name": "within",
+ "type": "duration",
+ "required": false,
+ "description": "The time of the expectation (by default until the rule
ends)",
+ "example": "2m"
+ }
+ ],
+ "expectation": {
+ "kinds": [
+ {
+ "name": "completed",
+ "description": "The exchanges completed in the route"
+ },
+ {
+ "name": "handled",
+ "description": "The exchanges handled by the error handler in the
route"
+ },
+ {
+ "name": "failed",
+ "description": "The exchanges failed in the route"
+ },
+ {
+ "name": "redeliveries",
+ "description": "The redeliveries in the route"
+ }
+ ],
+ "ops": [
+ {
+ "name": ">=",
+ "description": "at least"
+ },
+ {
+ "name": "<=",
+ "description": "at most"
+ },
+ {
+ "name": "=",
+ "description": "exactly"
+ }
+ ]
+ },
+ "limits": {
+ "maxLength": 2000,
+ "maxRepeat": 100,
+ "maxDuration": "camel.chaos.maxDuration (default 10m): the longest
until, delay, after, from, burst period and stall or lifecycle duration",
+ "time": "a number with a unit (ms, s, m, h, d), also combined as 1m30s;
a number alone is milliseconds",
+ "quoting": "a value with spaces or ; is quoted with \" or ' (\\\", \\'
and \\\\ inside quotes)"
+ },
+ "recipes": [
+ {
+ "id": "http-error",
+ "group": "Downstream",
+ "title": "Returns an error (HTTP status)",
+ "description": "An HTTP call fails with a status, as for a service
that is down or overloaded: tests onException, redelivery and the dead letter
channel",
+ "rule": "endpoint=${endpoint} error status=${status} rate=${rate}
until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The HTTP endpoint the route calls",
+ "example": "https:\/\/inventory\/stock",
+ "pick": {
+ "from": "producers",
+ "schemes": [
+ "http",
+ "https",
+ "netty-http",
+ "undertow",
+ "vertx-http"
+ ]
+ }
+ },
+ {
+ "name": "status",
+ "type": "integer",
+ "description": "The HTTP status",
+ "default": "503",
+ "example": "503"
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.25",
+ "example": "0.25"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "connection-refused",
+ "group": "Downstream",
+ "title": "Refuses the connection",
+ "description": "The send fails as when the other side is not there",
+ "rule": "endpoint=${endpoint} error
exception=java.net.ConnectException rate=${rate} until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint the route sends to",
+ "example": "kafka:orders",
+ "pick": {
+ "from": "producers"
+ }
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.25",
+ "example": "0.25"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "slow",
+ "group": "Downstream",
+ "title": "Is slow",
+ "description": "Each send waits a different time within a range, as a
slow service does",
+ "rule": "endpoint=${endpoint} delay duration=${duration} rate=${rate}
until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint the route sends to",
+ "example": "kafka:orders",
+ "pick": {
+ "from": "producers"
+ }
+ },
+ {
+ "name": "duration",
+ "type": "duration",
+ "description": "The time, or a range of times",
+ "default": "200ms..3s",
+ "example": "200ms..3s"
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.5",
+ "example": "0.5"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "timeout",
+ "group": "Downstream",
+ "title": "Times out",
+ "description": "The send waits, then fails without calling the
producer, as a call that times out",
+ "rule": "endpoint=${endpoint} error after=${after}
exception=java.net.SocketTimeoutException rate=${rate} until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint the route sends to",
+ "example": "kafka:orders",
+ "pick": {
+ "from": "producers"
+ }
+ },
+ {
+ "name": "after",
+ "type": "duration",
+ "description": "How long before it times out",
+ "default": "5s",
+ "example": "5s"
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.1",
+ "example": "0.1"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "failed-after-success",
+ "group": "Downstream",
+ "title": "Did the work, then failed",
+ "description": "The send succeeds and then fails, as a reply that is
lost: a redelivery sends it again, so this tests that a duplicate does no harm",
+ "rule": "endpoint=${endpoint} error at=reply until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint the route sends to",
+ "example": "kafka:orders",
+ "pick": {
+ "from": "producers"
+ }
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "How many sends fail",
+ "default": "1",
+ "example": "1"
+ }
+ ]
+ },
+ {
+ "id": "outage",
+ "group": "Downstream",
+ "title": "Is down for a while",
+ "description": "Every send fails during the first part of each period,
as outage windows",
+ "rule": "endpoint=${endpoint} error rate=burst:${outage}\/${period}
until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint the route sends to",
+ "example": "kafka:orders",
+ "pick": {
+ "from": "producers"
+ }
+ },
+ {
+ "name": "outage",
+ "type": "duration",
+ "description": "How long each outage lasts",
+ "default": "10s",
+ "example": "10s"
+ },
+ {
+ "name": "period",
+ "type": "duration",
+ "description": "The time from one outage to the next",
+ "default": "1m",
+ "example": "1m"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "corrupt-entry",
+ "group": "Messages",
+ "title": "Arrives corrupt",
+ "description": "The messages that enter the route are changed: random
headers, the wrong charset, cut in half or empty",
+ "rule": "route=${route} corrupt mode=${mode} rate=${rate}
until=${until}",
+ "placeholders": [
+ {
+ "name": "route",
+ "type": "route",
+ "description": "The route",
+ "example": "orders",
+ "pick": {
+ "from": "routes"
+ }
+ },
+ {
+ "name": "mode",
+ "type": "enum",
+ "description": "How the message is changed",
+ "default": "headers",
+ "example": "headers",
+ "pick": {
+ "from": "values",
+ "values": [
+ "headers",
+ "charset",
+ "truncate",
+ "empty",
+ "remove-header"
+ ]
+ }
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.25",
+ "example": "0.25"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "empty-body",
+ "group": "Messages",
+ "title": "Is sent with an empty body",
+ "description": "The message sent to the endpoint has an empty body",
+ "rule": "endpoint=${endpoint} corrupt mode=empty rate=${rate}
until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint the route sends to",
+ "example": "kafka:orders",
+ "pick": {
+ "from": "producers"
+ }
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.25",
+ "example": "0.25"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "missing-header",
+ "group": "Messages",
+ "title": "Is sent without a header",
+ "description": "A header is missing from the message sent to the
endpoint",
+ "rule": "endpoint=${endpoint} corrupt mode=remove-header
header=${header} rate=${rate} until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint the route sends to",
+ "example": "kafka:orders",
+ "pick": {
+ "from": "producers"
+ }
+ },
+ {
+ "name": "header",
+ "type": "string",
+ "description": "The header, or a pattern with *",
+ "example": "X-Trace-Id",
+ "pick": {
+ "from": "headers"
+ }
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.25",
+ "example": "0.25"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "poll-error",
+ "group": "Consumers and routes",
+ "title": "Polls fail",
+ "description": "The polls of a scheduled poll consumer fail, as when
the source is not reachable",
+ "rule": "endpoint=${endpoint} error at=poll rate=${rate}
until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint a route consumes with a scheduled
poll",
+ "example": "file:inbox",
+ "pick": {
+ "from": "pollingConsumers"
+ }
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.5",
+ "example": "0.5"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "poll-skip",
+ "group": "Consumers and routes",
+ "title": "Polls are skipped",
+ "description": "The polls of a scheduled poll consumer are skipped, as
a consumer that falls behind",
+ "rule": "endpoint=${endpoint} drop at=poll rate=${rate}
until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint a route consumes with a scheduled
poll",
+ "example": "file:inbox",
+ "pick": {
+ "from": "pollingConsumers"
+ }
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.5",
+ "example": "0.5"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "stall",
+ "group": "Consumers and routes",
+ "title": "The route stalls",
+ "description": "The route is suspended for a while, and its messages
wait",
+ "rule": "route=${route} stall duration=${duration}",
+ "placeholders": [
+ {
+ "name": "route",
+ "type": "route",
+ "description": "The route",
+ "example": "orders",
+ "pick": {
+ "from": "routes"
+ }
+ },
+ {
+ "name": "duration",
+ "type": "duration",
+ "description": "How long it stalls",
+ "default": "20s",
+ "example": "20s"
+ }
+ ]
+ },
+ {
+ "id": "flap",
+ "group": "Consumers and routes",
+ "title": "The route flaps",
+ "description": "The route stops and starts again several times",
+ "rule": "route=${route} lifecycle action=stop duration=${duration}
repeat=${repeat} every=${every}",
+ "placeholders": [
+ {
+ "name": "route",
+ "type": "route",
+ "description": "The route",
+ "example": "orders",
+ "pick": {
+ "from": "routes"
+ }
+ },
+ {
+ "name": "duration",
+ "type": "duration",
+ "description": "How long it is down each time",
+ "default": "10s",
+ "example": "10s"
+ },
+ {
+ "name": "repeat",
+ "type": "integer",
+ "description": "How many times",
+ "default": "3",
+ "example": "3"
+ },
+ {
+ "name": "every",
+ "type": "duration",
+ "description": "The time from one time to the next",
+ "default": "1m",
+ "example": "1m"
+ }
+ ]
+ },
+ {
+ "id": "removed",
+ "group": "Consumers and routes",
+ "title": "The route is removed",
+ "description": "The route is removed, and added back after a while",
+ "rule": "route=${route} lifecycle action=remove duration=${duration}",
+ "placeholders": [
+ {
+ "name": "route",
+ "type": "route",
+ "description": "The route",
+ "example": "orders",
+ "pick": {
+ "from": "routes"
+ }
+ },
+ {
+ "name": "duration",
+ "type": "duration",
+ "description": "How long it is gone",
+ "default": "30s",
+ "example": "30s"
+ }
+ ]
+ },
+ {
+ "id": "dlq-unreachable",
+ "group": "Error handling",
+ "title": "The dead letter channel is unreachable",
+ "description": "The send of the dead letter channel fails, so the
messages it handles are lost (add it before the rule that makes them fail)",
+ "rule": "endpoint=${endpoint} error at=dlq",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint the route sends to",
+ "example": "kafka:orders",
+ "pick": {
+ "from": "producers"
+ }
+ }
+ ]
+ },
+ {
+ "id": "step-error",
+ "group": "Error handling",
+ "title": "A step fails",
+ "description": "A step of a route fails, as when its processor throws",
+ "rule": "step=${step} error rate=${rate} until=${until}",
+ "placeholders": [
+ {
+ "name": "step",
+ "type": "step",
+ "description": "The step",
+ "example": "enrich",
+ "pick": {
+ "from": "steps"
+ }
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.1",
+ "example": "0.1"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "step-slow",
+ "group": "Error handling",
+ "title": "A step is slow",
+ "description": "A step of a route waits before its processor is
called",
+ "rule": "step=${step} delay duration=${duration} rate=${rate}
until=${until}",
+ "placeholders": [
+ {
+ "name": "step",
+ "type": "step",
+ "description": "The step",
+ "example": "enrich",
+ "pick": {
+ "from": "steps"
+ }
+ },
+ {
+ "name": "duration",
+ "type": "duration",
+ "description": "The time, or a range of times",
+ "default": "1s",
+ "example": "1s"
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.25",
+ "example": "0.25"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ }
+ ]
+ }
+}
+
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 8032ef961160..bfb51bdfaba6 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
@@ -617,6 +617,29 @@ exchange gets which draw: with several threads (concurrent
consumers, parallel p
so two runs with the same seed can give different reports. With one thread and
the same exchanges in the same order,
the same seed gives the same faults.
+== The model of the rules
+
+The rules are also described as data, for the tools that help to write them (a
form in a terminal UI, an AI that turns
+a request into rules, the docs):
`META-INF/org/apache/camel/chaos/chaos-model.json` in the camel-chaos JAR, also
in the
+Camel catalog (`CamelCatalog.chaosModelJSon()`). It has the targets, the hooks
of each target and the faults each one
+offers (and whether it is reached only with `at=`), the parameters of each
fault with their types and limits, the
+options, the kinds and ops of an expectation, the limits, and recipes: known
rules in plain words, grouped by what goes
+wrong, each a rule with placeholders (`+${endpoint}+`, `+${rate}+`) and what
to pick them from (the endpoints the routes
+send to, the routes, the steps), such as:
+
+[source,json]
+----
+{
+ "id": "failed-after-success",
+ "group": "Downstream",
+ "title": "Did the work, then failed",
+ "rule": "endpoint=${endpoint} error at=reply until=${until}"
+}
+----
+
+The model is made from the tables of the parser, so it says what the parser
accepts; a tool still adds a rule through
+`ChaosManager` or the CLI connector, which check it as for any other rule.
+
== Limits
* A rule that ended stays in the rules (and the report) as `ended`. The hooks
skip it at once, and the next change
diff --git
a/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/CamelCatalog.java
b/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/CamelCatalog.java
index a0bbd7f2e0e4..fd9e8d349236 100644
---
a/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/CamelCatalog.java
+++
b/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/CamelCatalog.java
@@ -679,6 +679,16 @@ public interface CamelCatalog {
*/
String devConsolesOpenApiSpec();
+ /**
+ * The model of the chaos rules of camel-chaos, as JSON: the targets, the
hooks of each target and the faults each
+ * offers, the parameters of each fault, the options, the expectations,
the limits, and the recipes (known rules as
+ * templates), for the tools that help to write rules.
+ *
+ * @return the model, or null when the catalog has none
+ * @since 4.24
+ */
+ String chaosModelJSon();
+
/**
* Lists all the models (EIPs) summary details in JSon
*/
diff --git
a/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/DefaultCamelCatalog.java
b/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/DefaultCamelCatalog.java
index 9dd4534fa102..71f78886ae58 100644
---
a/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/DefaultCamelCatalog.java
+++
b/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/DefaultCamelCatalog.java
@@ -563,6 +563,11 @@ public class DefaultCamelCatalog extends
AbstractCachingCamelCatalog implements
return cache(BASE_RESOURCE_DIR + "/dev-consoles-openapi.json",
this::loadResource);
}
+ @Override
+ public String chaosModelJSon() {
+ return cache(BASE_RESOURCE_DIR + "/chaos-model.json",
this::loadResource);
+ }
+
@Override
public String listModelsAsJson() {
return cache(LIST_MODELS_AS_JSON, () ->
JsonMapper.serialize(findModelNames().stream()
diff --git
a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
index f7dfaf283c34..bef4e38d383f 100644
---
a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
+++
b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
@@ -2197,6 +2197,17 @@ public class CamelCatalogTest {
Assertions.assertTrue(advisory.getComponents().contains("camel-bean"));
}
+ @Test
+ public void chaosModel() {
+ String json = catalog.chaosModelJSon();
+ Assertions.assertNotNull(json);
+
+ JsonObject chaos = JsonMapper.deserialize(json).getJsonObject("chaos");
+ Assertions.assertNotNull(chaos);
+ Assertions.assertFalse(chaos.getCollection("faults").isEmpty());
+ Assertions.assertFalse(chaos.getCollection("recipes").isEmpty());
+ }
+
@Test
public void devConsolesOpenApiSpec() {
String json = catalog.devConsolesOpenApiSpec();
diff --git a/components/camel-chaos/pom.xml b/components/camel-chaos/pom.xml
index bb1b30568a96..638a9c8e6f26 100644
--- a/components/camel-chaos/pom.xml
+++ b/components/camel-chaos/pom.xml
@@ -110,6 +110,21 @@
<build>
<plugins>
+ <plugin>
+ <!-- the model of the chaos rules
(META-INF/org/apache/camel/chaos/chaos-model.json), from ChaosModel -->
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-package-maven-plugin</artifactId>
+ <version>${project.version}</version>
+ <executions>
+ <execution>
+ <id>generate-chaos-model</id>
+ <goals>
+ <goal>generate-chaos-model</goal>
+ </goals>
+ <phase>process-classes</phase>
+ </execution>
+ </executions>
+ </plugin>
<plugin>
<!-- sets the property net.bytebuddy:byte-buddy-agent:jar to
the path of the agent of the DST test -->
<groupId>org.apache.maven.plugins</groupId>
diff --git
a/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/chaos/chaos-model.json
b/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/chaos/chaos-model.json
new file mode 100644
index 000000000000..7505fb834508
--- /dev/null
+++
b/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/chaos/chaos-model.json
@@ -0,0 +1,1078 @@
+{
+ "chaos": {
+ "description": "The chaos rules of camel-chaos: a rule is one line, the
target, the fault and its parameters as name=value, then the options (such as
at, rate, from, until) and optionally an expectation",
+ "syntax": "<target>=<pattern> <fault> [<parameter>=<value> ...]
[<option>=<value> ...]",
+ "targets": [
+ {
+ "name": "route",
+ "description": "A route id, with * for any characters; one route id
(no pattern) for stall and lifecycle",
+ "pattern": true,
+ "pick": "routes"
+ },
+ {
+ "name": "endpoint",
+ "description": "An endpoint URI pattern, with * for any characters;
matched with or without the query parameters, and the \/\/ after the scheme
does not count (kafka:orders* matches kafka:\/\/orders?brokers=b1)",
+ "pattern": true,
+ "pick": "endpoints"
+ },
+ {
+ "name": "component",
+ "description": "The scheme of the endpoint URIs, with * for any
characters (no ':')",
+ "pattern": true,
+ "pick": "components"
+ },
+ {
+ "name": "step",
+ "description": "A step id (to1, or the id set with id(\"enrich\")),
with * for any characters",
+ "pattern": true,
+ "pick": "steps"
+ }
+ ],
+ "hooks": [
+ {
+ "name": "send",
+ "description": "The sends to a matching endpoint (to, toD, wireTap,
enrich, recipientList, a ProducerTemplate...), before the producer is called,
inside the error handler",
+ "targets": [
+ "endpoint",
+ "component"
+ ],
+ "faults": [
+ "error",
+ "delay",
+ "drop",
+ "corrupt"
+ ],
+ "onlyWithAt": false
+ },
+ {
+ "name": "reply",
+ "description": "The reply of such a send, after the producer
succeeded: its side effect happened (only with at=reply)",
+ "targets": [
+ "endpoint",
+ "component"
+ ],
+ "faults": [
+ "error",
+ "corrupt"
+ ],
+ "onlyWithAt": true
+ },
+ {
+ "name": "dlq",
+ "description": "The send of the dead letter channel to a matching
endpoint (only with at=dlq)",
+ "targets": [
+ "endpoint",
+ "component"
+ ],
+ "faults": [
+ "error",
+ "delay",
+ "drop",
+ "corrupt"
+ ],
+ "onlyWithAt": true
+ },
+ {
+ "name": "poll",
+ "description": "The polls of the scheduled poll consumers of the
routes",
+ "targets": [
+ "endpoint",
+ "component"
+ ],
+ "faults": [
+ "error",
+ "delay",
+ "drop"
+ ],
+ "onlyWithAt": false
+ },
+ {
+ "name": "entry",
+ "description": "Each exchange that enters a matching route, before the
first step",
+ "targets": [
+ "route"
+ ],
+ "faults": [
+ "corrupt"
+ ],
+ "onlyWithAt": false
+ },
+ {
+ "name": "step",
+ "description": "Each matching step of a route, right before its
processor is called",
+ "targets": [
+ "step"
+ ],
+ "faults": [
+ "error",
+ "delay"
+ ],
+ "onlyWithAt": false
+ }
+ ],
+ "faults": [
+ {
+ "name": "error",
+ "description": "Fails the send, the poll or the step with an exception
(a ChaosException without exception=), or the failure of an HTTP component for
status=",
+ "targets": [
+ "component",
+ "endpoint",
+ "step"
+ ],
+ "hooks": [
+ "send",
+ "reply",
+ "dlq",
+ "poll",
+ "step"
+ ],
+ "parameters": [
+ {
+ "name": "after",
+ "type": "duration",
+ "required": false,
+ "description": "Waits that long first, then fails, as a call that
times out; the producer is not called",
+ "example": "5s",
+ "notAt": [
+ "reply"
+ ]
+ },
+ {
+ "name": "body",
+ "type": "string",
+ "required": false,
+ "description": "The body of the HTTP response (needs status=)",
+ "example": "no stock"
+ },
+ {
+ "name": "exception",
+ "type": "class",
+ "required": false,
+ "description": "The class of the exception: a public Throwable
with a public constructor taking a String or none (not an
InterruptedException)",
+ "example": "java.net.ConnectException"
+ },
+ {
+ "name": "header.<name>",
+ "type": "string",
+ "required": false,
+ "description": "A header of the HTTP response, such as
header.Retry-After=5 (needs status=)",
+ "example": "5",
+ "prefix": "header."
+ },
+ {
+ "name": "message",
+ "type": "string",
+ "required": false,
+ "description": "The message of the exception (the class needs a
constructor taking a String)",
+ "example": "broker down"
+ },
+ {
+ "name": "status",
+ "type": "integer",
+ "required": false,
+ "description": "The failure of an HTTP component for this status
(not with exception= or message=)",
+ "example": "503",
+ "min": 300,
+ "max": 599,
+ "schemes": [
+ "http",
+ "https",
+ "netty-http",
+ "undertow",
+ "vertx-http"
+ ]
+ }
+ ],
+ "options": [
+ "at",
+ "expect",
+ "from",
+ "rate",
+ "until",
+ "within"
+ ]
+ },
+ {
+ "name": "delay",
+ "description": "Waits before the send, the poll or the step, as for a
slow endpoint",
+ "targets": [
+ "component",
+ "endpoint",
+ "step"
+ ],
+ "hooks": [
+ "send",
+ "dlq",
+ "poll",
+ "step"
+ ],
+ "parameters": [
+ {
+ "name": "duration",
+ "type": "duration",
+ "required": true,
+ "description": "How long to wait, or a range such as 100ms..2s for
a draw between the two times (from the seeded sequence of the rule)",
+ "example": "500ms",
+ "range": true
+ }
+ ],
+ "options": [
+ "at",
+ "expect",
+ "from",
+ "rate",
+ "until",
+ "within"
+ ]
+ },
+ {
+ "name": "drop",
+ "description": "A send that reports success without calling the
producer, or a poll that is skipped",
+ "targets": [
+ "component",
+ "endpoint"
+ ],
+ "hooks": [
+ "send",
+ "dlq",
+ "poll"
+ ],
+ "parameters": [
+
+ ],
+ "options": [
+ "at",
+ "expect",
+ "from",
+ "rate",
+ "until",
+ "within"
+ ]
+ },
+ {
+ "name": "corrupt",
+ "description": "Changes the message: its headers, the charset of its
body, or its body",
+ "targets": [
+ "component",
+ "endpoint",
+ "route"
+ ],
+ "hooks": [
+ "send",
+ "reply",
+ "dlq",
+ "entry"
+ ],
+ "parameters": [
+ {
+ "name": "header",
+ "type": "string",
+ "required": false,
+ "description": "The headers mode=remove-header removes: a name or
a pattern with *, ignoring case (without it every header that is not Camel*)",
+ "example": "X-Trace-*"
+ },
+ {
+ "name": "mode",
+ "type": "enum",
+ "required": true,
+ "description": "headers: the headers that are not Camel* get
random values; charset: a text body becomes UTF-16 bytes; truncate: a String,
byte[] or stream body ends after half of it; empty: the body becomes empty of
its kind (or null); remove-header: removes headers",
+ "example": "headers",
+ "enum": [
+ "headers",
+ "charset",
+ "truncate",
+ "empty",
+ "remove-header"
+ ]
+ }
+ ],
+ "options": [
+ "at",
+ "expect",
+ "from",
+ "rate",
+ "until",
+ "within"
+ ]
+ },
+ {
+ "name": "lifecycle",
+ "description": "Suspends, stops or removes the route of a route=<id>
target, and brings it back after the duration; repeat= takes it down more than
once",
+ "targets": [
+ "route"
+ ],
+ "hooks": [
+
+ ],
+ "parameters": [
+ {
+ "name": "action",
+ "type": "enum",
+ "required": true,
+ "description": "What to do to the route",
+ "example": "stop",
+ "enum": [
+ "suspend",
+ "stop",
+ "remove"
+ ]
+ },
+ {
+ "name": "duration",
+ "type": "duration",
+ "required": true,
+ "description": "How long the route stays down (at most
camel.chaos.maxDuration)",
+ "example": "20s"
+ },
+ {
+ "name": "every",
+ "type": "duration",
+ "required": false,
+ "description": "The time from one time the route goes down to the
next, longer than duration=",
+ "example": "1m"
+ },
+ {
+ "name": "repeat",
+ "type": "integer",
+ "required": false,
+ "description": "How many times to take the route down (needs
every= when more than 1)",
+ "example": "3",
+ "min": 1,
+ "max": 100
+ }
+ ],
+ "options": [
+ "expect",
+ "from",
+ "within"
+ ]
+ },
+ {
+ "name": "stall",
+ "description": "Short for lifecycle action=suspend: the route stalls
for the duration",
+ "targets": [
+ "route"
+ ],
+ "hooks": [
+
+ ],
+ "shortFor": "lifecycle action=suspend",
+ "parameters": [
+ {
+ "name": "duration",
+ "type": "duration",
+ "required": true,
+ "description": "How long the route stays down (at most
camel.chaos.maxDuration)",
+ "example": "20s"
+ },
+ {
+ "name": "every",
+ "type": "duration",
+ "required": false,
+ "description": "The time from one time the route goes down to the
next, longer than duration=",
+ "example": "1m"
+ },
+ {
+ "name": "repeat",
+ "type": "integer",
+ "required": false,
+ "description": "How many times to take the route down (needs
every= when more than 1)",
+ "example": "3",
+ "min": 1,
+ "max": 100
+ }
+ ],
+ "options": [
+ "expect",
+ "from",
+ "within"
+ ]
+ }
+ ],
+ "options": [
+ {
+ "name": "at",
+ "type": "enum",
+ "required": false,
+ "description": "Narrows the rule to one hook of its target (reply and
dlq are reached only with at=)",
+ "example": "send",
+ "enum": [
+ "send",
+ "reply",
+ "dlq",
+ "poll",
+ "entry",
+ "step"
+ ]
+ },
+ {
+ "name": "expect",
+ "type": "expectation",
+ "required": false,
+ "description": "An expectation of a route after the rule is added:
<route>:<count> (at least that many completed) or <route>:<kind><op><count>",
+ "example": "orders:handled<=0"
+ },
+ {
+ "name": "from",
+ "type": "duration",
+ "required": false,
+ "description": "When the rule starts after it is added, such as after
the warm-up (+30s or 30s)",
+ "example": "+30s"
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "required": false,
+ "description": "How often the rule fires: a probability above 0 and at
most 1, every Nth match, or bursts (every match during the first part of each
period); without it every match fires",
+ "example": "0.25",
+ "forms": [
+ "0.25",
+ "every:4",
+ "burst:5s\/30s"
+ ]
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "required": false,
+ "description": "When the rule ends: a number of matches or a time from
its start (at most camel.chaos.maxDuration, its default)",
+ "example": "10m"
+ },
+ {
+ "name": "within",
+ "type": "duration",
+ "required": false,
+ "description": "The time of the expectation (by default until the rule
ends)",
+ "example": "2m"
+ }
+ ],
+ "expectation": {
+ "kinds": [
+ {
+ "name": "completed",
+ "description": "The exchanges completed in the route"
+ },
+ {
+ "name": "handled",
+ "description": "The exchanges handled by the error handler in the
route"
+ },
+ {
+ "name": "failed",
+ "description": "The exchanges failed in the route"
+ },
+ {
+ "name": "redeliveries",
+ "description": "The redeliveries in the route"
+ }
+ ],
+ "ops": [
+ {
+ "name": ">=",
+ "description": "at least"
+ },
+ {
+ "name": "<=",
+ "description": "at most"
+ },
+ {
+ "name": "=",
+ "description": "exactly"
+ }
+ ]
+ },
+ "limits": {
+ "maxLength": 2000,
+ "maxRepeat": 100,
+ "maxDuration": "camel.chaos.maxDuration (default 10m): the longest
until, delay, after, from, burst period and stall or lifecycle duration",
+ "time": "a number with a unit (ms, s, m, h, d), also combined as 1m30s;
a number alone is milliseconds",
+ "quoting": "a value with spaces or ; is quoted with \" or ' (\\\", \\'
and \\\\ inside quotes)"
+ },
+ "recipes": [
+ {
+ "id": "http-error",
+ "group": "Downstream",
+ "title": "Returns an error (HTTP status)",
+ "description": "An HTTP call fails with a status, as for a service
that is down or overloaded: tests onException, redelivery and the dead letter
channel",
+ "rule": "endpoint=${endpoint} error status=${status} rate=${rate}
until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The HTTP endpoint the route calls",
+ "example": "https:\/\/inventory\/stock",
+ "pick": {
+ "from": "producers",
+ "schemes": [
+ "http",
+ "https",
+ "netty-http",
+ "undertow",
+ "vertx-http"
+ ]
+ }
+ },
+ {
+ "name": "status",
+ "type": "integer",
+ "description": "The HTTP status",
+ "default": "503",
+ "example": "503"
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.25",
+ "example": "0.25"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "connection-refused",
+ "group": "Downstream",
+ "title": "Refuses the connection",
+ "description": "The send fails as when the other side is not there",
+ "rule": "endpoint=${endpoint} error
exception=java.net.ConnectException rate=${rate} until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint the route sends to",
+ "example": "kafka:orders",
+ "pick": {
+ "from": "producers"
+ }
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.25",
+ "example": "0.25"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "slow",
+ "group": "Downstream",
+ "title": "Is slow",
+ "description": "Each send waits a different time within a range, as a
slow service does",
+ "rule": "endpoint=${endpoint} delay duration=${duration} rate=${rate}
until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint the route sends to",
+ "example": "kafka:orders",
+ "pick": {
+ "from": "producers"
+ }
+ },
+ {
+ "name": "duration",
+ "type": "duration",
+ "description": "The time, or a range of times",
+ "default": "200ms..3s",
+ "example": "200ms..3s"
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.5",
+ "example": "0.5"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "timeout",
+ "group": "Downstream",
+ "title": "Times out",
+ "description": "The send waits, then fails without calling the
producer, as a call that times out",
+ "rule": "endpoint=${endpoint} error after=${after}
exception=java.net.SocketTimeoutException rate=${rate} until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint the route sends to",
+ "example": "kafka:orders",
+ "pick": {
+ "from": "producers"
+ }
+ },
+ {
+ "name": "after",
+ "type": "duration",
+ "description": "How long before it times out",
+ "default": "5s",
+ "example": "5s"
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.1",
+ "example": "0.1"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "failed-after-success",
+ "group": "Downstream",
+ "title": "Did the work, then failed",
+ "description": "The send succeeds and then fails, as a reply that is
lost: a redelivery sends it again, so this tests that a duplicate does no harm",
+ "rule": "endpoint=${endpoint} error at=reply until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint the route sends to",
+ "example": "kafka:orders",
+ "pick": {
+ "from": "producers"
+ }
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "How many sends fail",
+ "default": "1",
+ "example": "1"
+ }
+ ]
+ },
+ {
+ "id": "outage",
+ "group": "Downstream",
+ "title": "Is down for a while",
+ "description": "Every send fails during the first part of each period,
as outage windows",
+ "rule": "endpoint=${endpoint} error rate=burst:${outage}\/${period}
until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint the route sends to",
+ "example": "kafka:orders",
+ "pick": {
+ "from": "producers"
+ }
+ },
+ {
+ "name": "outage",
+ "type": "duration",
+ "description": "How long each outage lasts",
+ "default": "10s",
+ "example": "10s"
+ },
+ {
+ "name": "period",
+ "type": "duration",
+ "description": "The time from one outage to the next",
+ "default": "1m",
+ "example": "1m"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "corrupt-entry",
+ "group": "Messages",
+ "title": "Arrives corrupt",
+ "description": "The messages that enter the route are changed: random
headers, the wrong charset, cut in half or empty",
+ "rule": "route=${route} corrupt mode=${mode} rate=${rate}
until=${until}",
+ "placeholders": [
+ {
+ "name": "route",
+ "type": "route",
+ "description": "The route",
+ "example": "orders",
+ "pick": {
+ "from": "routes"
+ }
+ },
+ {
+ "name": "mode",
+ "type": "enum",
+ "description": "How the message is changed",
+ "default": "headers",
+ "example": "headers",
+ "pick": {
+ "from": "values",
+ "values": [
+ "headers",
+ "charset",
+ "truncate",
+ "empty",
+ "remove-header"
+ ]
+ }
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.25",
+ "example": "0.25"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "empty-body",
+ "group": "Messages",
+ "title": "Is sent with an empty body",
+ "description": "The message sent to the endpoint has an empty body",
+ "rule": "endpoint=${endpoint} corrupt mode=empty rate=${rate}
until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint the route sends to",
+ "example": "kafka:orders",
+ "pick": {
+ "from": "producers"
+ }
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.25",
+ "example": "0.25"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "missing-header",
+ "group": "Messages",
+ "title": "Is sent without a header",
+ "description": "A header is missing from the message sent to the
endpoint",
+ "rule": "endpoint=${endpoint} corrupt mode=remove-header
header=${header} rate=${rate} until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint the route sends to",
+ "example": "kafka:orders",
+ "pick": {
+ "from": "producers"
+ }
+ },
+ {
+ "name": "header",
+ "type": "string",
+ "description": "The header, or a pattern with *",
+ "example": "X-Trace-Id",
+ "pick": {
+ "from": "headers"
+ }
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.25",
+ "example": "0.25"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "poll-error",
+ "group": "Consumers and routes",
+ "title": "Polls fail",
+ "description": "The polls of a scheduled poll consumer fail, as when
the source is not reachable",
+ "rule": "endpoint=${endpoint} error at=poll rate=${rate}
until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint a route consumes with a scheduled
poll",
+ "example": "file:inbox",
+ "pick": {
+ "from": "pollingConsumers"
+ }
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.5",
+ "example": "0.5"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "poll-skip",
+ "group": "Consumers and routes",
+ "title": "Polls are skipped",
+ "description": "The polls of a scheduled poll consumer are skipped, as
a consumer that falls behind",
+ "rule": "endpoint=${endpoint} drop at=poll rate=${rate}
until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint a route consumes with a scheduled
poll",
+ "example": "file:inbox",
+ "pick": {
+ "from": "pollingConsumers"
+ }
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.5",
+ "example": "0.5"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "stall",
+ "group": "Consumers and routes",
+ "title": "The route stalls",
+ "description": "The route is suspended for a while, and its messages
wait",
+ "rule": "route=${route} stall duration=${duration}",
+ "placeholders": [
+ {
+ "name": "route",
+ "type": "route",
+ "description": "The route",
+ "example": "orders",
+ "pick": {
+ "from": "routes"
+ }
+ },
+ {
+ "name": "duration",
+ "type": "duration",
+ "description": "How long it stalls",
+ "default": "20s",
+ "example": "20s"
+ }
+ ]
+ },
+ {
+ "id": "flap",
+ "group": "Consumers and routes",
+ "title": "The route flaps",
+ "description": "The route stops and starts again several times",
+ "rule": "route=${route} lifecycle action=stop duration=${duration}
repeat=${repeat} every=${every}",
+ "placeholders": [
+ {
+ "name": "route",
+ "type": "route",
+ "description": "The route",
+ "example": "orders",
+ "pick": {
+ "from": "routes"
+ }
+ },
+ {
+ "name": "duration",
+ "type": "duration",
+ "description": "How long it is down each time",
+ "default": "10s",
+ "example": "10s"
+ },
+ {
+ "name": "repeat",
+ "type": "integer",
+ "description": "How many times",
+ "default": "3",
+ "example": "3"
+ },
+ {
+ "name": "every",
+ "type": "duration",
+ "description": "The time from one time to the next",
+ "default": "1m",
+ "example": "1m"
+ }
+ ]
+ },
+ {
+ "id": "removed",
+ "group": "Consumers and routes",
+ "title": "The route is removed",
+ "description": "The route is removed, and added back after a while",
+ "rule": "route=${route} lifecycle action=remove duration=${duration}",
+ "placeholders": [
+ {
+ "name": "route",
+ "type": "route",
+ "description": "The route",
+ "example": "orders",
+ "pick": {
+ "from": "routes"
+ }
+ },
+ {
+ "name": "duration",
+ "type": "duration",
+ "description": "How long it is gone",
+ "default": "30s",
+ "example": "30s"
+ }
+ ]
+ },
+ {
+ "id": "dlq-unreachable",
+ "group": "Error handling",
+ "title": "The dead letter channel is unreachable",
+ "description": "The send of the dead letter channel fails, so the
messages it handles are lost (add it before the rule that makes them fail)",
+ "rule": "endpoint=${endpoint} error at=dlq",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint the route sends to",
+ "example": "kafka:orders",
+ "pick": {
+ "from": "producers"
+ }
+ }
+ ]
+ },
+ {
+ "id": "step-error",
+ "group": "Error handling",
+ "title": "A step fails",
+ "description": "A step of a route fails, as when its processor throws",
+ "rule": "step=${step} error rate=${rate} until=${until}",
+ "placeholders": [
+ {
+ "name": "step",
+ "type": "step",
+ "description": "The step",
+ "example": "enrich",
+ "pick": {
+ "from": "steps"
+ }
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.1",
+ "example": "0.1"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
+ {
+ "id": "step-slow",
+ "group": "Error handling",
+ "title": "A step is slow",
+ "description": "A step of a route waits before its processor is
called",
+ "rule": "step=${step} delay duration=${duration} rate=${rate}
until=${until}",
+ "placeholders": [
+ {
+ "name": "step",
+ "type": "step",
+ "description": "The step",
+ "example": "enrich",
+ "pick": {
+ "from": "steps"
+ }
+ },
+ {
+ "name": "duration",
+ "type": "duration",
+ "description": "The time, or a range of times",
+ "default": "1s",
+ "example": "1s"
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.25",
+ "example": "0.25"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ }
+ ]
+ }
+}
+
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc
b/components/camel-chaos/src/main/docs/chaos.adoc
index 8032ef961160..bfb51bdfaba6 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -617,6 +617,29 @@ exchange gets which draw: with several threads (concurrent
consumers, parallel p
so two runs with the same seed can give different reports. With one thread and
the same exchanges in the same order,
the same seed gives the same faults.
+== The model of the rules
+
+The rules are also described as data, for the tools that help to write them (a
form in a terminal UI, an AI that turns
+a request into rules, the docs):
`META-INF/org/apache/camel/chaos/chaos-model.json` in the camel-chaos JAR, also
in the
+Camel catalog (`CamelCatalog.chaosModelJSon()`). It has the targets, the hooks
of each target and the faults each one
+offers (and whether it is reached only with `at=`), the parameters of each
fault with their types and limits, the
+options, the kinds and ops of an expectation, the limits, and recipes: known
rules in plain words, grouped by what goes
+wrong, each a rule with placeholders (`+${endpoint}+`, `+${rate}+`) and what
to pick them from (the endpoints the routes
+send to, the routes, the steps), such as:
+
+[source,json]
+----
+{
+ "id": "failed-after-success",
+ "group": "Downstream",
+ "title": "Did the work, then failed",
+ "rule": "endpoint=${endpoint} error at=reply until=${until}"
+}
+----
+
+The model is made from the tables of the parser, so it says what the parser
accepts; a tool still adds a rule through
+`ChaosManager` or the CLI connector, which check it as for any other rule.
+
== Limits
* A rule that ended stays in the rules (and the report) as `ended`. The hooks
skip it at once, and the next change
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosModel.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosModel.java
new file mode 100644
index 000000000000..a8c0f2ede91e
--- /dev/null
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosModel.java
@@ -0,0 +1,602 @@
+/*
+ * 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.EnumSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.camel.component.chaos.ChaosRule.Action;
+import org.apache.camel.component.chaos.ChaosRule.CorruptMode;
+import org.apache.camel.component.chaos.ChaosRule.Expectation;
+import org.apache.camel.component.chaos.ChaosRule.Fault;
+import org.apache.camel.component.chaos.ChaosRule.Hook;
+import org.apache.camel.component.chaos.ChaosRule.Target;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * The model of the chaos rules as data, for the tools that help to write them
(the chaos screen of the terminal UI, an
+ * AI that turns a prompt into rules, the docs): the targets, the hooks of
each target and the faults each hook offers,
+ * the parameters of each fault, the general options, the expectations, the
limits, and the recipes (known rules in
+ * plain words, as templates).
+ * <p/>
+ * The structure comes from the tables of {@link ChaosRuleParser}, so it
cannot drift from what the parser accepts; the
+ * descriptions and the recipes are written here. The build writes {@link
#FILE} from {@link #toJson()} (the
+ * {@code generate-chaos-model} goal of camel-package-maven-plugin), {@code
ChaosModelTest} checks that every recipe
+ * parses, and the catalog copies the file ({@code
CamelCatalog.chaosModelJSon()}).
+ */
+final class ChaosModel {
+
+ /** Where the model is, in the jar and under {@code
src/generated/resources}. */
+ static final String FILE =
"META-INF/org/apache/camel/chaos/chaos-model.json";
+
+ private ChaosModel() {
+ }
+
+ /** The model as JSON, pretty printed (the content of {@link #FILE}); the
build calls it. */
+ static String toJson() {
+ return Jsoner.prettyPrint(model().toJson(), 2) + "\n";
+ }
+
+ static JsonObject model() {
+ JsonObject chaos = new JsonObject();
+ chaos.put("description", "The chaos rules of camel-chaos: a rule is
one line, the target, the fault and its"
+ + " parameters as name=value, then the
options (such as at, rate, from, until) and"
+ + " optionally an expectation");
+ chaos.put("syntax", "<target>=<pattern> <fault> [<parameter>=<value>
...] [<option>=<value> ...]");
+ chaos.put("targets", targets());
+ chaos.put("hooks", hooks());
+ chaos.put("faults", faults());
+ chaos.put("options", options());
+ chaos.put("expectation", expectation());
+ chaos.put("limits", limits());
+ chaos.put("recipes", recipes());
+ JsonObject root = new JsonObject();
+ root.put("chaos", chaos);
+ return root;
+ }
+
+ // ---- the targets and the hooks ----
+
+ private static JsonArray targets() {
+ JsonArray answer = new JsonArray();
+ for (Target target : Target.values()) {
+ JsonObject jo = new JsonObject();
+ jo.put("name", target.key);
+ switch (target) {
+ case ENDPOINT -> {
+ jo.put("description", "An endpoint URI pattern, with * for
any characters; matched with or without"
+ + " the query parameters, and the //
after the scheme does not count"
+ + " (kafka:orders* matches
kafka://orders?brokers=b1)");
+ jo.put("pattern", true);
+ jo.put("pick", "endpoints");
+ }
+ case COMPONENT -> {
+ jo.put("description", "The scheme of the endpoint URIs,
with * for any characters (no ':')");
+ jo.put("pattern", true);
+ jo.put("pick", "components");
+ }
+ case ROUTE -> {
+ jo.put("description", "A route id, with * for any
characters; one route id (no pattern) for"
+ + " stall and lifecycle");
+ jo.put("pattern", true);
+ jo.put("pick", "routes");
+ }
+ case STEP -> {
+ jo.put("description", "A step id (to1, or the id set with
id(\"enrich\")), with * for any"
+ + " characters");
+ jo.put("pattern", true);
+ jo.put("pick", "steps");
+ }
+ default -> throw new IllegalStateException("Unknown target " +
target);
+ }
+ answer.add(jo);
+ }
+ return answer;
+ }
+
+ private static JsonArray hooks() {
+ // the targets and the faults of each hook, from the hooks of each
target the parser offers
+ Map<Hook, List<String>> targets = new LinkedHashMap<>();
+ Map<Hook, Set<Fault>> faults = new LinkedHashMap<>();
+ for (Target target : Target.values()) {
+ for (Map.Entry<Hook, Set<Fault>> entry :
ChaosRuleParser.offered(target).entrySet()) {
+ targets.computeIfAbsent(entry.getKey(), k -> new
ArrayList<>()).add(target.key);
+ faults.computeIfAbsent(entry.getKey(), k ->
EnumSet.noneOf(Fault.class)).addAll(entry.getValue());
+ }
+ }
+ JsonArray answer = new JsonArray();
+ for (Hook hook : Hook.values()) {
+ JsonObject jo = new JsonObject();
+ jo.put("name", hook.key);
+ jo.put("description", switch (hook) {
+ case SEND -> "The sends to a matching endpoint (to, toD,
wireTap, enrich, recipientList, a"
+ + " ProducerTemplate...), before the producer is
called, inside the error handler";
+ case REPLY -> "The reply of such a send, after the producer
succeeded: its side effect happened (only"
+ + " with at=reply)";
+ case DLQ -> "The send of the dead letter channel to a matching
endpoint (only with at=dlq)";
+ case POLL -> "The polls of the scheduled poll consumers of the
routes";
+ case ENTRY -> "Each exchange that enters a matching route,
before the first step";
+ case STEP -> "Each matching step of a route, right before its
processor is called";
+ });
+ jo.put("targets", new JsonArray(targets.getOrDefault(hook,
List.of())));
+ jo.put("faults", keys(faults.getOrDefault(hook, Set.of())));
+ jo.put("onlyWithAt", ChaosRuleParser.onlyWithAt(hook));
+ answer.add(jo);
+ }
+ return answer;
+ }
+
+ // ---- the faults and their parameters ----
+
+ private static JsonArray faults() {
+ JsonArray answer = new JsonArray();
+ for (Fault fault : Fault.values()) {
+ if (fault == Fault.LIFECYCLE) {
+ answer.add(fault("lifecycle", fault, false,
+ "Suspends, stops or removes the route of a route=<id>
target, and brings it back after the"
+ + " duration;
repeat= takes it down more than once"));
+ answer.add(fault("stall", fault, true, "Short for lifecycle
action=suspend: the route stalls for the"
+ + " duration"));
+ } else {
+ answer.add(fault(fault.key, fault, false, switch (fault) {
+ case ERROR -> "Fails the send, the poll or the step with
an exception (a ChaosException without"
+ + " exception=), or the failure of an HTTP
component for status=";
+ case DELAY -> "Waits before the send, the poll or the
step, as for a slow endpoint";
+ case DROP -> "A send that reports success without calling
the producer, or a poll that is skipped";
+ case CORRUPT -> "Changes the message: its headers, the
charset of its body, or its body";
+ default -> throw new IllegalStateException("Unknown fault
" + fault);
+ }));
+ }
+ }
+ return answer;
+ }
+
+ private static JsonObject fault(String name, Fault fault, boolean stall,
String description) {
+ JsonObject jo = new JsonObject();
+ jo.put("name", name);
+ jo.put("description", description);
+ // the targets that have a hook offering the fault; stall and
lifecycle act on a route at no hook
+ Set<String> targets = new TreeSet<>();
+ Set<Hook> hooks = EnumSet.noneOf(Hook.class);
+ for (Target target : Target.values()) {
+ for (Map.Entry<Hook, Set<Fault>> entry :
ChaosRuleParser.offered(target).entrySet()) {
+ if (entry.getValue().contains(fault)) {
+ targets.add(target.key);
+ hooks.add(entry.getKey());
+ }
+ }
+ }
+ if (fault == Fault.LIFECYCLE) {
+ targets.add(Target.ROUTE.key);
+ }
+ jo.put("targets", new JsonArray(List.copyOf(targets)));
+ jo.put("hooks", keys(hooks));
+ if (stall) {
+ jo.put("shortFor", "lifecycle action=suspend");
+ }
+ JsonArray parameters = new JsonArray();
+ for (String key : new TreeSet<>(ChaosRuleParser.faultKeys(fault,
stall))) {
+ parameters.add(parameter(fault, key));
+ }
+ jo.put("parameters", parameters);
+ jo.put("options", new JsonArray(List.copyOf(new
TreeSet<>(ChaosRuleParser.optionKeys(fault)))));
+ return jo;
+ }
+
+ private static JsonObject parameter(Fault fault, String key) {
+ String scope = fault.key + " " + key;
+ return switch (scope) {
+ case "error exception" -> param("exception", "class", false,
+ "The class of the exception: a public Throwable with a
public constructor taking a String or none"
+ + "
(not an InterruptedException)",
+ "java.net.ConnectException");
+ case "error message" -> param("message", "string", false,
+ "The message of the exception (the class needs a
constructor taking a String)", "broker down");
+ case "error after" -> withNote(param("after", "duration", false,
+ "Waits that long first, then fails, as a call that times
out; the producer is not called", "5s"),
+ "notAt", new JsonArray(List.of(Hook.REPLY.key)));
+ case "error status" -> {
+ JsonObject p = param("status", "integer", false,
+ "The failure of an HTTP component for this status (not
with exception= or message=)", "503");
+ p.put("min", 300);
+ p.put("max", 599);
+ p.put("schemes", new JsonArray(List.copyOf(new
TreeSet<>(ChaosRuleParser.HTTP_SCHEMES))));
+ yield p;
+ }
+ case "error body" -> param("body", "string", false, "The body of
the HTTP response (needs status=)",
+ "no stock");
+ case "error header." -> {
+ JsonObject p = param("header.<name>", "string", false,
+ "A header of the HTTP response, such as
header.Retry-After=5 (needs status=)", "5");
+ p.put("prefix", ChaosRuleParser.HEADER_PREFIX);
+ yield p;
+ }
+ case "delay duration" -> {
+ JsonObject p = param("duration", "duration", true,
+ "How long to wait, or a range such as 100ms..2s for a
draw between the two times (from the"
+ + " seeded
sequence of the rule)",
+ "500ms");
+ p.put("range", true);
+ yield p;
+ }
+ case "corrupt mode" -> {
+ JsonObject p = param("mode", "enum", true,
+ "headers: the headers that are not Camel* get random
values; charset: a text body becomes"
+ + " UTF-16 bytes;
truncate: a String, byte[] or stream body ends"
+ + " after half of
it; empty: the body becomes empty of its kind"
+ + " (or null);
remove-header: removes headers",
+ CorruptMode.HEADERS.key);
+ p.put("enum", new JsonArray(corruptModes()));
+ yield p;
+ }
+ case "corrupt header" -> param("header", "string", false,
+ "The headers mode=remove-header removes: a name or a
pattern with *, ignoring case (without it"
+ + "
every header that is not Camel*)",
+ "X-Trace-*");
+ case "lifecycle action" -> {
+ JsonObject p = param("action", "enum", true, "What to do to
the route", Action.STOP.key);
+ JsonArray actions = new JsonArray();
+ for (Action action : Action.values()) {
+ actions.add(action.key);
+ }
+ p.put("enum", actions);
+ yield p;
+ }
+ case "lifecycle duration" -> param("duration", "duration", true,
+ "How long the route stays down (at most
camel.chaos.maxDuration)", "20s");
+ case "lifecycle repeat" -> {
+ JsonObject p = param("repeat", "integer", false,
+ "How many times to take the route down (needs every=
when more than 1)", "3");
+ p.put("min", 1);
+ p.put("max", ChaosRuleParser.MAX_REPEAT);
+ yield p;
+ }
+ case "lifecycle every" -> param("every", "duration", false,
+ "The time from one time the route goes down to the next,
longer than duration=", "1m");
+ default -> throw new IllegalStateException("The chaos model has no
description of the parameter " + scope);
+ };
+ }
+
+ // ---- the options, the expectation and the limits ----
+
+ private static JsonArray options() {
+ JsonArray answer = new JsonArray();
+ for (String key :
ChaosRuleParser.GENERAL_KEYS.stream().sorted().toList()) {
+ answer.add(switch (key) {
+ case "at" -> {
+ JsonObject p = param("at", "enum", false,
+ "Narrows the rule to one hook of its target (reply
and dlq are reached only with at=)",
+ Hook.SEND.key);
+ JsonArray hooks = new JsonArray();
+ for (Hook hook : Hook.values()) {
+ hooks.add(hook.key);
+ }
+ p.put("enum", hooks);
+ yield p;
+ }
+ case "rate" -> {
+ JsonObject p = param("rate", "rate", false,
+ "How often the rule fires: a probability above 0
and at most 1, every Nth match, or bursts"
+ + " (every
match during the first part of each period);"
+ + " without it
every match fires",
+ "0.25");
+ p.put("forms", new JsonArray(List.of("0.25", "every:4",
"burst:5s/30s")));
+ yield p;
+ }
+ case "until" -> param("until", "countOrDuration", false,
+ "When the rule ends: a number of matches or a time
from its start (at most"
+ + "
camel.chaos.maxDuration, its default)",
+ "10m");
+ case "from" -> param("from", "duration", false,
+ "When the rule starts after it is added, such as after
the warm-up (+30s or 30s)", "+30s");
+ case "expect" -> param("expect", "expectation", false,
+ "An expectation of a route after the rule is added:
<route>:<count> (at least that many"
+ + "
completed) or <route>:<kind><op><count>",
+ "orders:handled<=0");
+ case "within" -> param("within", "duration", false,
+ "The time of the expectation (by default until the
rule ends)", "2m");
+ default -> throw new IllegalStateException("The chaos model
has no description of the option " + key);
+ });
+ }
+ return answer;
+ }
+
+ private static JsonObject expectation() {
+ JsonObject jo = new JsonObject();
+ JsonArray kinds = new JsonArray();
+ for (Expectation.Kind kind : Expectation.Kind.values()) {
+ JsonObject k = new JsonObject();
+ k.put("name", kind.key);
+ k.put("description", "The " + kind.words(2) + " in the route");
+ kinds.add(k);
+ }
+ jo.put("kinds", kinds);
+ JsonArray ops = new JsonArray();
+ for (Expectation.Op op : Expectation.Op.values()) {
+ JsonObject o = new JsonObject();
+ o.put("name", op.key);
+ o.put("description", op.words);
+ ops.add(o);
+ }
+ jo.put("ops", ops);
+ return jo;
+ }
+
+ private static JsonObject limits() {
+ JsonObject jo = new JsonObject();
+ jo.put("maxLength", ChaosRuleParser.MAX_LENGTH);
+ jo.put("maxRepeat", ChaosRuleParser.MAX_REPEAT);
+ jo.put("maxDuration", "camel.chaos.maxDuration (default 10m): the
longest until, delay, after, from, burst period"
+ + " and stall or lifecycle duration");
+ jo.put("time", "a number with a unit (ms, s, m, h, d), also combined
as 1m30s; a number alone is milliseconds");
+ jo.put("quoting", "a value with spaces or ; is quoted with \" or '
(\\\", \\' and \\\\ inside quotes)");
+ return jo;
+ }
+
+ // ---- the recipes ----
+
+ /**
+ * A placeholder of a recipe: {@code ${name}} in its rule. The example is
what the model test fills in to parse the
+ * recipe; the default is what a tool offers first.
+ */
+ record Placeholder(String name, String type, String description, String
example, String defaultValue,
+ JsonObject pick) {
+ }
+
+ /** A known rule in plain words, as a template. */
+ record Recipe(String id, String group, String title, String description,
String rule, List<Placeholder> placeholders) {
+ }
+
+ static List<Recipe> recipeList() {
+ List<Recipe> answer = new ArrayList<>();
+ // downstream
+ answer.add(new Recipe(
+ "http-error", "Downstream", "Returns an error (HTTP status)",
+ "An HTTP call fails with a status, as for a service that is
down or overloaded: tests onException,"
+
+ " redelivery and the dead letter channel",
+ "endpoint=${endpoint} error status=${status} rate=${rate}
until=${until}",
+ List.of(httpEndpoint(), new Placeholder("status", "integer",
"The HTTP status", "503", "503", null),
+ rate("0.25"), until("10m"))));
+ answer.add(new Recipe(
+ "connection-refused", "Downstream", "Refuses the connection",
+ "The send fails as when the other side is not there",
+ "endpoint=${endpoint} error
exception=java.net.ConnectException rate=${rate} until=${until}",
+ List.of(producer(), rate("0.25"), until("10m"))));
+ answer.add(new Recipe(
+ "slow", "Downstream", "Is slow",
+ "Each send waits a different time within a range, as a slow
service does",
+ "endpoint=${endpoint} delay duration=${duration} rate=${rate}
until=${until}",
+ List.of(producer(), new Placeholder(
+ "duration", "duration", "The time, or a range of
times", "200ms..3s",
+ "200ms..3s", null), rate("0.5"), until("10m"))));
+ answer.add(new Recipe(
+ "timeout", "Downstream", "Times out",
+ "The send waits, then fails without calling the producer, as a
call that times out",
+ "endpoint=${endpoint} error after=${after}
exception=java.net.SocketTimeoutException rate=${rate}"
+
+ " until=${until}",
+ List.of(producer(), new Placeholder("after", "duration", "How
long before it times out", "5s", "5s", null),
+ rate("0.1"), until("10m"))));
+ answer.add(new Recipe(
+ "failed-after-success", "Downstream", "Did the work, then
failed",
+ "The send succeeds and then fails, as a reply that is lost: a
redelivery sends it again, so this tests"
+
+ " that a duplicate does no harm",
+ "endpoint=${endpoint} error at=reply until=${until}",
+ List.of(producer(), new Placeholder("until",
"countOrDuration", "How many sends fail", "1", "1", null))));
+ answer.add(new Recipe(
+ "outage", "Downstream", "Is down for a while",
+ "Every send fails during the first part of each period, as
outage windows",
+ "endpoint=${endpoint} error rate=burst:${outage}/${period}
until=${until}",
+ List.of(producer(), new Placeholder("outage", "duration", "How
long each outage lasts", "10s", "10s", null),
+ new Placeholder("period", "duration", "The time from
one outage to the next", "1m", "1m", null),
+ until("10m"))));
+ // messages
+ answer.add(new Recipe(
+ "corrupt-entry", "Messages", "Arrives corrupt",
+ "The messages that enter the route are changed: random
headers, the wrong charset, cut in half or"
+ + " empty",
+ "route=${route} corrupt mode=${mode} rate=${rate}
until=${until}",
+ List.of(route(), new Placeholder(
+ "mode", "enum", "How the message is changed",
"headers", "headers",
+ pick("values", "values", corruptModes())),
+ rate("0.25"), until("10m"))));
+ answer.add(new Recipe(
+ "empty-body", "Messages", "Is sent with an empty body",
+ "The message sent to the endpoint has an empty body",
+ "endpoint=${endpoint} corrupt mode=empty rate=${rate}
until=${until}",
+ List.of(producer(), rate("0.25"), until("10m"))));
+ answer.add(new Recipe(
+ "missing-header", "Messages", "Is sent without a header",
+ "A header is missing from the message sent to the endpoint",
+ "endpoint=${endpoint} corrupt mode=remove-header
header=${header} rate=${rate} until=${until}",
+ List.of(producer(), new Placeholder(
+ "header", "string", "The header, or a pattern with *",
"X-Trace-Id",
+ null, pick("headers", null, null)), rate("0.25"),
until("10m"))));
+ // consumers and routes
+ answer.add(new Recipe(
+ "poll-error", "Consumers and routes", "Polls fail",
+ "The polls of a scheduled poll consumer fail, as when the
source is not reachable",
+ "endpoint=${endpoint} error at=poll rate=${rate}
until=${until}",
+ List.of(polled(), rate("0.5"), until("10m"))));
+ answer.add(new Recipe(
+ "poll-skip", "Consumers and routes", "Polls are skipped",
+ "The polls of a scheduled poll consumer are skipped, as a
consumer that falls behind",
+ "endpoint=${endpoint} drop at=poll rate=${rate}
until=${until}",
+ List.of(polled(), rate("0.5"), until("10m"))));
+ answer.add(new Recipe(
+ "stall", "Consumers and routes", "The route stalls",
+ "The route is suspended for a while, and its messages wait",
+ "route=${route} stall duration=${duration}",
+ List.of(route(), new Placeholder("duration", "duration", "How
long it stalls", "20s", "20s", null))));
+ answer.add(new Recipe(
+ "flap", "Consumers and routes", "The route flaps",
+ "The route stops and starts again several times",
+ "route=${route} lifecycle action=stop duration=${duration}
repeat=${repeat} every=${every}",
+ List.of(route(), new Placeholder("duration", "duration", "How
long it is down each time", "10s", "10s", null),
+ new Placeholder("repeat", "integer", "How many times",
"3", "3", null),
+ new Placeholder("every", "duration", "The time from
one time to the next", "1m", "1m", null))));
+ answer.add(new Recipe(
+ "removed", "Consumers and routes", "The route is removed",
+ "The route is removed, and added back after a while",
+ "route=${route} lifecycle action=remove duration=${duration}",
+ List.of(route(), new Placeholder("duration", "duration", "How
long it is gone", "30s", "30s", null))));
+ // error handling
+ answer.add(new Recipe(
+ "dlq-unreachable", "Error handling", "The dead letter channel
is unreachable",
+ "The send of the dead letter channel fails, so the messages it
handles are lost (add it before the rule"
+
+ " that makes them fail)",
+ "endpoint=${endpoint} error at=dlq",
+ List.of(producer())));
+ answer.add(new Recipe(
+ "step-error", "Error handling", "A step fails",
+ "A step of a route fails, as when its processor throws",
+ "step=${step} error rate=${rate} until=${until}",
+ List.of(step(), rate("0.1"), until("10m"))));
+ answer.add(new Recipe(
+ "step-slow", "Error handling", "A step is slow",
+ "A step of a route waits before its processor is called",
+ "step=${step} delay duration=${duration} rate=${rate}
until=${until}",
+ List.of(step(), new Placeholder("duration", "duration", "The
time, or a range of times", "1s", "1s", null),
+ rate("0.25"), until("10m"))));
+ return answer;
+ }
+
+ private static JsonArray recipes() {
+ JsonArray answer = new JsonArray();
+ for (Recipe recipe : recipeList()) {
+ JsonObject jo = new JsonObject();
+ jo.put("id", recipe.id());
+ jo.put("group", recipe.group());
+ jo.put("title", recipe.title());
+ jo.put("description", recipe.description());
+ jo.put("rule", recipe.rule());
+ JsonArray placeholders = new JsonArray();
+ for (Placeholder p : recipe.placeholders()) {
+ JsonObject po = new JsonObject();
+ po.put("name", p.name());
+ po.put("type", p.type());
+ po.put("description", p.description());
+ if (p.defaultValue() != null) {
+ po.put("default", p.defaultValue());
+ }
+ po.put("example", p.example());
+ if (p.pick() != null) {
+ po.put("pick", p.pick());
+ }
+ placeholders.add(po);
+ }
+ jo.put("placeholders", placeholders);
+ answer.add(jo);
+ }
+ return answer;
+ }
+
+ private static Placeholder producer() {
+ return new Placeholder(
+ "endpoint", "endpoint", "The endpoint the route sends to",
"kafka:orders", null,
+ pick("producers", null, null));
+ }
+
+ private static Placeholder httpEndpoint() {
+ return new Placeholder(
+ "endpoint", "endpoint", "The HTTP endpoint the route calls",
"https://inventory/stock",
+ null, pick("producers", "schemes", List.copyOf(new
TreeSet<>(ChaosRuleParser.HTTP_SCHEMES))));
+ }
+
+ private static Placeholder polled() {
+ return new Placeholder(
+ "endpoint", "endpoint", "The endpoint a route consumes with a
scheduled poll",
+ "file:inbox", null, pick("pollingConsumers", null, null));
+ }
+
+ private static Placeholder route() {
+ return new Placeholder("route", "route", "The route", "orders", null,
pick("routes", null, null));
+ }
+
+ private static Placeholder step() {
+ return new Placeholder("step", "step", "The step", "enrich", null,
pick("steps", null, null));
+ }
+
+ private static Placeholder rate(String value) {
+ return new Placeholder(
+ "rate", "rate", "How often: a probability, every:N or
burst:<time>/<period>", value, value,
+ null);
+ }
+
+ private static Placeholder until(String value) {
+ return new Placeholder(
+ "until", "countOrDuration", "When the rule ends: a number of
matches or a time", value,
+ value, null);
+ }
+
+ /**
+ * Where a tool picks the value of a placeholder from: the endpoints the
routes send to (producers), the endpoints
+ * the routes consume with a scheduled poll (pollingConsumers), the
routes, the steps, the headers, or the values
+ * listed.
+ */
+ private static JsonObject pick(String from, String key, List<String>
values) {
+ JsonObject jo = new JsonObject();
+ jo.put("from", from);
+ if (values != null) {
+ jo.put(key, new JsonArray(values));
+ }
+ return jo;
+ }
+
+ private static List<String> corruptModes() {
+ List<String> answer = new ArrayList<>();
+ for (CorruptMode mode : CorruptMode.values()) {
+ answer.add(mode.key);
+ }
+ return answer;
+ }
+
+ // ---- helpers ----
+
+ private static JsonObject param(String name, String type, boolean
required, String description, String example) {
+ JsonObject jo = new JsonObject();
+ jo.put("name", name);
+ jo.put("type", type);
+ jo.put("required", required);
+ jo.put("description", description);
+ jo.put("example", example);
+ return jo;
+ }
+
+ private static JsonObject withNote(JsonObject jo, String key, Object
value) {
+ jo.put(key, value);
+ return jo;
+ }
+
+ private static <E extends Enum<E>> JsonArray keys(Set<E> values) {
+ JsonArray answer = new JsonArray();
+ for (E value : values) {
+ answer.add(switch (value) {
+ case Hook h -> h.key;
+ case Fault f -> f.key;
+ default -> value.name();
+ });
+ }
+ return answer;
+ }
+}
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
index 879e027485f9..32165cb3a225 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
@@ -20,7 +20,6 @@ import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.EnumSet;
-import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
@@ -56,11 +55,16 @@ final class ChaosRuleParser {
*/
static final Set<String> HTTP_SCHEMES = Set.of("http", "https",
"netty-http", "undertow", "vertx-http");
- private static final Set<String> GENERAL_KEYS = Set.of("at", "rate",
"until", "from", "expect", "within");
- private static final Set<String> ERROR_KEYS = Set.of("exception",
"message", "status", "body", "after");
+ // the options of a rule besides the parameters of its fault: all of them
for the faults at a hook, and those of a
+ // stall or lifecycle rule, which acts on its route at no hook
+ static final Set<String> GENERAL_KEYS = Set.of("at", "rate", "until",
"from", "expect", "within");
+ static final Set<String> LIFECYCLE_KEYS = Set.of("from", "expect",
"within");
+ // the prefix of the response headers of error status=:
header.<name>=<value>
+ static final String HEADER_PREFIX = "header.";
+ private static final Set<String> ERROR_KEYS
+ = Set.of("exception", "message", "status", "body", "after",
HEADER_PREFIX);
// the most times a lifecycle rule acts (repeat=)
static final int MAX_REPEAT = 100;
- private static final String HEADER_PREFIX = "header.";
// the parameters of every fault, to tell a misspelt target from a
parameter
private static final Set<String> FAULT_KEYS
= Set.of("exception", "message", "status", "body", "after",
"duration", "mode", "header", "action", "repeat",
@@ -72,6 +76,36 @@ final class ChaosRuleParser {
private ChaosRuleParser() {
}
+ /**
+ * The parameters of a fault, as the parser accepts them ({@code header.}
stands for the {@code header.<name>}
+ * parameters of {@code error}). The chaos model ({@link ChaosModel}) is
built from it.
+ *
+ * @param stall for {@code Fault.LIFECYCLE}: the rule says stall, which
takes no action
+ */
+ static Set<String> faultKeys(Fault fault, boolean stall) {
+ return switch (fault) {
+ case ERROR -> ERROR_KEYS;
+ case DELAY -> Set.of("duration");
+ case DROP -> Set.of();
+ case CORRUPT -> Set.of("mode", "header");
+ case LIFECYCLE -> stall ? Set.of("duration", "repeat", "every") :
Set.of("action", "duration", "repeat", "every");
+ };
+ }
+
+ /** The options a rule with the fault takes besides the parameters of the
fault. */
+ static Set<String> optionKeys(Fault fault) {
+ return fault == Fault.LIFECYCLE ? LIFECYCLE_KEYS : GENERAL_KEYS;
+ }
+
+ /**
+ * Whether a rule reaches the hook only with {@code at=} naming it: the
send of the dead letter channel, so
+ * {@code endpoint=*} does not lose the messages that the dead letter
channel should keep, and the reply, so a rule
+ * fails a send before the producer is called unless it says otherwise.
+ */
+ static boolean onlyWithAt(Hook hook) {
+ return hook == Hook.DLQ || hook == Hook.REPLY;
+ }
+
/**
* Splits the rules of {@code camel.chaos.rules}, which are separated by
{@code ;}. A {@code ;} inside a quoted
* value does not separate; empty parts are left out.
@@ -215,7 +249,7 @@ final class ChaosRuleParser {
}
}
case DELAY -> {
- checkKeys(params, Set.of("duration"), b.fault);
+ checkKeys(params, faultKeys(Fault.DELAY, false), b.fault);
String duration = params.get("duration");
if (duration == null) {
throw new IllegalArgumentException(
@@ -243,9 +277,9 @@ final class ChaosRuleParser {
checkMax("duration", duration, b.duration, maxDuration);
}
}
- case DROP -> checkKeys(params, Set.of(), b.fault);
+ case DROP -> checkKeys(params, faultKeys(Fault.DROP, false),
b.fault);
case CORRUPT -> {
- checkKeys(params, Set.of("mode", "header"), b.fault);
+ checkKeys(params, faultKeys(Fault.CORRUPT, false), b.fault);
String mode = params.get("mode");
if (mode == null) {
throw new IllegalArgumentException("the fault corrupt
needs mode=<mode> (" + MODES + ")");
@@ -452,9 +486,7 @@ final class ChaosRuleParser {
* The parameters of {@code error}. Returns whether the error is the
failure of an HTTP component.
*/
private static boolean errorParameters(ChaosRule.Builder b, Map<String,
String> params, ClassResolver classResolver) {
- Set<String> keys = new HashSet<>(ERROR_KEYS);
- keys.add(HEADER_PREFIX);
- checkKeys(params, keys, Fault.ERROR);
+ checkKeys(params, faultKeys(Fault.ERROR, false), Fault.ERROR);
Map<String, String> headers = new LinkedHashMap<>();
params.forEach((k, v) -> {
@@ -561,8 +593,7 @@ final class ChaosRuleParser {
"stall is short for lifecycle action=suspend, so it takes
no action=; use lifecycle action=...");
}
// at, rate and until are rejected above
- checkKeys(params, b.stall ? Set.of("duration", "repeat", "every") :
Set.of("action", "duration", "repeat", "every"),
- word, Set.of("from", "expect", "within"));
+ checkKeys(params, faultKeys(Fault.LIFECYCLE, b.stall), word,
optionKeys(Fault.LIFECYCLE));
if (b.target != Target.ROUTE) {
throw new IllegalArgumentException(
"the fault " + word + " acts on a route, so it takes only
a route=<id> target, not " + b.target.key
@@ -706,11 +737,7 @@ final class ChaosRuleParser {
}
Set<Hook> hooks = EnumSet.noneOf(Hook.class);
for (Map.Entry<Hook, Set<Fault>> entry : offered.entrySet()) {
- // the send of the dead letter channel is reached only with
at=dlq, so endpoint=* does not lose the messages
- // that the dead letter channel should keep; the reply only with
at=reply, so a rule fails a send before the
- // producer is called unless it says otherwise
- boolean asked = b.at == null
- ? entry.getKey() != Hook.DLQ && entry.getKey() !=
Hook.REPLY : b.at == entry.getKey();
+ boolean asked = b.at == null ? !onlyWithAt(entry.getKey()) : b.at
== entry.getKey();
if (asked && entry.getValue().contains(b.fault)) {
hooks.add(entry.getKey());
}
diff --git
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosModelTest.java
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosModelTest.java
new file mode 100644
index 000000000000..111d5db2d4ff
--- /dev/null
+++
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosModelTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.camel.component.chaos.ChaosModel.Placeholder;
+import org.apache.camel.component.chaos.ChaosModel.Recipe;
+import org.apache.camel.component.chaos.ChaosRule.Hook;
+import org.apache.camel.component.chaos.ChaosRule.Target;
+import org.apache.camel.impl.engine.DefaultClassResolver;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The model of the chaos rules ({@link ChaosModel}): the build wrote it, it
agrees with the parser, and every recipe
+ * parses.
+ */
+class ChaosModelTest {
+
+ private static final Path GENERATED =
Path.of("src/generated/resources").resolve(ChaosModel.FILE);
+ private static final Pattern PLACEHOLDER =
Pattern.compile("\\$\\{([a-zA-Z]+)}");
+ private static final long MAX = TimeUnit.MINUTES.toMillis(10);
+
+ @Test
+ void theBuildWroteTheModel() throws Exception {
+ // the generate-chaos-model goal of camel-package-maven-plugin writes
it into the jar and src/generated
+ try (InputStream in =
getClass().getClassLoader().getResourceAsStream(ChaosModel.FILE)) {
+ assertThat(in).isNotNull();
+ assertThat(new String(in.readAllBytes(),
StandardCharsets.UTF_8)).isEqualTo(ChaosModel.toJson());
+ }
+ assertThat(Files.readString(GENERATED,
StandardCharsets.UTF_8)).isEqualTo(ChaosModel.toJson());
+ }
+
+ @Test
+ void theModelIsValidJsonWithTheChaosKey() throws Exception {
+ JsonObject root = (JsonObject) Jsoner.deserialize(ChaosModel.toJson());
+ assertThat(root).containsOnlyKeys("chaos");
+ assertThat(chaos()).containsKeys("targets", "hooks", "faults",
"options", "expectation", "limits", "recipes");
+ }
+
+ @Test
+ void theHooksAndFaultsAreThoseOfTheParser() {
+ JsonArray hooks = (JsonArray) chaos().get("hooks");
+ assertThat(hooks).hasSize(Hook.values().length);
+ for (Object o : hooks) {
+ JsonObject hook = (JsonObject) o;
+ Hook h = hook(hook.getString("name"));
+ assertThat(hook.getBoolean("onlyWithAt")).isEqualTo(h == Hook.DLQ
|| h == Hook.REPLY);
+ for (Object t : (JsonArray) hook.get("targets")) {
+ Target target = target((String) t);
+ Set<String> faults = new HashSet<>();
+ ChaosRuleParser.offered(target).get(h).forEach(f ->
faults.add(f.key));
+ assertThat(new HashSet<>((JsonArray)
hook.get("faults"))).containsAll(faults);
+ }
+ }
+ assertThat(names("faults")).containsExactly("error", "delay", "drop",
"corrupt", "lifecycle", "stall");
+ JsonObject stall = byName("faults", "stall");
+ assertThat(stall.getString("shortFor")).isEqualTo("lifecycle
action=suspend");
+ assertThat(paramNames(stall)).containsExactly("duration", "every",
"repeat");
+ assertThat(paramNames(byName("faults",
"lifecycle"))).containsExactly("action", "duration", "every", "repeat");
+ assertThat(paramNames(byName("faults", "error")))
+ .containsExactly("after", "body", "exception",
"header.<name>", "message", "status");
+ assertThat(strings(byName("faults",
"lifecycle").get("options"))).containsExactly("expect", "from", "within");
+ assertThat(strings(byName("faults",
"error").get("hooks"))).containsExactly("send", "reply", "dlq", "poll", "step");
+ assertThat(names("options")).containsExactly("at", "expect", "from",
"rate", "until", "within");
+ }
+
+ @Test
+ void theRecipesHaveUniqueIdsAndTheirPlaceholders() {
+ Set<String> ids = new HashSet<>();
+ for (Recipe recipe : ChaosModel.recipeList()) {
+ assertThat(ids.add(recipe.id())).as("recipe id %s",
recipe.id()).isTrue();
+ Set<String> used = new LinkedHashSet<>();
+ Matcher m = PLACEHOLDER.matcher(recipe.rule());
+ while (m.find()) {
+ used.add(m.group(1));
+ }
+ Set<String> declared = new LinkedHashSet<>();
+ recipe.placeholders().forEach(p -> declared.add(p.name()));
+ assertThat(declared).as("the placeholders of recipe %s",
recipe.id()).isEqualTo(used);
+ }
+ }
+
+ static List<Recipe> recipes() {
+ return ChaosModel.recipeList();
+ }
+
+ @ParameterizedTest
+ @MethodSource("recipes")
+ void everyRecipeParsesWithItsExamplesAndDefaults(Recipe recipe) {
+ // the examples
+ ChaosRuleParser.parse(fill(recipe, false), new DefaultClassResolver(),
MAX);
+ // the defaults, with the examples where there is no default
+ ChaosRuleParser.parse(fill(recipe, true), new DefaultClassResolver(),
MAX);
+ }
+
+ @Test
+ void theModeOfACorruptRecipeOffersEveryMode() {
+ Recipe recipe = ChaosModel.recipeList().stream().filter(r ->
r.id().equals("corrupt-entry")).findFirst()
+ .orElseThrow();
+ Placeholder mode = recipe.placeholders().stream().filter(p ->
p.name().equals("mode")).findFirst().orElseThrow();
+ for (Object value : (JsonArray) mode.pick().get("values")) {
+ String rule = fill(recipe, true).replace("mode=headers", "mode=" +
value);
+ ChaosRuleParser.parse(rule, new DefaultClassResolver(), MAX);
+ }
+ }
+
+ private static String fill(Recipe recipe, boolean defaults) {
+ String rule = recipe.rule();
+ for (Placeholder p : recipe.placeholders()) {
+ String value = defaults && p.defaultValue() != null ?
p.defaultValue() : p.example();
+ rule = rule.replace("${" + p.name() + "}", value);
+ }
+ assertThat(rule).doesNotContain("${");
+ return rule;
+ }
+
+ private static JsonObject chaos() {
+ return (JsonObject) ChaosModel.model().get("chaos");
+ }
+
+ private static List<String> names(String key) {
+ return ((JsonArray) chaos().get(key)).stream().map(o -> ((JsonObject)
o).getString("name")).toList();
+ }
+
+ private static JsonObject byName(String key, String name) {
+ return (JsonObject) ((JsonArray) chaos().get(key)).stream()
+ .filter(o -> name.equals(((JsonObject)
o).getString("name"))).findFirst().orElseThrow();
+ }
+
+ private static List<String> paramNames(JsonObject fault) {
+ return ((JsonArray) fault.get("parameters")).stream().map(o ->
((Map<?, ?>) o).get("name").toString()).toList();
+ }
+
+ private static List<String> strings(Object array) {
+ return ((List<?>) array).stream().map(Object::toString).toList();
+ }
+
+ private static Hook hook(String key) {
+ for (Hook h : Hook.values()) {
+ if (h.key.equals(key)) {
+ return h;
+ }
+ }
+ throw new AssertionError("No hook " + key);
+ }
+
+ private static Target target(String key) {
+ for (Target t : Target.values()) {
+ if (t.key.equals(key)) {
+ return t;
+ }
+ }
+ throw new AssertionError("No target " + key);
+ }
+}
diff --git a/docs/user-manual/modules/ROOT/pages/camel-catalog.adoc
b/docs/user-manual/modules/ROOT/pages/camel-catalog.adoc
index 5b231a985486..7dc5f8090f11 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-catalog.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-catalog.adoc
@@ -48,6 +48,7 @@ org
└── camel
└── catalog
├── beans (JSON schema)
+ ├── chaos-model.json (the model of the chaos rules)
├── components (JSON schema)
├── dataformats (JSON schema)
├── dev-consoles (JSON schema)
@@ -77,6 +78,11 @@ covering every dev console endpoint, so the same contract
used internally to ser
dev console is also available standalone for tooling. A
`dev-consoles.properties` index file lists
all available dev console names.
+The `chaos-model.json` file, since Camel 4.24, is the model of the rules of
+xref:next@components:others:chaos.adoc[camel-chaos]: the targets, the hooks of
each target and the faults each one
+offers, the parameters of each fault, the options, the expectations, the
limits, and recipes (known rules in plain
+words, as templates). It is for the tools that help to write rules, such as a
form or an AI.
+
The `docs` directory contains the AsciiDoc documentation pages for all
components, EIPs,
data formats, languages, and other artifacts. A `docs.properties` index file
lists all
available documentation names.
@@ -244,4 +250,7 @@ String summary = catalog.listDevConsolesAsJson();
// the OpenAPI 3.0 specification aggregated across every dev console endpoint
String openApiSpec = catalog.devConsolesOpenApiSpec();
+
+// the model of the chaos rules of camel-chaos
+String chaosModel = catalog.chaosModelJSon();
----
diff --git
a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/GenerateChaosModelMojo.java
b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/GenerateChaosModelMojo.java
new file mode 100644
index 000000000000..69b458ed4235
--- /dev/null
+++
b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/GenerateChaosModelMojo.java
@@ -0,0 +1,76 @@
+/*
+ * 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.maven.packaging;
+
+import java.io.File;
+import java.lang.reflect.Method;
+
+import javax.inject.Inject;
+
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.apache.maven.project.MavenProjectHelper;
+import org.codehaus.plexus.build.BuildContext;
+
+/**
+ * Generates the model of the chaos rules of camel-chaos ({@code
META-INF/org/apache/camel/chaos/chaos-model.json}): the
+ * targets, hooks, faults, parameters, options and recipes, for the tools that
help to write rules. The model is made by
+ * {@code ChaosModel} of camel-chaos from the tables of its rule parser, so
this goal loads that class from the compiled
+ * classes of the module and writes what its {@code toJson()} returns, into
{@code src/generated/resources} and the
+ * output directory. The catalog copies the file ({@code
CamelCatalog.chaosModelJSon()}).
+ */
+@Mojo(name = "generate-chaos-model", threadSafe = true, defaultPhase =
LifecyclePhase.PROCESS_CLASSES,
+ requiresDependencyResolution = ResolutionScope.COMPILE)
+public class GenerateChaosModelMojo extends AbstractGeneratorMojo {
+
+ /** Where the model is, under the resources directories. */
+ public static final String FILE =
"META-INF/org/apache/camel/chaos/chaos-model.json";
+
+ /** The class that makes the model: a static {@code toJson()} that returns
it. */
+ @Parameter(defaultValue = "org.apache.camel.component.chaos.ChaosModel")
+ protected String modelClass;
+
+ /** The output directory for the generated resources. */
+ @Parameter(defaultValue = "${project.basedir}/src/generated/resources")
+ protected File resourcesOutputDir;
+
+ @Inject
+ public GenerateChaosModelMojo(MavenProjectHelper projectHelper,
BuildContext buildContext) {
+ super(projectHelper, buildContext);
+ }
+
+ @Override
+ public void execute() throws MojoExecutionException, MojoFailureException {
+ String json;
+ try {
+ Class<?> type = getProjectClassLoader().loadClass(modelClass);
+ Method toJson = type.getDeclaredMethod("toJson");
+ // the model is internal to camel-chaos (package-private)
+ toJson.setAccessible(true);
+ json = (String) toJson.invoke(null);
+ } catch (ReflectiveOperationException | LinkageError e) {
+ throw new MojoExecutionException("Cannot make the chaos model with
" + modelClass + ".toJson()", e);
+ }
+ if (updateResource(resourcesOutputDir.toPath(), FILE, json)) {
+ getLog().info("Updated the chaos model: " + FILE);
+ }
+ }
+}
diff --git
a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCatalogMojo.java
b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCatalogMojo.java
index 2fcac3ef4933..d286557b733d 100644
---
a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCatalogMojo.java
+++
b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCatalogMojo.java
@@ -437,6 +437,7 @@ public class PrepareCatalogMojo extends AbstractMojo {
Set<String> beans = executeBeans();
executeApis();
Set<String> consoles = executeDevConsoles();
+ executeChaosModel();
Set<String> others = executeOthers();
executeDocuments(components, dataformats, languages, others);
executeDocs();
@@ -917,6 +918,22 @@ public class PrepareCatalogMojo extends AbstractMojo {
return apiNames;
}
+ /**
+ * Copies the model of the chaos rules of camel-chaos into the catalog
(CamelCatalog.chaosModelJSon()).
+ */
+ protected void executeChaosModel() throws Exception {
+ List<Path> models = allJsonFiles.stream()
+ .filter(p ->
p.endsWith("META-INF/org/apache/camel/chaos/chaos-model.json"))
+ .toList();
+ if (models.size() != 1) {
+ getLog().warn("Expected one chaos-model.json (camel-chaos), found
" + models.size());
+ return;
+ }
+ Path target = consolesOutDir.toPath().resolve("../chaos-model.json");
+ FileUtil.updateFile(target, Files.readString(models.get(0)));
+ getLog().info("Copied the chaos model of camel-chaos");
+ }
+
protected Set<String> executeDevConsoles() throws Exception {
Path consolesOutDir = this.consolesOutDir.toPath();