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 28815807cc3d chore: camel-jbang - a route reached by a wireTap is
handed off to, not called (#29520)
28815807cc3d is described below
commit 28815807cc3d5392691f7396b4634955ea638614
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 10 07:46:36 2026 +0100
chore: camel-jbang - a route reached by a wireTap is handed off to, not
called (#29520)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../ROOT/pages/camel-jbang-tui-observe.adoc | 2 +-
.../jbang/core/commands/ai/JavaRouteReader.java | 6 +-
.../jbang/core/commands/ai/ProjectOverview.java | 14 +++-
.../dsl/jbang/core/commands/ai/ProjectRoutes.java | 29 ++++++--
.../core/commands/ai/ProjectOverviewTest.java | 82 ++++++++++++++++++++++
.../src/main/resources/tui/help/overview.md | 2 +-
6 files changed, 122 insertions(+), 13 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
index e72d184e60fc..ab7c0d28f6f1 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
@@ -28,7 +28,7 @@ image::jbang/camel-tui-about.png[About the chaos demo: its
purpose, what comes i
main source file (a YAML `#` comment, a Javadoc or `//` comment in Java, an
XML comment), else the descriptions of the
routes.
* *In*, *Routes* and *Out* say what starts it, its routes (a route called with
`direct` sits under its caller with `↳`,
-one handed off with `seda` with `⇢`) and what it talks to, with the exchanges
since it started and a sparkline of the
+one handed off with `seda` or a `wireTap` with `⇢`) and what it talks to, with
the exchanges since it started and a sparkline of the
last 5 minutes. *Up* and *Down* select a line, and *Enter* opens a route
drilled down on the Diagram, or an endpoint with
its details on the Endpoint tab.
* *Last 5m* has the exchanges and how they ended, with the cause that happened
most, and *What happened* a strip per route
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JavaRouteReader.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JavaRouteReader.java
index d9cfdab05f0e..6e0fac5ef8a6 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JavaRouteReader.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JavaRouteReader.java
@@ -54,6 +54,7 @@ import org.apache.camel.model.SwitchCaseDefinition;
import org.apache.camel.model.SwitchDefinition;
import org.apache.camel.model.ToDynamicDefinition;
import org.apache.camel.model.TryDefinition;
+import org.apache.camel.model.WireTapDefinition;
import org.apache.camel.model.errorhandler.DeadLetterChannelDefinition;
import org.apache.camel.model.errorhandler.NoErrorHandlerDefinition;
import org.apache.camel.model.language.ConstantExpression;
@@ -236,8 +237,9 @@ final class JavaRouteReader {
return;
}
boolean error = onError || p instanceof CatchDefinition || p
instanceof OnExceptionDefinition;
- if (p instanceof ToDynamicDefinition d) {
- // toD and wireTap
+ if (p instanceof WireTapDefinition<?> w) {
+ add(produces, mark(ProjectRoutes.endpoint(w.getUri(), null, true,
catalog).asTapped(), error));
+ } else if (p instanceof ToDynamicDefinition d) {
add(produces, mark(ProjectRoutes.endpoint(d.getUri(), null, true,
catalog), error));
} else if (p instanceof SendDefinition<?> s) {
add(produces, mark(ProjectRoutes.endpoint(s.getUri(), null, false,
catalog), error));
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverview.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverview.java
index 611e8a901ec7..a75c786b7553 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverview.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverview.java
@@ -296,11 +296,19 @@ public final class ProjectOverview {
continue;
}
String id = r.key() + "->" + target.key() + "@" + e.key();
+ String kind = linkKind(e.scheme(), catalog);
+ if (e.tapped() && "call".equals(kind)) {
+ // a wireTap sends a copy on another thread: the route
does not wait, nor get its failures
+ kind = "async";
+ }
Link known = links.get(id);
if (known == null) {
- links.put(id, new Link(r.key(), target.key(), e.uri(),
linkKind(e.scheme(), catalog), e.onError()));
- } else if (known.onError() && !e.onError()) {
- links.put(id, new Link(known.from(), known.to(),
known.endpoint(), known.kind(), false));
+ links.put(id, new Link(r.key(), target.key(), e.uri(),
kind, e.onError()));
+ } else if (known.onError() && !e.onError() ||
"call".equals(kind) && !"call".equals(known.kind())) {
+ // sent to outside failure handling too, or called as
well as tapped
+ links.put(id, new Link(
+ known.from(), known.to(), known.endpoint(),
+ "call".equals(kind) ? kind : known.kind(),
known.onError() && e.onError()));
}
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
index 68a831ab5bad..16575063aa77 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
@@ -104,16 +104,27 @@ public final class ProjectRoutes {
private ProjectRoutes() {
}
- /** An endpoint a route uses, as written, with its identity for matching
the other end. */
- public record Endpoint(String uri, String scheme, String key, boolean
dynamic, String label, boolean onError) {
+ /**
+ * An endpoint a route uses, as written, with its identity for matching
the other end.
+ *
+ * @param onError sent to while handling a failure
+ * @param tapped sent a copy of the message by a wireTap: the route goes
on without waiting for it
+ */
+ public record Endpoint(String uri, String scheme, String key, boolean
dynamic, String label, boolean onError,
+ boolean tapped) {
public Endpoint(String uri, String scheme, String key, boolean
dynamic, String label) {
- this(uri, scheme, key, dynamic, label, false);
+ this(uri, scheme, key, dynamic, label, false, false);
}
/** The same endpoint, sent to while handling a failure (doCatch,
onException, a dead letter channel). */
Endpoint asOnError() {
- return onError ? this : new Endpoint(uri, scheme, key, dynamic,
label, true);
+ return onError ? this : new Endpoint(uri, scheme, key, dynamic,
label, true, tapped);
+ }
+
+ /** The same endpoint, sent a copy of the message by a wireTap. */
+ Endpoint asTapped() {
+ return tapped ? this : new Endpoint(uri, scheme, key, dynamic,
label, onError, true);
}
}
@@ -756,7 +767,7 @@ public final class ProjectRoutes {
// enrich with an expression: known only at runtime
e = dynamicEndpoint(key);
}
- add(POLL_KEYS.contains(key) ? consumes : sent, e);
+ add(POLL_KEYS.contains(key) ? consumes : sent,
"wireTap".equals(key) ? tappedOf(e) : e);
} else if ("kamelet".equals(key)) {
String name = value instanceof ScalarNode s ?
s.getValue()
: value instanceof MappingNode km ?
str(value(km, "name")) : null;
@@ -880,6 +891,10 @@ public final class ProjectRoutes {
return e != null ? e.asOnError() : null;
}
+ private static Endpoint tappedOf(Endpoint e) {
+ return e != null ? e.asTapped() : null;
+ }
+
private static void add(List<Endpoint> list, Endpoint e) {
if (e != null) {
list.add(e);
@@ -957,6 +972,7 @@ public final class ProjectRoutes {
if (PRODUCER_KEYS.contains(name)) {
String uri = uriOrConstant(s);
Endpoint ep = uri != null ? endpoint(uri, null,
!"to".equals(name), catalog) : dynamicEndpoint(name);
+ ep = "wireTap".equals(name) ? tappedOf(ep) : ep;
add(produces, handlesFailure(s, e) ? onErrorOf(ep) : ep);
} else if (POLL_KEYS.contains(name)) {
String uri = uriOrConstant(s);
@@ -1055,7 +1071,8 @@ public final class ProjectRoutes {
for (int i = 0; i < all.getLength(); i++) {
Element s = (Element) all.item(i);
if (PRODUCER_KEYS.contains(localName(s)) && attr(s, "uri") !=
null) {
- add(produces, endpoint(attr(s, "uri"), null, false,
catalog));
+ Endpoint ep = endpoint(attr(s, "uri"), null, false,
catalog);
+ add(produces, "wireTap".equals(localName(s)) ?
tappedOf(ep) : ep);
} else if (attr(s, "deadLetterUri") != null) {
add(produces, endpoint(attr(s, "deadLetterUri"), null,
false, catalog));
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverviewTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverviewTest.java
index ae452c58fa4a..580fcda42ca0 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverviewTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverviewTest.java
@@ -216,6 +216,88 @@ class ProjectOverviewTest {
}
}
+ @Test
+ void aWireTapHandsOffInEveryDsl() {
+ // a wireTap sends a copy on another thread: the route does not wait
for the tapped one, nor get its failures
+ Map<String, String> sources = Map.of(
+ "orders.camel.yaml", """
+ - route:
+ id: yaml-orders
+ from:
+ uri: direct:yaml
+ steps:
+ - wireTap:
+ uri: direct:audit
+ """,
+ "orders.xml", """
+ <routes xmlns="http://camel.apache.org/schema/xml-io">
+ <route id="xml-orders">
+ <from uri="direct:xml"/>
+ <wireTap uri="direct:audit"/>
+ </route>
+ </routes>
+ """,
+ "Orders.java", """
+ public class Orders extends RouteBuilder {
+ public void configure() {
+ from("direct:java").routeId("java-orders")
+ .wireTap("direct:audit");
+ }
+ }
+ """,
+ "both.camel.yaml", """
+ - route:
+ id: tapped-and-called
+ from:
+ uri: direct:both
+ steps:
+ - wireTap:
+ uri: direct:audit
+ - to:
+ uri: direct:audit
+ """,
+ "audit.camel.yaml", """
+ - route:
+ id: audit
+ from:
+ uri: direct:audit
+ steps:
+ - to:
+ uri: log:audit
+ """);
+ Overview o = ProjectOverview.analyze(Path.of("orders"), sources,
CATALOG);
+ for (String id : List.of("yaml-orders", "xml-orders", "java-orders")) {
+ assertThat(o.route(id).produces()).as(id).singleElement()
+ .satisfies(e -> assertThat(e.tapped()).isTrue());
+ assertThat(o.links()).as(id).contains(new Link(id, "audit",
"direct:audit", "async"));
+ }
+ // a route that also calls it waits for it
+ assertThat(o.links()).contains(new Link("tapped-and-called", "audit",
"direct:audit", "call"))
+ .doesNotContain(new Link("tapped-and-called", "audit",
"direct:audit", "async"));
+ }
+
+ @Test
+ void aWireTapInAnXmlErrorHandlerHandsOff() {
+ String xml = """
+ <routes xmlns="http://camel.apache.org/schema/xml-io">
+ <onException>
+ <exception>java.lang.Exception</exception>
+ <wireTap uri="direct:alert"/>
+ <to uri="direct:parking"/>
+ </onException>
+ <route id="orders">
+ <from uri="direct:orders"/>
+ <to uri="log:orders"/>
+ </route>
+ </routes>
+ """;
+ Overview o = ProjectOverview.analyze(Path.of("orders"),
Map.of("orders.xml", xml), CATALOG);
+ Route handler = o.routes().stream().filter(r ->
"errorHandler".equals(r.kind())).findFirst().orElseThrow();
+ assertThat(handler.produces()).allSatisfy(e ->
assertThat(e.onError()).isTrue());
+
assertThat(handler.produces()).filteredOn(ProjectRoutes.Endpoint::tapped)
+
.extracting(ProjectRoutes.Endpoint::uri).containsExactly("direct:alert");
+ }
+
@Test
void linksRoutesOverSharedEndpoints() {
List<Link> links = overview().links();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
index 2ea670c691f2..a7e9943d64dd 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
@@ -21,7 +21,7 @@ happened to it, with no AI:
descriptions of its routes.
- **In, Routes, Out** — what starts it (timers, HTTP, consumers), its
routes (a route called with `direct:` sits under its caller with `↳`,
- one handed off with `seda:` with `⇢`) and what it talks to, from its
+ one handed off with `seda:` or a `wireTap` with `⇢`) and what it talks to,
from its
sources. Each line has the exchanges since it started (for an
endpoint, the sends to it), and In and Out a sparkline of the last 5
minutes with the rate now, or `idle`. Out also has the systems the