This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch CAMEL-23656/route-topology-service
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 7a222b7d87236bcf2e2f1a8bf0b6caecda642f42
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Jun 1 13:39:30 2026 +0200

    CAMEL-23656: Add route topology service to compute inter-route relationships
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../apache/camel/catalog/dev-consoles.properties   |   1 +
 .../camel/catalog/dev-consoles/route-topology.json |  15 ++
 .../org/apache/camel/spi/RouteTopologyDumper.java  |  77 +++++++++
 .../camel/impl/engine/AbstractCamelContext.java    |   9 +
 .../apache/camel/dev-console/route-topology.json   |  15 ++
 .../org/apache/camel/dev-console/route-topology    |   2 +
 .../org/apache/camel/dev-consoles.properties       |   2 +-
 .../impl/console/RouteTopologyDevConsole.java      |  97 +++++++++++
 .../impl/console/RouteBasedDevConsoleTest.java     |   1 +
 .../impl/console/RouteTopologyDevConsoleTest.java  | 183 +++++++++++++++++++++
 .../org/apache/camel/route-topology-dumper         |   2 +
 .../camel/impl/DefaultRouteTopologyDumper.java     |  89 ++++++++++
 .../org/apache/camel/support/PluginHelper.java     |  15 ++
 .../camel-jbang-cmd-route-topology.adoc            |  26 +++
 .../ROOT/pages/jbang-commands/camel-jbang-cmd.adoc |   1 +
 .../camel/cli/connector/LocalCliConnector.java     |  14 ++
 .../META-INF/camel-jbang-commands-metadata.json    |   2 +-
 .../dsl/jbang/core/commands/CamelJBangMain.java    |   1 +
 .../commands/action/CamelRouteTopologyAction.java  | 114 +++++++++++++
 19 files changed, 664 insertions(+), 2 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
index 9b3a109b8baa..8804844e3cf7 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
@@ -48,6 +48,7 @@ route-diagram
 route-dump
 route-group
 route-structure
+route-topology
 send
 service
 sftp
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route-topology.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route-topology.json
new file mode 100644
index 000000000000..738a98765ec9
--- /dev/null
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route-topology.json
@@ -0,0 +1,15 @@
+{
+  "console": {
+    "kind": "console",
+    "group": "camel",
+    "name": "route-topology",
+    "title": "Route Topology",
+    "description": "Route topology showing inter-route connections",
+    "deprecated": false,
+    "javaType": "org.apache.camel.impl.console.RouteTopologyDevConsole",
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-console",
+    "version": "4.21.0-SNAPSHOT"
+  }
+}
+
diff --git 
a/core/camel-api/src/main/java/org/apache/camel/spi/RouteTopologyDumper.java 
b/core/camel-api/src/main/java/org/apache/camel/spi/RouteTopologyDumper.java
new file mode 100644
index 000000000000..d7d6af83f953
--- /dev/null
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/RouteTopologyDumper.java
@@ -0,0 +1,77 @@
+/*
+ * 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.spi;
+
+import java.util.List;
+
+import org.apache.camel.CamelContext;
+
+/**
+ * SPI for computing inter-route topology, showing how routes connect to each 
other through shared endpoints.
+ *
+ * @since 4.21
+ */
+public interface RouteTopologyDumper {
+
+    /**
+     * Service factory key.
+     */
+    String FACTORY = "route-topology-dumper";
+
+    /**
+     * A node in the topology representing a route.
+     *
+     * @param routeId    the route id
+     * @param from       the input endpoint URI (scheme:context-path, query 
parameters stripped)
+     * @param fromScheme the component scheme of the input endpoint
+     * @param nodeType   the type of node: "route" for regular routes, 
"trigger" for timer/quartz/cron/scheduler
+     * @since            4.21
+     */
+    record TopologyNode(String routeId, String from, String fromScheme, String 
nodeType) {
+    }
+
+    /**
+     * An edge in the topology representing a connection between two routes 
via a shared endpoint.
+     *
+     * @param fromRouteId    the source route id (the route that sends to the 
endpoint)
+     * @param toRouteId      the target route id (the route that consumes from 
the endpoint)
+     * @param endpoint       the shared endpoint URI (scheme:context-path, 
query parameters stripped)
+     * @param connectionType the type of connection: "internal" for 
direct/seda, "external" for remote components
+     * @since                4.21
+     */
+    record TopologyEdge(String fromRouteId, String toRouteId, String endpoint, 
String connectionType) {
+    }
+
+    /**
+     * The result of computing route topology.
+     *
+     * @param nodes the route nodes
+     * @param edges the connections between routes
+     * @since       4.21
+     */
+    record TopologyResult(List<TopologyNode> nodes, List<TopologyEdge> edges) {
+    }
+
+    /**
+     * Computes the inter-route topology by analyzing route definitions and 
matching endpoints across routes.
+     *
+     * @param  context the CamelContext
+     * @return         the topology result with nodes and edges
+     */
+    TopologyResult dumpTopology(CamelContext context);
+
+}
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
index ceb556bfae20..d2b056f2ff58 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
@@ -168,6 +168,7 @@ import org.apache.camel.spi.RouteFactory;
 import org.apache.camel.spi.RoutePolicyFactory;
 import org.apache.camel.spi.RouteStartupOrder;
 import org.apache.camel.spi.RouteTemplateParameterSource;
+import org.apache.camel.spi.RouteTopologyDumper;
 import org.apache.camel.spi.RoutesLoader;
 import org.apache.camel.spi.RuntimeEndpointRegistry;
 import org.apache.camel.spi.ShutdownStrategy;
@@ -414,6 +415,7 @@ public abstract class AbstractCamelContext extends 
BaseService
         
camelContextExtension.lazyAddContextPlugin(SimpleFunctionRegistry.class, 
this::createSimpleFunctionRegistry);
         camelContextExtension.lazyAddContextPlugin(RestRegistry.class, 
this::createRestRegistry);
         camelContextExtension.lazyAddContextPlugin(RouteDiagramDumper.class, 
this::createRouteDiagramDumper);
+        camelContextExtension.lazyAddContextPlugin(RouteTopologyDumper.class, 
this::createRouteTopologyDumper);
     }
 
     protected static <T> T lookup(CamelContext context, String ref, Class<T> 
type) {
@@ -4391,6 +4393,13 @@ public abstract class AbstractCamelContext extends 
BaseService
                 "camel-diagram");
     }
 
+    protected RouteTopologyDumper createRouteTopologyDumper() {
+        return 
ResolverHelper.resolveMandatoryService(getCamelContextReference(),
+                RouteTopologyDumper.FACTORY,
+                RouteTopologyDumper.class,
+                "camel-core-engine");
+    }
+
     @Override
     public String getGlobalOption(String key) {
         String value = getGlobalOptions().get(key);
diff --git 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route-topology.json
 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route-topology.json
new file mode 100644
index 000000000000..738a98765ec9
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route-topology.json
@@ -0,0 +1,15 @@
+{
+  "console": {
+    "kind": "console",
+    "group": "camel",
+    "name": "route-topology",
+    "title": "Route Topology",
+    "description": "Route topology showing inter-route connections",
+    "deprecated": false,
+    "javaType": "org.apache.camel.impl.console.RouteTopologyDevConsole",
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-console",
+    "version": "4.21.0-SNAPSHOT"
+  }
+}
+
diff --git 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/route-topology
 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/route-topology
new file mode 100644
index 000000000000..1329fdb6c797
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/route-topology
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.impl.console.RouteTopologyDevConsole
diff --git 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
index 90006f59d203..84bbd4e88131 100644
--- 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
+++ 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
@@ -1,5 +1,5 @@
 # Generated by camel build tools - do NOT edit this file!
-dev-consoles=bean blocked browse circuit-breaker consumer context debug 
endpoint errors eval-language event gc health inflight internal-tasks 
java-security jvm log memory message-history processor producer properties 
receive reload rest rest-spec route route-controller route-dump route-group 
route-structure send service simple-language source startup-recorder 
system-properties thread top trace transformers type-converters variables
+dev-consoles=bean blocked browse circuit-breaker consumer context debug 
endpoint errors eval-language event gc health inflight internal-tasks 
java-security jvm log memory message-history processor producer properties 
receive reload rest rest-spec route route-controller route-dump route-group 
route-structure route-topology send service simple-language source 
startup-recorder system-properties thread top trace transformers 
type-converters variables
 groupId=org.apache.camel
 artifactId=camel-console
 version=4.21.0-SNAPSHOT
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteTopologyDevConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteTopologyDevConsole.java
new file mode 100644
index 000000000000..6dcb2c099193
--- /dev/null
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteTopologyDevConsole.java
@@ -0,0 +1,97 @@
+/*
+ * 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.impl.console;
+
+import java.util.Map;
+
+import org.apache.camel.spi.RouteTopologyDumper;
+import org.apache.camel.spi.RouteTopologyDumper.TopologyEdge;
+import org.apache.camel.spi.RouteTopologyDumper.TopologyNode;
+import org.apache.camel.spi.RouteTopologyDumper.TopologyResult;
+import org.apache.camel.spi.annotations.DevConsole;
+import org.apache.camel.support.PluginHelper;
+import org.apache.camel.support.console.AbstractDevConsole;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+@DevConsole(name = "route-topology", description = "Route topology showing 
inter-route connections")
+public class RouteTopologyDevConsole extends AbstractDevConsole {
+
+    public RouteTopologyDevConsole() {
+        super("camel", "route-topology", "Route Topology", "Route topology 
showing inter-route connections");
+    }
+
+    @Override
+    protected String doCallText(Map<String, Object> options) {
+        RouteTopologyDumper dumper = 
PluginHelper.getRouteTopologyDumper(getCamelContext());
+        if (dumper == null) {
+            return "";
+        }
+        TopologyResult result = dumper.dumpTopology(getCamelContext());
+
+        StringBuilder sb = new StringBuilder();
+        sb.append(String.format("Route Topology (%d routes, %d 
connections)%n%n",
+                result.nodes().size(), result.edges().size()));
+
+        for (TopologyNode node : result.nodes()) {
+            sb.append(String.format("  %s (%s) type=%s%n", node.routeId(), 
node.from(), node.nodeType()));
+
+            for (TopologyEdge edge : result.edges()) {
+                if (edge.fromRouteId().equals(node.routeId())) {
+                    sb.append(String.format("    --> %s via %s [%s]%n",
+                            edge.toRouteId(), edge.endpoint(), 
edge.connectionType()));
+                }
+            }
+        }
+        return sb.toString();
+    }
+
+    @Override
+    protected JsonObject doCallJson(Map<String, Object> options) {
+        JsonObject root = new JsonObject();
+        RouteTopologyDumper dumper = 
PluginHelper.getRouteTopologyDumper(getCamelContext());
+        if (dumper == null) {
+            return root;
+        }
+        TopologyResult result = dumper.dumpTopology(getCamelContext());
+
+        JsonArray nodesArr = new JsonArray();
+        for (TopologyNode node : result.nodes()) {
+            JsonObject jo = new JsonObject();
+            jo.put("routeId", node.routeId());
+            jo.put("from", node.from());
+            jo.put("fromScheme", node.fromScheme());
+            jo.put("nodeType", node.nodeType());
+            nodesArr.add(jo);
+        }
+        root.put("nodes", nodesArr);
+
+        JsonArray edgesArr = new JsonArray();
+        for (TopologyEdge edge : result.edges()) {
+            JsonObject jo = new JsonObject();
+            jo.put("fromRouteId", edge.fromRouteId());
+            jo.put("toRouteId", edge.toRouteId());
+            jo.put("endpoint", edge.endpoint());
+            jo.put("connectionType", edge.connectionType());
+            edgesArr.add(jo);
+        }
+        root.put("edges", edgesArr);
+
+        return root;
+    }
+
+}
diff --git 
a/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteBasedDevConsoleTest.java
 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteBasedDevConsoleTest.java
index 18aed23ef61b..a2399c08aec7 100644
--- 
a/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteBasedDevConsoleTest.java
+++ 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteBasedDevConsoleTest.java
@@ -60,6 +60,7 @@ public class RouteBasedDevConsoleTest extends 
AbstractDevConsoleTest {
                 Arguments.of("route-dump", "routes"),
                 Arguments.of("route-group", "routeGroups"),
                 Arguments.of("route-structure", "routes"),
+                Arguments.of("route-topology", "nodes"),
                 Arguments.of("source", "routes"));
     }
 
diff --git 
a/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteTopologyDevConsoleTest.java
 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteTopologyDevConsoleTest.java
new file mode 100644
index 000000000000..f97c87fdd974
--- /dev/null
+++ 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteTopologyDevConsoleTest.java
@@ -0,0 +1,183 @@
+/*
+ * 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.impl.console;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.console.DevConsole;
+import org.apache.camel.support.PluginHelper;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class RouteTopologyDevConsoleTest extends ContextTestSupport {
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("timer:tick").routeId("trigger")
+                        .to("direct:process");
+
+                from("direct:process").routeId("processor")
+                        .to("direct:validate")
+                        .to("mock:result");
+
+                from("direct:validate").routeId("validator")
+                        .to("mock:validated");
+
+                from("seda:loop").routeId("looper")
+                        .choice()
+                        .when(simple("${body} == 'retry'"))
+                        .to("seda:loop")
+                        .otherwise()
+                        .to("mock:done");
+
+                from("direct:isolated").routeId("isolated")
+                        .to("mock:nowhere");
+            }
+        };
+    }
+
+    @Test
+    void testConsoleExists() {
+        DevConsole console = 
PluginHelper.getDevConsoleResolver(context).resolveDevConsole("route-topology");
+        assertNotNull(console);
+        assertEquals("route-topology", console.getId());
+    }
+
+    @Test
+    void testJsonOutput() {
+        DevConsole console = 
PluginHelper.getDevConsoleResolver(context).resolveDevConsole("route-topology");
+        assertNotNull(console);
+
+        JsonObject json = (JsonObject) console.call(DevConsole.MediaType.JSON);
+        assertNotNull(json);
+
+        JsonArray nodes = (JsonArray) json.get("nodes");
+        assertNotNull(nodes);
+        assertEquals(5, nodes.size());
+
+        JsonArray edges = (JsonArray) json.get("edges");
+        assertNotNull(edges);
+        assertTrue(edges.size() >= 3);
+    }
+
+    @Test
+    void testTriggerNodeType() {
+        DevConsole console = 
PluginHelper.getDevConsoleResolver(context).resolveDevConsole("route-topology");
+        JsonObject json = (JsonObject) console.call(DevConsole.MediaType.JSON);
+        JsonArray nodes = (JsonArray) json.get("nodes");
+
+        JsonObject triggerNode = findNode(nodes, "trigger");
+        assertNotNull(triggerNode);
+        assertEquals("trigger", triggerNode.getString("nodeType"));
+        assertEquals("timer", triggerNode.getString("fromScheme"));
+
+        JsonObject processorNode = findNode(nodes, "processor");
+        assertNotNull(processorNode);
+        assertEquals("route", processorNode.getString("nodeType"));
+    }
+
+    @Test
+    void testInternalDirectConnection() {
+        DevConsole console = 
PluginHelper.getDevConsoleResolver(context).resolveDevConsole("route-topology");
+        JsonObject json = (JsonObject) console.call(DevConsole.MediaType.JSON);
+        JsonArray edges = (JsonArray) json.get("edges");
+
+        JsonObject edge = findEdge(edges, "trigger", "processor");
+        assertNotNull(edge, "Expected edge from trigger to processor via 
direct:process");
+        assertEquals("direct:process", edge.getString("endpoint"));
+        assertEquals("internal", edge.getString("connectionType"));
+    }
+
+    @Test
+    void testChainedConnections() {
+        DevConsole console = 
PluginHelper.getDevConsoleResolver(context).resolveDevConsole("route-topology");
+        JsonObject json = (JsonObject) console.call(DevConsole.MediaType.JSON);
+        JsonArray edges = (JsonArray) json.get("edges");
+
+        JsonObject edge = findEdge(edges, "processor", "validator");
+        assertNotNull(edge, "Expected edge from processor to validator via 
direct:validate");
+        assertEquals("direct:validate", edge.getString("endpoint"));
+        assertEquals("internal", edge.getString("connectionType"));
+    }
+
+    @Test
+    void testSelfReferencingRoute() {
+        DevConsole console = 
PluginHelper.getDevConsoleResolver(context).resolveDevConsole("route-topology");
+        JsonObject json = (JsonObject) console.call(DevConsole.MediaType.JSON);
+        JsonArray edges = (JsonArray) json.get("edges");
+
+        JsonObject selfEdge = findEdge(edges, "looper", "looper");
+        assertNotNull(selfEdge, "Expected self-referencing edge for looper via 
seda:loop");
+        assertEquals("seda:loop", selfEdge.getString("endpoint"));
+        assertEquals("internal", selfEdge.getString("connectionType"));
+    }
+
+    @Test
+    void testIsolatedRouteHasNoEdges() {
+        DevConsole console = 
PluginHelper.getDevConsoleResolver(context).resolveDevConsole("route-topology");
+        JsonObject json = (JsonObject) console.call(DevConsole.MediaType.JSON);
+        JsonArray edges = (JsonArray) json.get("edges");
+
+        for (int i = 0; i < edges.size(); i++) {
+            JsonObject edge = (JsonObject) edges.get(i);
+            if ("isolated".equals(edge.getString("fromRouteId")) || 
"isolated".equals(edge.getString("toRouteId"))) {
+                org.junit.jupiter.api.Assertions.fail("Isolated route should 
have no edges but found: " + edge);
+            }
+        }
+    }
+
+    @Test
+    void testTextOutput() {
+        DevConsole console = 
PluginHelper.getDevConsoleResolver(context).resolveDevConsole("route-topology");
+        String text = (String) console.call(DevConsole.MediaType.TEXT);
+        assertNotNull(text);
+        assertTrue(text.contains("Route Topology"));
+        assertTrue(text.contains("trigger"));
+        assertTrue(text.contains("processor"));
+        assertTrue(text.contains("direct:process"));
+    }
+
+    private JsonObject findNode(JsonArray nodes, String routeId) {
+        for (int i = 0; i < nodes.size(); i++) {
+            JsonObject node = (JsonObject) nodes.get(i);
+            if (routeId.equals(node.getString("routeId"))) {
+                return node;
+            }
+        }
+        return null;
+    }
+
+    private JsonObject findEdge(JsonArray edges, String fromRouteId, String 
toRouteId) {
+        for (int i = 0; i < edges.size(); i++) {
+            JsonObject edge = (JsonObject) edges.get(i);
+            if (fromRouteId.equals(edge.getString("fromRouteId"))
+                    && toRouteId.equals(edge.getString("toRouteId"))) {
+                return edge;
+            }
+        }
+        return null;
+    }
+
+}
diff --git 
a/core/camel-core-engine/src/generated/resources/META-INF/services/org/apache/camel/route-topology-dumper
 
b/core/camel-core-engine/src/generated/resources/META-INF/services/org/apache/camel/route-topology-dumper
new file mode 100644
index 000000000000..e6e6b9eef2b7
--- /dev/null
+++ 
b/core/camel-core-engine/src/generated/resources/META-INF/services/org/apache/camel/route-topology-dumper
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.impl.DefaultRouteTopologyDumper
diff --git 
a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultRouteTopologyDumper.java
 
b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultRouteTopologyDumper.java
new file mode 100644
index 000000000000..112aba2539ed
--- /dev/null
+++ 
b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultRouteTopologyDumper.java
@@ -0,0 +1,89 @@
+/*
+ * 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.impl;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.model.EndpointRequiredDefinition;
+import org.apache.camel.model.Model;
+import org.apache.camel.model.ProcessorDefinitionHelper;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.spi.RouteTopologyDumper;
+import org.apache.camel.spi.annotations.JdkService;
+import org.apache.camel.util.URISupport;
+
+@JdkService(RouteTopologyDumper.FACTORY)
+public class DefaultRouteTopologyDumper implements RouteTopologyDumper {
+
+    private static final Set<String> TRIGGER_SCHEMES = Set.of("timer", 
"quartz", "cron", "scheduler");
+    private static final Set<String> INTERNAL_SCHEMES = Set.of("direct", 
"seda", "vertx", "disruptor", "disruptor-vm");
+
+    @Override
+    public TopologyResult dumpTopology(CamelContext context) {
+        Model model = 
context.getCamelContextExtension().getContextPlugin(Model.class);
+        List<RouteDefinition> routeDefs = model.getRouteDefinitions();
+
+        List<TopologyNode> nodes = new ArrayList<>();
+        Map<String, List<String>> inputUriToRouteIds = new HashMap<>();
+
+        for (RouteDefinition rd : routeDefs) {
+            String inputUri = rd.getInput().getEndpointUri();
+            String normalizedInput = URISupport.stripQuery(inputUri);
+            String scheme = extractScheme(normalizedInput);
+            String nodeType = TRIGGER_SCHEMES.contains(scheme) ? "trigger" : 
"route";
+
+            nodes.add(new TopologyNode(rd.getRouteId(), normalizedInput, 
scheme, nodeType));
+            inputUriToRouteIds
+                    .computeIfAbsent(normalizedInput, k -> new ArrayList<>())
+                    .add(rd.getRouteId());
+        }
+
+        List<TopologyEdge> edges = new ArrayList<>();
+        for (RouteDefinition rd : routeDefs) {
+            Collection<EndpointRequiredDefinition> outputs
+                    = ProcessorDefinitionHelper.filterTypeInOutputs(
+                            rd.getOutputs(), EndpointRequiredDefinition.class);
+
+            for (EndpointRequiredDefinition erd : outputs) {
+                String outputUri = URISupport.stripQuery(erd.getEndpointUri());
+                String scheme = extractScheme(outputUri);
+
+                List<String> targetRouteIds = 
inputUriToRouteIds.get(outputUri);
+                if (targetRouteIds != null) {
+                    String connType = INTERNAL_SCHEMES.contains(scheme) ? 
"internal" : "external";
+                    for (String targetId : targetRouteIds) {
+                        edges.add(new TopologyEdge(rd.getRouteId(), targetId, 
outputUri, connType));
+                    }
+                }
+            }
+        }
+
+        return new TopologyResult(nodes, edges);
+    }
+
+    private static String extractScheme(String uri) {
+        int idx = uri.indexOf(':');
+        return idx > 0 ? uri.substring(0, idx) : uri;
+    }
+
+}
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/PluginHelper.java 
b/core/camel-support/src/main/java/org/apache/camel/support/PluginHelper.java
index 1d270ef76137..99c33772c33c 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/PluginHelper.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/PluginHelper.java
@@ -56,6 +56,7 @@ import org.apache.camel.spi.RestBindingJaxbDataFormatFactory;
 import org.apache.camel.spi.RestRegistry;
 import org.apache.camel.spi.RouteDiagramDumper;
 import org.apache.camel.spi.RouteFactory;
+import org.apache.camel.spi.RouteTopologyDumper;
 import org.apache.camel.spi.RoutesLoader;
 import org.apache.camel.spi.SimpleFunctionRegistry;
 import org.apache.camel.spi.UnitOfWorkFactory;
@@ -583,6 +584,20 @@ public final class PluginHelper {
         return 
extendedCamelContext.getContextPlugin(ModelToStructureDumper.class);
     }
 
+    /**
+     * Gets the {@link RouteTopologyDumper} to use.
+     */
+    public static RouteTopologyDumper getRouteTopologyDumper(CamelContext 
camelContext) {
+        return getRouteTopologyDumper(camelContext.getCamelContextExtension());
+    }
+
+    /**
+     * Gets the {@link RouteTopologyDumper} to use.
+     */
+    public static RouteTopologyDumper 
getRouteTopologyDumper(ExtendedCamelContext extendedCamelContext) {
+        return 
extendedCamelContext.getContextPlugin(RouteTopologyDumper.class);
+    }
+
     /**
      * Gets the {@link DeferServiceFactory} to use.
      */
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-topology.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-topology.adoc
new file mode 100644
index 000000000000..88713e908bfd
--- /dev/null
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-topology.adoc
@@ -0,0 +1,26 @@
+
+// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
+= camel cmd route-topology
+
+Display inter-route topology connections
+
+
+== Usage
+
+[source,bash]
+----
+camel cmd route-topology [options]
+----
+
+
+
+== Options
+
+[cols="2,5,1,2",options="header"]
+|===
+| Option | Description | Default | Type
+| `--json` | Output in JSON Format |  | boolean
+| `-h,--help` | Display the help and sub-commands |  | boolean
+|===
+
+
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc
index 5ce6c69d3117..4a9792305122 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc
@@ -30,6 +30,7 @@ camel cmd [options]
 | xref:jbang-commands/camel-jbang-cmd-resume-route.adoc[resume-route] | Resume 
Camel routes
 | xref:jbang-commands/camel-jbang-cmd-route-diagram.adoc[route-diagram] | 
Display Camel route diagram in the terminal
 | xref:jbang-commands/camel-jbang-cmd-route-structure.adoc[route-structure] | 
Dump Camel route structure
+| xref:jbang-commands/camel-jbang-cmd-route-topology.adoc[route-topology] | 
Display inter-route topology connections
 | xref:jbang-commands/camel-jbang-cmd-send.adoc[send] | Send messages to 
endpoints
 | xref:jbang-commands/camel-jbang-cmd-start-group.adoc[start-group] | Start 
Camel route groups
 | xref:jbang-commands/camel-jbang-cmd-start-route.adoc[start-route] | Start 
Camel routes
diff --git 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index 2f87e61735d4..9f849c3e9767 100644
--- 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++ 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -345,6 +345,8 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
                 doActionRouteDumpTask(root);
             } else if ("route-structure".equals(action)) {
                 doActionRouteStructureTask(root);
+            } else if ("route-topology".equals(action)) {
+                doActionRouteTopologyTask();
             } else if ("route-controller".equals(action)) {
                 doActionRouteControllerTask(root);
             } else if ("startup-recorder".equals(action)) {
@@ -748,6 +750,18 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
         }
     }
 
+    private void doActionRouteTopologyTask() throws Exception {
+        DevConsole dc = 
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
+                .resolveById("route-topology");
+        if (dc != null) {
+            JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON, 
Map.of());
+            LOG.trace("Updating output file: {}", outputFile);
+            IOHelper.writeText(json.toJson(), outputFile);
+        } else {
+            IOHelper.writeText("{}", outputFile);
+        }
+    }
+
     private void doActionSourceTask(JsonObject root) throws Exception {
         DevConsole dc = 
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
                 .resolveById("source");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index d8fc3f98ab8f..dc2e07a409a1 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -3,7 +3,7 @@
     { "name": "ask", "fullName": "ask", "description": "Ask a question about a 
running Camel application using AI", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": 
"--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, 
OPENAI_API_KEY, or LLM_API_KEY env vars", "javaType": "java.lang.String", 
"type": "string" }, { "names": "--api-type", "description": "API type: 
'ollama', 'openai', or 'anthropic'", "javaType": "LlmClient.ApiType", "type" 
[...]
     { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind 
source and sink Kamelets as a new Camel integration", "deprecated": true, 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": 
[ { "names": "--error-handler", "description": "Add error handler 
(none|log|sink:<endpoint>). Sink endpoints are expected in the format 
[[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet 
name.", "javaType": "java.lang.String", "type": "stri [...]
     { "name": "catalog", "fullName": "catalog", "description": "List artifacts 
from Camel Catalog", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ 
{ "names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"component", "fullName": "catalog component", "description": "List components 
from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
-    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
+    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
     { "name": "completion", "fullName": "completion", "description": "Generate 
completion script for bash\/zsh", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ] },
     { "name": "config", "fullName": "config", "description": "Get and set user 
configuration values", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", 
"fullName": "config get", "description": "Display user configuration value", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
     { "name": "debug", "fullName": "debug", "description": "Debug local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", 
"options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd 
HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background", "description": "Run in the background", "defaultValue": 
"false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To  [...]
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
index 04a0f9d39a6a..e5102ba30f99 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
@@ -117,6 +117,7 @@ public class CamelJBangMain implements Callable<Integer> {
                         .addSubcommand("resume-route", new CommandLine(new 
CamelRouteResumeAction(this)))
                         .addSubcommand("route-diagram", new CommandLine(new 
CamelRouteDiagramAction(this)))
                         .addSubcommand("route-structure", new CommandLine(new 
CamelRouteStructureAction(this)))
+                        .addSubcommand("route-topology", new CommandLine(new 
CamelRouteTopologyAction(this)))
                         .addSubcommand("send", new CommandLine(new 
CamelSendAction(this)))
                         .addSubcommand("start-group", new CommandLine(new 
CamelRouteGroupStartAction(this)))
                         .addSubcommand("start-route", new CommandLine(new 
CamelRouteStartAction(this)))
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteTopologyAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteTopologyAction.java
new file mode 100644
index 000000000000..57f9674229b9
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteTopologyAction.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.action;
+
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "route-topology", description = "Display inter-route topology 
connections", sortOptions = false,
+         showDefaultValues = true,
+         footer = {
+                 "%nExamples:",
+                 "  camel cmd route-topology",
+                 "  camel cmd route-topology --json" })
+public class CamelRouteTopologyAction extends ActionBaseCommand {
+
+    @CommandLine.Parameters(description = "Name or pid of a running Camel 
integration", arity = "0..1")
+    String name = "*";
+
+    @CommandLine.Option(names = { "--json" },
+                        description = "Output in JSON Format")
+    boolean jsonOutput;
+
+    public CamelRouteTopologyAction(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doCall() throws Exception {
+        List<Long> pids = findPids(name);
+        if (pids.isEmpty()) {
+            printer().println("No running Camel integration found");
+            return 1;
+        }
+        if (pids.size() > 1) {
+            printer().println("Name or pid " + name + " matches " + pids.size()
+                              + " running Camel integrations. Specify a name 
or PID that matches exactly one.");
+            return 1;
+        }
+
+        long pid = pids.get(0);
+        Path outputFile = prepareAction(Long.toString(pid), "route-topology", 
root -> {
+        });
+
+        JsonObject jo = getJsonObject(outputFile);
+        if (jo != null) {
+            if (jsonOutput) {
+                String dump = Jsoner.prettyPrint(jo.toJson(), 2);
+                printer().println(dump);
+            } else {
+                printTopology(jo);
+            }
+        } else {
+            printer().println("Response from running Camel with PID " + pid + 
" not received within 5 seconds");
+            return 1;
+        }
+
+        PathUtils.deleteFile(outputFile);
+        return 0;
+    }
+
+    private void printTopology(JsonObject jo) {
+        JsonArray nodes = (JsonArray) jo.get("nodes");
+        JsonArray edges = (JsonArray) jo.get("edges");
+
+        int nodeCount = nodes != null ? nodes.size() : 0;
+        int edgeCount = edges != null ? edges.size() : 0;
+        printer().printf("Route Topology (%d routes, %d connections)%n%n", 
nodeCount, edgeCount);
+
+        if (nodes != null) {
+            for (Object n : nodes) {
+                JsonObject node = (JsonObject) n;
+                printer().printf("  %s (%s) type=%s%n",
+                        node.getString("routeId"),
+                        node.getString("from"),
+                        node.getString("nodeType"));
+
+                if (edges != null) {
+                    for (Object e : edges) {
+                        JsonObject edge = (JsonObject) e;
+                        if 
(node.getString("routeId").equals(edge.getString("fromRouteId"))) {
+                            printer().printf("    --> %s via %s [%s]%n",
+                                    edge.getString("toRouteId"),
+                                    edge.getString("endpoint"),
+                                    edge.getString("connectionType"));
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+}


Reply via email to