You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/06/01 17:07:30 UTC
(camel) 01/05: CAMEL-23656: Add route topology service to compute inter-route relationships
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 <cl...@gmail.com>
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 <no...@anthropic.com>
---
.../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"));
+ }
+ }
+ }
+ }
+ }
+ }
+
+}