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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 80dd201302ea CAMEL-25063: camel-management - Route and CamelContext 
MBeans: fix bugs found in a deep review (#26946)
80dd201302ea is described below

commit 80dd201302eaffde681e37b56b9f3a117c3cae12
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Sep 28 09:13:40 2026 +0200

    CAMEL-25063: camel-management - Route and CamelContext MBeans: fix bugs 
found in a deep review (#26946)
    
    - dumpStepStatsAsXml of the CamelContext is well-formed XML
    - the generatedIds flag of dumpRouteAsXml/dumpRoutesAsXml/dumpRoutesAsYaml 
is used (regression in 4.16)
    - reset(true) of a route also resets its steps
    - reset(true) of a route with * or ? in its id no longer resets other routes
    - the percentiles no longer fail when concurrent exchanges take the count 
past the window
    - reset() also resets the load averages of a route, the context and a route 
group
    - the XML stats dumps escape quotes and the source location
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../management/mbean/ManagedCamelContext.java      |  20 +--
 .../mbean/ManagedPerformanceCounter.java           |   8 +-
 .../camel/management/mbean/ManagedRoute.java       |  40 +++--
 .../camel/management/mbean/ManagedRouteGroup.java  |   6 +
 .../ManagedRouteAndContextEdgeCasesTest.java       | 163 +++++++++++++++++++++
 5 files changed, 208 insertions(+), 29 deletions(-)

diff --git 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedCamelContext.java
 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedCamelContext.java
index 3bbcfe8b7805..c6ca85c46fc1 100644
--- 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedCamelContext.java
+++ 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedCamelContext.java
@@ -53,6 +53,7 @@ import org.apache.camel.spi.ManagementStrategy;
 import org.apache.camel.spi.UnitOfWork;
 import org.apache.camel.support.CamelContextHelper;
 import org.apache.camel.support.PluginHelper;
+import org.apache.camel.util.StringHelper;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 
@@ -95,6 +96,7 @@ public class ManagedCamelContext extends 
ManagedPerformanceCounter implements Ma
     @Override
     public void reset() {
         super.reset();
+        load.reset();
         remoteExchangesTotal.reset();
         remoteExchangesCompleted.reset();
         remoteExchangesFailed.reset();
@@ -561,7 +563,7 @@ public class ManagedCamelContext extends 
ManagedPerformanceCounter implements Ma
 
     @Override
     public String dumpRoutesAsXml(boolean resolvePlaceholders, boolean 
generatedIds) throws Exception {
-        return dumpRoutesAsXml(resolvePlaceholders, true, false);
+        return dumpRoutesAsXml(resolvePlaceholders, generatedIds, false);
     }
 
     @Override
@@ -601,7 +603,7 @@ public class ManagedCamelContext extends 
ManagedPerformanceCounter implements Ma
     @Override
     public String dumpRoutesAsYaml(boolean resolvePlaceholders, boolean 
uriAsParameters, boolean generatedIds)
             throws Exception {
-        return dumpRoutesAsYaml(resolvePlaceholders, uriAsParameters, true, 
false);
+        return dumpRoutesAsYaml(resolvePlaceholders, uriAsParameters, 
generatedIds, false);
     }
 
     @Override
@@ -710,7 +712,7 @@ public class ManagedCamelContext extends 
ManagedPerformanceCounter implements Ma
                     sb.append(String.format(" group=\"%s\"", 
escapeXml(route.getRouteGroup())));
                 }
                 if (route.getSourceLocation() != null) {
-                    sb.append(String.format(" sourceLocation=\"%s\"", 
route.getSourceLocation()));
+                    sb.append(String.format(" sourceLocation=\"%s\"", 
escapeXml(route.getSourceLocation())));
                 }
 
                 // use substring as we only want the attributes
@@ -871,7 +873,7 @@ public class ManagedCamelContext extends 
ManagedPerformanceCounter implements Ma
                     sb.append(String.format(" group=\"%s\"", 
escapeXml(route.getRouteGroup())));
                 }
                 if (route.getSourceLocation() != null) {
-                    sb.append(String.format(" sourceLocation=\"%s\"", 
route.getSourceLocation()));
+                    sb.append(String.format(" sourceLocation=\"%s\"", 
escapeXml(route.getSourceLocation())));
                 }
 
                 // use substring as we only want the attributes
@@ -895,9 +897,9 @@ public class ManagedCamelContext extends 
ManagedPerformanceCounter implements Ma
                         sb.append(" 
exchangesInflight=\"").append(step.getExchangesInflight()).append("\"");
                         sb.append(" ").append(stat, 7, 
stat.length()).append("\n");
                     }
-                    sb.append("      </stepStats>\n");
                 }
-                sb.append("    </stepStat>\n");
+                sb.append("      </stepStats>\n");
+                sb.append("    </routeStat>\n");
             }
             sb.append("  </routeStats>\n");
         }
@@ -1016,10 +1018,8 @@ public class ManagedCamelContext extends 
ManagedPerformanceCounter implements Ma
     }
 
     private static String escapeXml(String text) {
-        return text
-                .replace("&", "&amp;")
-                .replace("<", "&lt;")
-                .replace(">", "&gt;");
+        // also quotes as the values are used in attributes
+        return StringHelper.xmlEncode(text);
     }
 
 }
diff --git 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedPerformanceCounter.java
 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedPerformanceCounter.java
index 9bca27e455b6..40675eba41d5 100644
--- 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedPerformanceCounter.java
+++ 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedPerformanceCounter.java
@@ -390,10 +390,12 @@ public abstract class ManagedPerformanceCounter extends 
ManagedCounter
         if (percentileWindow == null || percentileCount == 0) {
             return -1;
         }
-        long[] snapshot = new long[percentileCount];
-        System.arraycopy(percentileWindow, 0, snapshot, 0, percentileCount);
+        // the count is updated without a lock by concurrent exchanges, so it 
may go past the window size
+        int count = Math.min(percentileCount, PERCENTILE_WINDOW_SIZE);
+        long[] snapshot = new long[count];
+        System.arraycopy(percentileWindow, 0, snapshot, 0, count);
         Arrays.sort(snapshot);
-        int index = (int) Math.ceil(percentile * percentileCount) - 1;
+        int index = (int) Math.ceil(percentile * count) - 1;
         return snapshot[Math.max(0, index)];
     }
 
diff --git 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRoute.java
 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRoute.java
index 49775a6e3976..780ea82cea6a 100644
--- 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRoute.java
+++ 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRoute.java
@@ -63,6 +63,7 @@ import org.apache.camel.spi.ManagementStrategy;
 import org.apache.camel.spi.RoutePolicy;
 import org.apache.camel.support.PluginHelper;
 import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.util.StringHelper;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.xml.LwModelHelper;
@@ -436,7 +437,7 @@ public class ManagedRoute extends ManagedPerformanceCounter 
implements ManagedRo
 
     @Override
     public String dumpRouteAsXml(boolean resolvePlaceholders, boolean 
generatedIds) throws Exception {
-        return dumpRouteAsXml(resolvePlaceholders, true, false);
+        return dumpRouteAsXml(resolvePlaceholders, generatedIds, false);
     }
 
     @Override
@@ -589,7 +590,7 @@ public class ManagedRoute extends ManagedPerformanceCounter 
implements ManagedRo
             answer.append(String.format(" group=\"%s\"", 
escapeXml(getRouteGroup())));
         }
         if (sourceLocation != null) {
-            answer.append(String.format(" sourceLocation=\"%s\"", 
getSourceLocation()));
+            answer.append(String.format(" sourceLocation=\"%s\"", 
escapeXml(getSourceLocation())));
         }
         // use substring as we only want the attributes
         String stat = dumpStatsAsXml(fullStats);
@@ -759,7 +760,7 @@ public class ManagedRoute extends ManagedPerformanceCounter 
implements ManagedRo
             answer.append(String.format(" group=\"%s\"", 
escapeXml(getRouteGroup())));
         }
         if (sourceLocation != null) {
-            answer.append(String.format(" sourceLocation=\"%s\"", 
getSourceLocation()));
+            answer.append(String.format(" sourceLocation=\"%s\"", 
escapeXml(getSourceLocation())));
         }
         // use substring as we only want the attributes
         String stat = dumpStatsAsXml(fullStats);
@@ -812,7 +813,7 @@ public class ManagedRoute extends ManagedPerformanceCounter 
implements ManagedRo
                 sb.append("\n    <routeLocation")
                         .append(String.format(
                                 " routeId=\"%s\" id=\"%s\" index=\"%s\" 
sourceLocation=\"%s\" sourceLineNumber=\"%s\"/>",
-                                escapeXml(route.getRouteId()), id, 0, 
location, line));
+                                escapeXml(route.getRouteId()), escapeXml(id), 
0, escapeXml(location), line));
             }
             for (ManagedProcessorMBean processor : processors) {
                 // the step must belong to this route
@@ -823,7 +824,7 @@ public class ManagedRoute extends ManagedPerformanceCounter 
implements ManagedRo
                             .append(String.format(
                                     " routeId=\"%s\" id=\"%s\" index=\"%s\" 
sourceLocation=\"%s\" sourceLineNumber=\"%s\"/>",
                                     escapeXml(route.getRouteId()), 
escapeXml(processor.getProcessorId()), processor.getIndex(),
-                                    location, line));
+                                    escapeXml(location), line));
                 }
             }
         }
@@ -831,10 +832,15 @@ public class ManagedRoute extends 
ManagedPerformanceCounter implements ManagedRo
         return sb.toString();
     }
 
+    @Override
+    public void reset() {
+        super.reset();
+        load.reset();
+    }
+
     @Override
     public void reset(boolean includeProcessors) throws Exception {
         reset();
-        load.reset();
 
         // and now reset all processors for this route
         if (includeProcessors) {
@@ -842,12 +848,16 @@ public class ManagedRoute extends 
ManagedPerformanceCounter implements ManagedRo
             if (server != null) {
                 // get all the processor mbeans and sort them accordingly to 
their index
                 String prefix = 
getContext().getManagementStrategy().getManagementAgent().getIncludeHostName() 
? "*/" : "";
-                ObjectName query = ObjectName.getInstance(
-                        jmxDomain + ":context=" + prefix + 
getContext().getManagementName() + ",type=processors,*");
-                QueryExp queryExp = Query.match(new 
AttributeValueExp("RouteId"), new StringValueExp(getRouteId()));
-                Set<ObjectName> names = server.queryNames(query, queryExp);
-                for (ObjectName name : names) {
-                    server.invoke(name, "reset", null, null);
+                // the route id must be equal (match would treat * and ? in 
the route id as wildcards)
+                QueryExp queryExp = Query.eq(new AttributeValueExp("RouteId"), 
new StringValueExp(getRouteId()));
+                // steps are registered as their own type
+                for (String type : new String[] { "processors", "steps" }) {
+                    ObjectName query = ObjectName.getInstance(
+                            jmxDomain + ":context=" + prefix + 
getContext().getManagementName() + ",type=" + type + ",*");
+                    Set<ObjectName> names = server.queryNames(query, queryExp);
+                    for (ObjectName name : names) {
+                        server.invoke(name, "reset", null, null);
+                    }
                 }
             }
         }
@@ -1029,10 +1039,8 @@ public class ManagedRoute extends 
ManagedPerformanceCounter implements ManagedRo
     }
 
     private static String escapeXml(String text) {
-        return text
-                .replace("&", "&amp;")
-                .replace("<", "&lt;")
-                .replace(">", "&gt;");
+        // also quotes as the values are used in attributes
+        return StringHelper.xmlEncode(text);
     }
 
 }
diff --git 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRouteGroup.java
 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRouteGroup.java
index fc42869151c0..dd554ff13bb4 100644
--- 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRouteGroup.java
+++ 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRouteGroup.java
@@ -45,6 +45,12 @@ public class ManagedRouteGroup extends 
ManagedPerformanceCounter implements Mana
         this.jmxDomain = 
context.getManagementStrategy().getManagementAgent().getMBeanObjectDomainName();
     }
 
+    @Override
+    public void reset() {
+        super.reset();
+        load.reset();
+    }
+
     @Override
     public void init(ManagementStrategy strategy) {
         super.init(strategy);
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteAndContextEdgeCasesTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteAndContextEdgeCasesTest.java
new file mode 100644
index 000000000000..1f94325126a6
--- /dev/null
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteAndContextEdgeCasesTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.io.StringReader;
+import java.lang.reflect.Field;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.w3c.dom.Document;
+
+import org.xml.sax.InputSource;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ManagementStatisticsLevel;
+import org.apache.camel.api.management.ManagedCamelContext;
+import org.apache.camel.api.management.mbean.ManagedCamelContextMBean;
+import org.apache.camel.api.management.mbean.ManagedRouteMBean;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.management.mbean.LoadTriplet;
+import org.apache.camel.management.mbean.ManagedPerformanceCounter;
+import org.apache.camel.management.mbean.ManagedRoute;
+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;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@DisabledOnOs(OS.AIX)
+public class ManagedRouteAndContextEdgeCasesTest extends ManagementTestSupport 
{
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+        
context.getManagementStrategy().getManagementAgent().setStatisticsLevel(ManagementStatisticsLevel.Extended);
+        return context;
+    }
+
+    private ManagedCamelContext mcc() {
+        return 
context.getCamelContextExtension().getContextPlugin(ManagedCamelContext.class);
+    }
+
+    private static Document parse(String xml) throws Exception {
+        return 
DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new 
InputSource(new StringReader(xml)));
+    }
+
+    @Test
+    public void testDumpStepStatsAsXmlIsWellFormed() throws Exception {
+        template.sendBody("direct:start", "Hello");
+        template.sendBody("direct:other", "Hello");
+
+        Document doc = 
parse(mcc().getManagedCamelContext().dumpStepStatsAsXml(true));
+        // every route, with the steps of the route
+        assertEquals(3, doc.getElementsByTagName("routeStat").getLength());
+        assertEquals(3, doc.getElementsByTagName("stepStats").getLength());
+        assertEquals(2, doc.getElementsByTagName("stepStat").getLength());
+    }
+
+    @Test
+    public void testDumpWithoutGeneratedIds() throws Exception {
+        ManagedRouteMBean route = mcc().getManagedRoute("route1");
+        assertFalse(route.dumpRouteAsXml(false, false).contains("id=\"to"));
+        assertTrue(route.dumpRouteAsXml(false, true).contains("id=\"to"));
+
+        ManagedCamelContextMBean camel = mcc().getManagedCamelContext();
+        assertFalse(camel.dumpRoutesAsXml(false, false).contains("id=\"to"));
+        assertFalse(camel.dumpRoutesAsYaml(false, false, false).contains("id: 
to"));
+    }
+
+    @Test
+    public void testResetIncludesSteps() throws Exception {
+        template.sendBody("direct:start", "Hello");
+        assertEquals(1, mcc().getManagedStep("foo").getExchangesTotal());
+
+        mcc().getManagedRoute("route1").reset(true);
+        assertEquals(0, mcc().getManagedStep("foo").getExchangesTotal());
+    }
+
+    @Test
+    public void testResetOfRouteIdWithWildcard() throws Exception {
+        template.sendBody("direct:star", "Hello");
+        template.sendBody("direct:start", "Hello");
+
+        mcc().getManagedRoute("rou*").reset(true);
+        assertEquals(0, 
mcc().getManagedProcessor("starLog").getExchangesTotal());
+        // route1 is not the route rou* (but matches it as a wildcard)
+        assertEquals(1, 
mcc().getManagedProcessor("fooLog").getExchangesTotal());
+    }
+
+    @Test
+    public void testPercentileWhenCountGoesPastTheWindow() throws Exception {
+        ManagedRoute route = new ManagedRoute(context, 
context.getRoute("route1"));
+        route.init(context.getManagementStrategy());
+        route.completedExchange(template.send("direct:start", e -> 
e.getMessage().setBody("Hello")), 5);
+
+        // the count is updated without a lock, so concurrent exchanges can 
take it one past the window
+        Field count = 
ManagedPerformanceCounter.class.getDeclaredField("percentileCount");
+        count.setAccessible(true);
+        count.setInt(route, 1025);
+
+        route.getProcessingTimeP50();
+        parse(route.dumpStatsAsXml(true));
+    }
+
+    @Test
+    public void testResetResetsLoad() throws Exception {
+        ManagedRoute route = new ManagedRoute(context, 
context.getRoute("route1"));
+        route.init(context.getManagementStrategy());
+        Field field = ManagedRoute.class.getDeclaredField("load");
+        field.setAccessible(true);
+        LoadTriplet load = (LoadTriplet) field.get(route);
+        load.update(5);
+        assertFalse(route.getLoad01().isEmpty());
+
+        route.reset();
+        assertEquals("", route.getLoad01());
+    }
+
+    @Test
+    public void testDumpRouteStatsWithQuoteInGroup() throws Exception {
+        template.sendBody("direct:other", "Hello");
+        parse(mcc().getManagedRoute("other").dumpRouteStatsAsXml(true, true));
+        parse(mcc().getManagedCamelContext().dumpRoutesStatsAsXml(true, true));
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start").routeId("route1")
+                        .step("foo")
+                        .to("log:foo").id("fooLog")
+                        .end()
+                        .to("mock:result");
+
+                from("direct:other").routeId("other").group("my\"group")
+                        .step("bar")
+                        .to("log:bar").id("otherLog")
+                        .end();
+
+                from("direct:star").routeId("rou*")
+                        .to("log:star").id("starLog");
+            }
+        };
+    }
+}

Reply via email to