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/05/03 10:46:43 UTC
(camel) 01/01: CAMEL-23402: Route structure dump to walk model for structure to be accurate
This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch dum
in repository https://gitbox.apache.org/repos/asf/camel.git
commit 61c28fd0bb4f2f000ef2f26c25ca5319af3a6c5d
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Sun May 3 12:46:22 2026 +0200
CAMEL-23402: Route structure dump to walk model for structure to be accurate
---
.../java/org/apache/camel/spi/ModelDumpLine.java | 4 +-
.../camel/impl/engine/AbstractCamelContext.java | 11 +-
.../camel/impl/DefaultModelToStructureDumper.java | 70 ++++-----
.../org/apache/camel/model/CatchDefinition.java | 4 +-
.../camel/model/ProcessorDefinitionHelper.java | 4 +
.../management/DumpRouteStructureChoiceTest.java | 170 +++++++++++++++++++++
.../camel/management/DumpRouteStructureTest.java | 16 +-
.../commands/action/CamelRouteStructureAction.java | 4 +
8 files changed, 232 insertions(+), 51 deletions(-)
diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/ModelDumpLine.java b/core/camel-api/src/main/java/org/apache/camel/spi/ModelDumpLine.java
index 8b880b6500c2..c0758fcdde5b 100644
--- a/core/camel-api/src/main/java/org/apache/camel/spi/ModelDumpLine.java
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/ModelDumpLine.java
@@ -17,7 +17,7 @@
package org.apache.camel.spi;
/**
- * Represents a line in a model dumper of the route structure (not with full details like a XML or YAML dump).
+ * Represents a line in a model dumper of the route structure (not with full details such as XML or YAML dump).
*
* @param location line source location:line (if present)
* @param type the kind of EIP node
@@ -26,4 +26,6 @@ package org.apache.camel.spi;
* @param code EIP code such as label or short name that is human-readable or pseudocode
*/
public record ModelDumpLine(String location, String type, String id, int level, String code) {
+
+ // TODO: description
}
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 b648c21ccdb5..e19ed149107c 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
@@ -3174,6 +3174,12 @@ public abstract class AbstractCamelContext extends BaseService
LOG.debug("Skip starting routes as CamelContext has been configured with autoStartup=false");
}
+ // dump routes when as we have model before creating the runtime models, which can help during troubleshooting
+ // when routes have problems starting
+ if (getDumpRoutes() != null && !"false".equals(getDumpRoutes())) {
+ doDumpRoutes();
+ }
+
if (!getRouteController().isSupervising()) {
// invoke this logic to warmup the routes and if possible also start the routes (using default route controller)
StartupStep subStep
@@ -3185,11 +3191,6 @@ public abstract class AbstractCamelContext extends BaseService
startupStepRecorder.endStep(subStep);
}
- // dump routes after they have been initialized and started so we have full information
- if (getDumpRoutes() != null && !"false".equals(getDumpRoutes())) {
- doDumpRoutes();
- }
-
// ensure extra dev consoles is loaded in case additional JARs has been dynamically added to the classpath
if (devConsole) {
StartupStep step = startupStepRecorder.beginStep(CamelContext.class, null, "Scan DevConsoles (phase 2)");
diff --git a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModelToStructureDumper.java b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModelToStructureDumper.java
index 353762780d0e..f868b216d8dd 100644
--- a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModelToStructureDumper.java
+++ b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModelToStructureDumper.java
@@ -17,17 +17,14 @@
package org.apache.camel.impl;
import java.util.ArrayList;
-import java.util.Comparator;
import java.util.List;
-import java.util.Set;
-
-import javax.management.MBeanServer;
-import javax.management.ObjectName;
import org.apache.camel.CamelContext;
+import org.apache.camel.NamedNode;
import org.apache.camel.Route;
-import org.apache.camel.api.management.mbean.ManagedProcessorMBean;
import org.apache.camel.model.Model;
+import org.apache.camel.model.OptionalIdentifiedDefinition;
+import org.apache.camel.model.ProcessorDefinitionHelper;
import org.apache.camel.model.RouteDefinition;
import org.apache.camel.spi.ModelDumpLine;
import org.apache.camel.spi.ModelToStructureDumper;
@@ -63,50 +60,39 @@ public class DefaultModelToStructureDumper implements ModelToStructureDumper {
}
answer.add(new ModelDumpLine(loc, "from", routeId, 1, "from[" + uri + "]"));
- MBeanServer server = context.getManagementStrategy().getManagementAgent().getMBeanServer();
- if (server != null) {
- String jmxDomain = context.getManagementStrategy().getManagementAgent().getMBeanObjectDomainName();
- // get all the processor mbeans and sort them accordingly to their index
- String prefix = context.getManagementStrategy().getManagementAgent().getIncludeHostName() ? "*/" : "";
- ObjectName query = ObjectName.getInstance(
- jmxDomain + ":context=" + prefix + context.getManagementName() + ",type=processors,*");
- Set<ObjectName> names = server.queryNames(query, null);
- List<ManagedProcessorMBean> mps = new ArrayList<>();
- for (ObjectName on : names) {
- ManagedProcessorMBean processor = context.getManagementStrategy().getManagementAgent().newProxyClient(on,
- ManagedProcessorMBean.class);
- // the processor must belong to this route
- if (def.getRouteId().equals(processor.getRouteId())) {
- mps.add(processor);
- }
- }
- // sort by index
- mps.sort(new OrderProcessorMBeans());
+ var outputs = ProcessorDefinitionHelper.filterTypeInOutputs(def.getOutputs(), OptionalIdentifiedDefinition.class);
- // dump in text format padded by level
- for (ManagedProcessorMBean processor : mps) {
- // include scheme in loc
- loc = scheme + ":" + processor.getSourceLocationShort();
- String kind = processor.getProcessorName();
- String id = processor.getProcessorId();
- int level = processor.getLevel() + 1;
- String code = brief ? processor.getProcessorName() : processor.getModelLabel();
- answer.add(new ModelDumpLine(loc, kind, id, level, code));
+ for (var output : outputs) {
+ loc = scheme + ":" + output.getLocation();
+ if (output.getLineNumber() > 0) {
+ loc += ":" + output.getLineNumber();
}
+ String kind = output.getShortName();
+ String id = output.getId();
+ int level = getLevel(output) + 1;
+ boolean choice = "choice".equals(output.getShortName());
+ String code = choice || brief ? output.getShortName() : output.getLabel();
+ answer.add(new ModelDumpLine(loc, kind, id, level, code));
}
return answer;
}
- /**
- * Used for sorting the processor mbeans accordingly to their index.
- */
- private static final class OrderProcessorMBeans implements Comparator<ManagedProcessorMBean> {
-
- @Override
- public int compare(ManagedProcessorMBean o1, ManagedProcessorMBean o2) {
- return o1.getIndex().compareTo(o2.getIndex());
+ private static int getLevel(NamedNode node) {
+ int level = 0;
+ while (node != null && node.getParent() != null) {
+ // special for choice
+ boolean choice = "choice".equals(node.getParent().getShortName());
+ if (choice) {
+ level++;
+ }
+ boolean shallow = "when".equals(node.getShortName()) || "otherwise".equals(node.getShortName());
+ if (!shallow) {
+ level++;
+ }
+ node = node.getParent();
}
+ return level;
}
}
diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/CatchDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/CatchDefinition.java
index 10173bd5ee54..208d29aa7b6d 100644
--- a/core/camel-core-model/src/main/java/org/apache/camel/model/CatchDefinition.java
+++ b/core/camel-core-model/src/main/java/org/apache/camel/model/CatchDefinition.java
@@ -75,7 +75,7 @@ public class CatchDefinition extends OutputDefinition<CatchDefinition> {
@Override
public String toString() {
- return "DoCatch[ " + getExceptionClasses() + " -> " + getOutputs() + "]";
+ return "DoCatch[" + String.join(",", getExceptions()) + " -> " + getOutputs() + "]";
}
@Override
@@ -85,7 +85,7 @@ public class CatchDefinition extends OutputDefinition<CatchDefinition> {
@Override
public String getLabel() {
- return "doCatch[ " + getExceptionClasses() + "]";
+ return "doCatch[" + String.join(",", getExceptions()) + "]";
}
@Override
diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/ProcessorDefinitionHelper.java b/core/camel-core-model/src/main/java/org/apache/camel/model/ProcessorDefinitionHelper.java
index 74b50756febe..ef437cbd326e 100644
--- a/core/camel-core-model/src/main/java/org/apache/camel/model/ProcessorDefinitionHelper.java
+++ b/core/camel-core-model/src/main/java/org/apache/camel/model/ProcessorDefinitionHelper.java
@@ -307,6 +307,10 @@ public final class ProcessorDefinitionHelper {
// otherwise is optional
if (choice.getOtherwise() != null) {
+ OtherwiseDefinition other = choice.getOtherwise();
+ if (type.isInstance(other)) {
+ found.add((T) other);
+ }
List<ProcessorDefinition<?>> children = choice.getOtherwise().getOutputs();
doFindType(children, type, found, ++current, maxDeep);
}
diff --git a/core/camel-management/src/test/java/org/apache/camel/management/DumpRouteStructureChoiceTest.java b/core/camel-management/src/test/java/org/apache/camel/management/DumpRouteStructureChoiceTest.java
new file mode 100644
index 000000000000..f5bcd4eb6da1
--- /dev/null
+++ b/core/camel-management/src/test/java/org/apache/camel/management/DumpRouteStructureChoiceTest.java
@@ -0,0 +1,170 @@
+/*
+ * 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.management;
+
+import java.util.List;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.TestSupportNodeIdFactory;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.ModelDumpLine;
+import org.apache.camel.spi.ModelToStructureDumper;
+import org.apache.camel.spi.NodeIdFactory;
+import org.apache.camel.support.PluginHelper;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@DisabledOnOs(OS.AIX)
+public class DumpRouteStructureChoiceTest extends ManagementTestSupport {
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+ context.getCamelContextExtension().addContextPlugin(NodeIdFactory.class, new TestSupportNodeIdFactory());
+ return context;
+ }
+
+ @Override
+ public boolean isUseRouteBuilder() {
+ return false;
+ }
+
+ @Test
+ public void testDump() throws Exception {
+ TestSupportNodeIdFactory.resetCounters();
+
+ context.addRoutes(createRouteBuilder());
+ context.start();
+
+ ModelToStructureDumper dumper = PluginHelper.getModelToStructureDumper(context);
+ List<ModelDumpLine> lines = dumper.dumpStructure(context, "myChoiceRoute", false);
+ assertEquals(10, lines.size());
+ assertEquals(0, lines.get(0).level());
+ assertEquals("route", lines.get(0).type());
+ assertEquals("myChoiceRoute", lines.get(0).id());
+ assertEquals("route[myChoiceRoute]", lines.get(0).code());
+ assertEquals(1, lines.get(1).level());
+ assertEquals("from", lines.get(1).type());
+ assertEquals("myChoiceRoute", lines.get(1).id());
+ assertEquals("from[direct://start]", lines.get(1).code());
+ assertEquals(2, lines.get(2).level());
+ assertEquals("choice", lines.get(2).type());
+ assertEquals("choice1", lines.get(2).id());
+ assertEquals("choice", lines.get(2).code());
+ assertEquals(3, lines.get(3).level());
+ assertEquals("when", lines.get(3).type());
+ assertEquals("when1", lines.get(3).id());
+ assertEquals("when[xpath{$foo = 'bar'}]", lines.get(3).code());
+ assertEquals(4, lines.get(4).level());
+ assertEquals("to", lines.get(4).type());
+ assertEquals("to1", lines.get(4).id());
+ assertEquals("to[mock:x]", lines.get(4).code());
+ assertEquals(3, lines.get(5).level());
+ assertEquals("when", lines.get(5).type());
+ assertEquals("when2", lines.get(5).id());
+ assertEquals("when[xpath{$foo = 'cheese'}]", lines.get(5).code());
+ assertEquals(4, lines.get(6).level());
+ assertEquals("to", lines.get(6).type());
+ assertEquals("to2", lines.get(6).id());
+ assertEquals("to[mock:y]", lines.get(6).code());
+ assertEquals(3, lines.get(7).level());
+ assertEquals("otherwise", lines.get(7).type());
+ assertEquals("otherwise1", lines.get(7).id());
+ assertEquals("otherwise", lines.get(7).code());
+ assertEquals(4, lines.get(8).level());
+ assertEquals("to", lines.get(8).type());
+ assertEquals("to3", lines.get(8).id());
+ assertEquals("to[mock:z]", lines.get(8).code());
+ assertEquals(2, lines.get(9).level());
+ assertEquals("to", lines.get(9).type());
+ assertEquals("to4", lines.get(9).id());
+ assertEquals("to[mock:end]", lines.get(9).code());
+ }
+
+ @Test
+ public void testDumpBrief() throws Exception {
+ TestSupportNodeIdFactory.resetCounters();
+
+ context.addRoutes(createRouteBuilder());
+ context.start();
+
+ ModelToStructureDumper dumper = PluginHelper.getModelToStructureDumper(context);
+ List<ModelDumpLine> lines = dumper.dumpStructure(context, "myChoiceRoute", true);
+ assertEquals(10, lines.size());
+ assertEquals(0, lines.get(0).level());
+ assertEquals("route", lines.get(0).type());
+ assertEquals("myChoiceRoute", lines.get(0).id());
+ assertEquals("route[myChoiceRoute]", lines.get(0).code());
+ assertEquals(1, lines.get(1).level());
+ assertEquals("from", lines.get(1).type());
+ assertEquals("myChoiceRoute", lines.get(1).id());
+ assertEquals("from[direct://start]", lines.get(1).code());
+ assertEquals(2, lines.get(2).level());
+ assertEquals("choice", lines.get(2).type());
+ assertEquals("choice1", lines.get(2).id());
+ assertEquals("choice", lines.get(2).code());
+ assertEquals(3, lines.get(3).level());
+ assertEquals("when", lines.get(3).type());
+ assertEquals("when1", lines.get(3).id());
+ assertEquals("when", lines.get(3).code());
+ assertEquals(4, lines.get(4).level());
+ assertEquals("to", lines.get(4).type());
+ assertEquals("to1", lines.get(4).id());
+ assertEquals("to", lines.get(4).code());
+ assertEquals(3, lines.get(5).level());
+ assertEquals("when", lines.get(5).type());
+ assertEquals("when2", lines.get(5).id());
+ assertEquals("when", lines.get(5).code());
+ assertEquals(4, lines.get(6).level());
+ assertEquals("to", lines.get(6).type());
+ assertEquals("to2", lines.get(6).id());
+ assertEquals("to", lines.get(6).code());
+ assertEquals(3, lines.get(7).level());
+ assertEquals("otherwise", lines.get(7).type());
+ assertEquals("otherwise1", lines.get(7).id());
+ assertEquals("otherwise", lines.get(7).code());
+ assertEquals(4, lines.get(8).level());
+ assertEquals("to", lines.get(8).type());
+ assertEquals("to3", lines.get(8).id());
+ assertEquals("to", lines.get(8).code());
+ assertEquals(2, lines.get(9).level());
+ assertEquals("to", lines.get(9).type());
+ assertEquals("to4", lines.get(9).id());
+ assertEquals("to", lines.get(9).code());
+ }
+
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ context.setDebugging(true);
+
+ from("direct:start").routeId("myChoiceRoute")
+ .choice()
+ .when().xpath("$foo = 'bar'").to("mock:x")
+ .when().xpath("$foo = 'cheese'").to("mock:y")
+ .otherwise().to("mock:z")
+ .end()
+ .to("mock:end");
+ }
+ };
+ }
+
+}
diff --git a/core/camel-management/src/test/java/org/apache/camel/management/DumpRouteStructureTest.java b/core/camel-management/src/test/java/org/apache/camel/management/DumpRouteStructureTest.java
index 1b6345225e32..2ae2a65d771e 100644
--- a/core/camel-management/src/test/java/org/apache/camel/management/DumpRouteStructureTest.java
+++ b/core/camel-management/src/test/java/org/apache/camel/management/DumpRouteStructureTest.java
@@ -18,9 +18,12 @@ package org.apache.camel.management;
import java.util.List;
+import org.apache.camel.CamelContext;
+import org.apache.camel.TestSupportNodeIdFactory;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.spi.ModelDumpLine;
import org.apache.camel.spi.ModelToStructureDumper;
+import org.apache.camel.spi.NodeIdFactory;
import org.apache.camel.support.PluginHelper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
@@ -31,8 +34,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
@DisabledOnOs(OS.AIX)
public class DumpRouteStructureTest extends ManagementTestSupport {
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+ context.getCamelContextExtension().addContextPlugin(NodeIdFactory.class, new TestSupportNodeIdFactory());
+ return context;
+ }
+
@Test
public void testDump() throws Exception {
+ TestSupportNodeIdFactory.resetCounters();
+
ModelToStructureDumper dumper = PluginHelper.getModelToStructureDumper(context);
List<ModelDumpLine> lines = dumper.dumpStructure(context, "myOtherRoute", false);
assertEquals(5, lines.size());
@@ -60,6 +72,8 @@ public class DumpRouteStructureTest extends ManagementTestSupport {
@Test
public void testDumpBrief() throws Exception {
+ TestSupportNodeIdFactory.resetCounters();
+
ModelToStructureDumper dumper = PluginHelper.getModelToStructureDumper(context);
List<ModelDumpLine> lines = dumper.dumpStructure(context, "myOtherRoute", true);
assertEquals(5, lines.size());
@@ -93,7 +107,7 @@ public class DumpRouteStructureTest extends ManagementTestSupport {
from("seda:bar?size=1234&multipleConsumers=true").routeId("myOtherRoute")
.filter().header("bar").id("myfilter")
- .to("mock:bar").id("mybar")
+ .to("mock:bar").id("mybar")
.end()
.to("log:end").id("myend");
}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
index f69f913f0948..64ffb17a02af 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
@@ -139,6 +139,10 @@ public class CamelRouteStructureAction extends ActionBaseCommand {
code.id = line.getString("id");
code.level = line.getInteger("level");
code.code = line.getString("code");
+ if (brief) {
+ // make code brief
+ code.code = StringHelper.before(code.code, "[", code.code);
+ }
row.code.add(code);
}
}