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 <[email protected]> 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); } }
