This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch wiretap-hand-off in repository https://gitbox.apache.org/repos/asf/camel.git
commit 70a2297ce995353b498565334749b6a66ecd1bd3 Author: Claus Ibsen <[email protected]> AuthorDate: Mon Nov 9 23:19:38 2026 +0100 chore: camel-jbang - a route reached by a wireTap is handed off to, not called A wireTap sends a copy of the message on another thread: the route goes on without waiting for the tapped route, and does not get its failures back. The route readers recorded a wireTap like a to, so a wireTap to a direct endpoint was a call (↳ in About, "calls" in the summary). They now mark the endpoint a wireTap sends to, and its link is a hand-off; a route that also calls it with a to still calls it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[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 | 26 ++++++++-- .../core/commands/ai/ProjectOverviewTest.java | 60 ++++++++++++++++++++++ .../src/main/resources/tui/help/overview.md | 2 +- 6 files changed, 98 insertions(+), 12 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..8514869af540 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); 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..40d1bd330003 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,66 @@ 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 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
