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

Croway 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 86d6e297ed23 CAMEL-24887: Fix route group MBean exchange counters to 
count once per exchange (#27576)
86d6e297ed23 is described below

commit 86d6e297ed23aeddb5c6ca99ae44238f794ff79c
Author: Torsten Mielke <[email protected]>
AuthorDate: Thu Oct 8 18:03:18 2026 +0200

    CAMEL-24887: Fix route group MBean exchange counters to count once per 
exchange (#27576)
    
    * CAMEL-24887: Fix route group MBean exchange counters to count once per 
exchange
    
    The CAMEL-24590 fix (4.22.1) correctly shared a single ManagedRouteGroup
    instance across all routes in a group, but broke exchange counting: the
    group MBean counted each exchange once per member route instead of once
    per group.
    
    Fix by deduplicating in ManagedRouteGroup using the same route stack
    level approach that ManagedCamelContext already uses (CAMEL-18557).
    A new UnitOfWork.routeStackLevel(Predicate<Route>) method filters the
    stack by group, so an exchange is only counted when the current route is
    the outermost route of its group on the stack.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-24887: Address review feedback - clarify wording and wireTap test
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-24887: Remove stray blank line in test configure()
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    ---------
    
    Co-authored-by: Torsten Mielke <[email protected]>
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../main/java/org/apache/camel/spi/UnitOfWork.java |  18 ++
 .../camel/impl/engine/DefaultUnitOfWork.java       |  21 +++
 ...aultUnitOfWorkRouteStackLevelPredicateTest.java | 131 +++++++++++++++
 .../camel/management/mbean/ManagedRouteGroup.java  |  28 +++
 .../ManagedRouteGroupExchangeCountTest.java        | 187 +++++++++++++++++++++
 .../ManagedRouteGroupFailuresHandledTest.java      |   4 +-
 .../camel/management/ManagedRouteGroupTest.java    |   6 +-
 .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc    |   7 +
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |   7 +
 .../modules/ROOT/pages/route-group.adoc            |   6 +
 10 files changed, 410 insertions(+), 5 deletions(-)

diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/UnitOfWork.java 
b/core/camel-api/src/main/java/org/apache/camel/spi/UnitOfWork.java
index 99870ee8c594..1204c1c71609 100644
--- a/core/camel-api/src/main/java/org/apache/camel/spi/UnitOfWork.java
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/UnitOfWork.java
@@ -247,6 +247,24 @@ public interface UnitOfWork {
      */
     int routeStackLevel(boolean includeRouteTemplate, boolean includeKamelet);
 
+    /**
+     * Gets the number of routes on the route stack that this {@link 
UnitOfWork} currently is being routed through,
+     * which match the given filter.
+     * <p/>
+     * For example to know whether the current route is the outermost route of 
a route group on the stack.
+     * <p/>
+     * The default returns {@code 0}, which means a custom {@link UnitOfWork} 
that does not override this method will
+     * not benefit from the route-group deduplication in {@code 
ManagedRouteGroup}. Implementations that maintain their
+     * own route stack should override this method.
+     *
+     * @param  filter the filter
+     * @return        the number of matching routes on the route stack
+     * @since         4.23
+     */
+    default int routeStackLevel(Predicate<Route> filter) {
+        return 0;
+    }
+
     /**
      * Whether the unit of work should call the before/after process methods 
or not.
      */
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultUnitOfWork.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultUnitOfWork.java
index 60ce4d4b8c3e..1035b1929bae 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultUnitOfWork.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultUnitOfWork.java
@@ -449,6 +449,27 @@ public class DefaultUnitOfWork implements UnitOfWork {
         }
     }
 
+    @Override
+    public int routeStackLevel(Predicate<Route> filter) {
+        lock.lock();
+        try {
+            int level = 0;
+            if (route != null && filter.test(route)) {
+                level++;
+            }
+            if (routeStack != null) {
+                for (Route r : routeStack) {
+                    if (filter.test(r)) {
+                        level++;
+                    }
+                }
+            }
+            return level;
+        } finally {
+            lock.unlock();
+        }
+    }
+
     private static int countRoute(Route r, boolean includeRouteTemplate, 
boolean includeKamelet) {
         if (r.isCreatedByKamelet()) {
             return includeKamelet ? 1 : 0;
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultUnitOfWorkRouteStackLevelPredicateTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultUnitOfWorkRouteStackLevelPredicateTest.java
new file mode 100644
index 000000000000..34b4574e258a
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultUnitOfWorkRouteStackLevelPredicateTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.engine;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Route;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.UnitOfWork;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit test for {@link 
DefaultUnitOfWork#routeStackLevel(java.util.function.Predicate)} (CAMEL-24887).
+ */
+public class DefaultUnitOfWorkRouteStackLevelPredicateTest extends 
ContextTestSupport {
+
+    @Test
+    public void testEmptyStackReturnsZero() throws Exception {
+        UnitOfWork uow = new DefaultUnitOfWork(createExchangeWithBody("test"));
+        assertThat(uow.routeStackLevel(r -> true)).isZero();
+    }
+
+    @Test
+    public void testSingleRouteMatchingGroup() throws Exception {
+        UnitOfWork uow = new DefaultUnitOfWork(createExchangeWithBody("test"));
+        Route routeA = context.getRoute("a");
+
+        uow.pushRoute(routeA);
+
+        assertThat(uow.routeStackLevel(r -> 
"alpha".equals(r.getGroup()))).isEqualTo(1);
+        assertThat(uow.routeStackLevel(r -> 
"beta".equals(r.getGroup()))).isZero();
+    }
+
+    @Test
+    public void testNestedRoutesInSameGroup() throws Exception {
+        UnitOfWork uow = new DefaultUnitOfWork(createExchangeWithBody("test"));
+        Route routeA = context.getRoute("a");
+        Route routeB = context.getRoute("b");
+
+        uow.pushRoute(routeA);
+        uow.pushRoute(routeB);
+
+        assertThat(uow.routeStackLevel(r -> 
"alpha".equals(r.getGroup()))).isEqualTo(2);
+    }
+
+    @Test
+    public void testNestedRoutesDifferentGroups() throws Exception {
+        UnitOfWork uow = new DefaultUnitOfWork(createExchangeWithBody("test"));
+        Route routeA = context.getRoute("a");
+        Route routeC = context.getRoute("c");
+
+        uow.pushRoute(routeA);
+        uow.pushRoute(routeC);
+
+        assertThat(uow.routeStackLevel(r -> 
"alpha".equals(r.getGroup()))).isEqualTo(1);
+        assertThat(uow.routeStackLevel(r -> 
"beta".equals(r.getGroup()))).isEqualTo(1);
+    }
+
+    @Test
+    public void testPopReducesLevel() throws Exception {
+        UnitOfWork uow = new DefaultUnitOfWork(createExchangeWithBody("test"));
+        Route routeA = context.getRoute("a");
+        Route routeB = context.getRoute("b");
+
+        uow.pushRoute(routeA);
+        uow.pushRoute(routeB);
+        assertThat(uow.routeStackLevel(r -> 
"alpha".equals(r.getGroup()))).isEqualTo(2);
+
+        uow.popRoute();
+        assertThat(uow.routeStackLevel(r -> 
"alpha".equals(r.getGroup()))).isEqualTo(1);
+
+        uow.popRoute();
+        assertThat(uow.routeStackLevel(r -> 
"alpha".equals(r.getGroup()))).isZero();
+    }
+
+    @Test
+    public void testRouteWithNoGroupDoesNotMatch() throws Exception {
+        UnitOfWork uow = new DefaultUnitOfWork(createExchangeWithBody("test"));
+        Route routeD = context.getRoute("d");
+
+        uow.pushRoute(routeD);
+
+        assertThat(uow.routeStackLevel(r -> 
"alpha".equals(r.getGroup()))).isZero();
+        assertThat(uow.routeStackLevel(r -> r.getGroup() == 
null)).isEqualTo(1);
+    }
+
+    @Test
+    public void testMixedGroupsAndNoGroup() throws Exception {
+        UnitOfWork uow = new DefaultUnitOfWork(createExchangeWithBody("test"));
+        Route routeA = context.getRoute("a");
+        Route routeD = context.getRoute("d");
+        Route routeC = context.getRoute("c");
+
+        uow.pushRoute(routeA);
+        uow.pushRoute(routeD);
+        uow.pushRoute(routeC);
+
+        assertThat(uow.routeStackLevel(r -> 
"alpha".equals(r.getGroup()))).isEqualTo(1);
+        assertThat(uow.routeStackLevel(r -> 
"beta".equals(r.getGroup()))).isEqualTo(1);
+        assertThat(uow.routeStackLevel(r -> r.getGroup() == 
null)).isEqualTo(1);
+        assertThat(uow.routeStackLevel(r -> true)).isEqualTo(3);
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:a").routeId("a").routeGroup("alpha").to("mock:a");
+                from("direct:b").routeId("b").routeGroup("alpha").to("mock:b");
+                from("direct:c").routeId("c").routeGroup("beta").to("mock:c");
+                from("direct:d").routeId("d").to("mock:d");
+            }
+        };
+    }
+}
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 dd554ff13bb4..6aedd0b5b58a 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
@@ -20,6 +20,7 @@ import java.util.Date;
 import java.util.List;
 
 import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
 import org.apache.camel.ManagementStatisticsLevel;
 import org.apache.camel.Route;
 import org.apache.camel.ServiceStatus;
@@ -27,6 +28,7 @@ import org.apache.camel.api.management.ManagedResource;
 import org.apache.camel.api.management.mbean.ManagedRouteGroupMBean;
 import org.apache.camel.api.management.mbean.RouteError;
 import org.apache.camel.spi.ManagementStrategy;
+import org.apache.camel.spi.UnitOfWork;
 import org.apache.camel.util.TimeUtils;
 
 @ManagedResource(description = "Managed Route Group")
@@ -213,6 +215,32 @@ public class ManagedRouteGroup extends 
ManagedPerformanceCounter implements Mana
         }
     }
 
+    @Override
+    public void processExchange(Exchange exchange, String type) {
+        if (isOutermostRouteOfGroup(exchange)) {
+            super.processExchange(exchange, type);
+        }
+    }
+
+    @Override
+    public void completedExchange(Exchange exchange, long time) {
+        if (isOutermostRouteOfGroup(exchange)) {
+            super.completedExchange(exchange, time);
+        }
+    }
+
+    @Override
+    public void failedExchange(Exchange exchange) {
+        if (isOutermostRouteOfGroup(exchange)) {
+            super.failedExchange(exchange);
+        }
+    }
+
+    private boolean isOutermostRouteOfGroup(Exchange exchange) {
+        UnitOfWork uow = exchange.getUnitOfWork();
+        return uow == null || uow.routeStackLevel(r -> 
group.equals(r.getGroup())) <= 1;
+    }
+
     @Override
     public void onTimer() {
         super.onTimer();
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteGroupExchangeCountTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteGroupExchangeCountTest.java
new file mode 100644
index 000000000000..531420a4792c
--- /dev/null
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteGroupExchangeCountTest.java
@@ -0,0 +1,187 @@
+/*
+ * 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.concurrent.TimeUnit;
+
+import org.apache.camel.api.management.ManagedCamelContext;
+import org.apache.camel.api.management.mbean.ManagedRouteGroupMBean;
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Tests that a route group counts each exchange once per entry into the 
group: calls nested inside another route of the
+ * same group are not counted again (CAMEL-24887).
+ */
+@DisabledOnOs(OS.AIX)
+public class ManagedRouteGroupExchangeCountTest extends ManagementTestSupport {
+
+    @Test
+    public void testGroupCountsExchangeOnce() throws Exception {
+        ManagedCamelContext mcc = 
context.getCamelContextExtension().getContextPlugin(ManagedCamelContext.class);
+
+        // send 1 message through x1 -> x2 (both in group "g")
+        template.sendBody("direct:x1", "Hello");
+
+        ManagedRouteGroupMBean group = mcc.getManagedRouteGroup("g");
+        assertNotNull(group);
+        assertEquals(1, group.getExchangesCompleted());
+        assertEquals(1, group.getExchangesTotal());
+        assertEquals(0, group.getExchangesInflight());
+
+        // send 2 more messages
+        template.sendBody("direct:x1", "World");
+        template.sendBody("direct:x1", "!");
+
+        assertEquals(3, group.getExchangesCompleted());
+        assertEquals(3, group.getExchangesTotal());
+    }
+
+    @Test
+    public void testGroupCountsOnceWhenRoutingThroughNonGroupRoute() throws 
Exception {
+        ManagedCamelContext mcc = 
context.getCamelContextExtension().getContextPlugin(ManagedCamelContext.class);
+
+        // send 1 message through x1 (group "g") -> y (no group) -> x2 (group 
"g")
+        template.sendBody("direct:x1-via-y", "Hello");
+
+        ManagedRouteGroupMBean group = mcc.getManagedRouteGroup("g");
+        assertNotNull(group);
+        assertEquals(1, group.getExchangesCompleted());
+        assertEquals(1, group.getExchangesTotal());
+    }
+
+    @Test
+    public void testGroupCountsWhenEntryIsOutsideGroup() throws Exception {
+        ManagedCamelContext mcc = 
context.getCamelContextExtension().getContextPlugin(ManagedCamelContext.class);
+
+        // send 1 message to a route outside the group that calls x2 (in group 
"g") directly
+        template.sendBody("direct:outside", "Hello");
+
+        ManagedRouteGroupMBean group = mcc.getManagedRouteGroup("g");
+        assertNotNull(group);
+        assertEquals(1, group.getExchangesCompleted());
+    }
+
+    @Test
+    public void testGroupCountsFailure() throws Exception {
+        ManagedCamelContext mcc = 
context.getCamelContextExtension().getContextPlugin(ManagedCamelContext.class);
+
+        // send 1 message that will fail in x2-fail (group "g")
+        try {
+            template.sendBody("direct:x1-fail", "Hello");
+        } catch (Exception e) {
+            // expected
+        }
+
+        ManagedRouteGroupMBean group = mcc.getManagedRouteGroup("g");
+        assertNotNull(group);
+        assertEquals(1, group.getExchangesFailed());
+        assertEquals(0, group.getExchangesCompleted());
+        assertEquals(0, group.getExchangesInflight());
+    }
+
+    @Test
+    public void testTwoGroupsOnOnePath() throws Exception {
+        ManagedCamelContext mcc = 
context.getCamelContextExtension().getContextPlugin(ManagedCamelContext.class);
+
+        // send 1 message through g1-route (group "g1") -> g2-route (group 
"g2")
+        template.sendBody("direct:g1-route", "Hello");
+
+        ManagedRouteGroupMBean group1 = mcc.getManagedRouteGroup("g1");
+        ManagedRouteGroupMBean group2 = mcc.getManagedRouteGroup("g2");
+        assertNotNull(group1);
+        assertNotNull(group2);
+        assertEquals(1, group1.getExchangesCompleted());
+        assertEquals(1, group2.getExchangesCompleted());
+    }
+
+    @Test
+    public void testWireTapGroupCounts() throws Exception {
+        ManagedCamelContext mcc = 
context.getCamelContextExtension().getContextPlugin(ManagedCamelContext.class);
+
+        getMockEndpoint("mock:wt-done").expectedMessageCount(1);
+
+        template.sendBody("direct:wt", "Hello");
+
+        assertMockEndpointsSatisfied();
+
+        // wait for the wireTapped exchange to complete in route "wt-target"
+        await().atMost(5, TimeUnit.SECONDS)
+                .untilAsserted(() -> assertEquals(1L, 
mcc.getManagedRoute("wt-target").getExchangesCompleted()));
+
+        // each route counts independently at the route level
+        assertEquals(1, mcc.getManagedRoute("wt").getExchangesCompleted());
+        assertEquals(1, 
mcc.getManagedRoute("wt-target").getExchangesCompleted());
+
+        // the group count is consistent with how the CamelContext MBean 
counts exchanges
+        long ctxCompleted = 
mcc.getManagedCamelContext().getExchangesCompleted();
+
+        ManagedRouteGroupMBean group = mcc.getManagedRouteGroup("g");
+        assertNotNull(group);
+        assertEquals(ctxCompleted, group.getExchangesCompleted());
+        assertEquals(ctxCompleted, group.getExchangesTotal());
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                // x1 -> x2, both in group "g"
+                from("direct:x1").routeId("x1").routeGroup("g")
+                        .to("direct:x2");
+                from("direct:x2").routeId("x2").routeGroup("g")
+                        .log("x2 done");
+
+                // x1 -> y (no group) -> x2, tests routing through non-group 
route
+                from("direct:x1-via-y").routeId("x1-via-y").routeGroup("g")
+                        .to("direct:y");
+                from("direct:y").routeId("y")
+                        .to("direct:x2");
+
+                // outside (no group) -> x2 (group "g")
+                from("direct:outside").routeId("outside")
+                        .to("direct:x2");
+
+                // failure path: x1-fail -> x2-fail (throws), both in group "g"
+                from("direct:x1-fail").routeId("x1-fail").routeGroup("g")
+                        .to("direct:x2-fail");
+                from("direct:x2-fail").routeId("x2-fail").routeGroup("g")
+                        .throwException(new RuntimeException("Test failure"));
+
+                // two groups: g1-route (group "g1") -> g2-route (group "g2")
+                from("direct:g1-route").routeId("g1-route").routeGroup("g1")
+                        .to("direct:g2-route");
+                from("direct:g2-route").routeId("g2-route").routeGroup("g2")
+                        .log("g2 done");
+
+                // wireTap: wt (group "g") wireTaps to wt-target (group "g")
+                from("direct:wt").routeId("wt").routeGroup("g")
+                        .wireTap("direct:wt-target");
+                from("direct:wt-target").routeId("wt-target").routeGroup("g")
+                        .to("mock:wt-done");
+            }
+        };
+    }
+
+}
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteGroupFailuresHandledTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteGroupFailuresHandledTest.java
index 38b7a53e1064..5e4127863ccb 100644
--- 
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteGroupFailuresHandledTest.java
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteGroupFailuresHandledTest.java
@@ -42,8 +42,8 @@ public class ManagedRouteGroupFailuresHandledTest extends 
ManagementTestSupport
         ManagedRouteGroupMBean group = mcc.getManagedRouteGroup("flow");
         assertNotNull(group);
 
-        // group stats must aggregate across all member routes: trigger, step1 
and step2 each completed once
-        assertEquals(3, group.getExchangesCompleted());
+        // group counts each exchange once (outermost route of the group)
+        assertEquals(1, group.getExchangesCompleted());
 
         // the handled failure recorded on the trigger route must be reflected 
at the group level
         assertEquals(1, group.getFailuresHandled());
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteGroupTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteGroupTest.java
index aee8516341c8..bdab8fb39e6f 100644
--- 
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteGroupTest.java
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteGroupTest.java
@@ -56,11 +56,11 @@ public class ManagedRouteGroupTest extends 
ManagementTestSupport {
         Integer size = (Integer) mbeanServer.getAttribute(on, "GroupSize");
         if ("first".equals(group)) {
             assertEquals(3, size);
-            // group stats aggregate across all member routes (start, a, e 
each processed the exchange)
-            assertEquals(3, val);
+            // start -> a and start -> e are nested: start is outermost, so 
counted once
+            assertEquals(1, val);
         } else {
             assertEquals(2, size);
-            // group stats aggregate across all member routes (c, d each 
processed the exchange)
+            // c and d are called sequentially by "start" (not nested), each 
is outermost for "second"
             assertEquals(2, val);
         }
 
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index 59d9bcc08758..c742eee2a6c5 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -13,6 +13,13 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation] 
page for details.
 
 == Upgrading from 4.22.1 to 4.22.2
 
+=== camel-management - Route group exchange counters count once per entry into 
the group again
+
+In 4.22.1, the route group MBean (`ManagedRouteGroupMBean`) exchange counters 
and processing times
+counted each exchange once per member route instead of once per entry into the 
group. This was an
+unintended side effect of CAMEL-24590. The counters now count each exchange 
once per entry into the
+group again, as in 4.22.0: calls nested inside another route of the same group 
are not counted again.
+
 === camel-netty-http - the roles of a security constraint are matched by role 
name
 
 The roles of a `SecurityConstraintMapping` inclusion are a comma-separated 
list of role names, and an authenticated
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index fd366329fbe7..34c6c60689ce 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -13,6 +13,13 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation] 
page for details.
 
 == Upgrading Camel 4.22 to 4.23
 
+=== camel-management - Route group exchange counters count once per entry into 
the group
+
+The route group MBean (`ManagedRouteGroupMBean`) exchange counters and 
processing times now count
+each exchange once per entry into the group: calls nested inside another route 
of the same group
+are not counted again. In 4.22.1, a regression (CAMEL-24590) caused them to 
count once per member
+route instead.
+
 === camel-core - Rest DSL response Content-Type
 
 Under json or xml binding, a response without a Content-Type header again 
takes it from the `produces` of the rest
diff --git a/docs/user-manual/modules/ROOT/pages/route-group.adoc 
b/docs/user-manual/modules/ROOT/pages/route-group.adoc
index 3e070cc84cd0..8157a157bfe8 100644
--- a/docs/user-manual/modules/ROOT/pages/route-group.adoc
+++ b/docs/user-manual/modules/ROOT/pages/route-group.adoc
@@ -177,6 +177,12 @@ Camel has optional support for xref:jmx.adoc[JMX 
management]. This includes mana
 The following code will get the JMX MBean `ManagedRouteGroupMBean` for the 
route group with name `order`.
 Using this MBean you can get metrics, and also operations to start and stop 
all routes in this group.
 
+Each exchange is counted once per entry into the group: calls nested inside 
another route of the
+same group are not counted again. For example, if route A (group G) calls 
route B (group G) via
+`direct:`, the group counts it once. However, if an outside route calls A and 
then B sequentially,
+each call is a separate entry, so the group counts two.
+The per-route statistics are available on each route's `ManagedRouteMBean`.
+
 For example to get the total number of `Exchange`(s) that has failed within 
this group:
 
 [source,java]

Reply via email to