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 eb341770d40a CAMEL-26780: camel-core - the runtime endpoint registry
counts an input hit when an exchange enters the route (#29476)
eb341770d40a is described below
commit eb341770d40a61f975449c751cd23aff48584caa
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 9 15:05:41 2026 +0100
CAMEL-26780: camel-core - the runtime endpoint registry counts an input hit
when an exchange enters the route (#29476)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../resilience4j/ResilienceInputHitsTest.java | 81 +++++++++++++++
.../engine/DefaultRuntimeEndpointRegistry.java | 81 +++++++++------
.../management/ManagedEndpointInputHitsTest.java | 110 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 9 ++
4 files changed, 253 insertions(+), 28 deletions(-)
diff --git
a/components/camel-resilience4j/src/test/java/org/apache/camel/component/resilience4j/ResilienceInputHitsTest.java
b/components/camel-resilience4j/src/test/java/org/apache/camel/component/resilience4j/ResilienceInputHitsTest.java
new file mode 100644
index 000000000000..2f47bd2129ac
--- /dev/null
+++
b/components/camel-resilience4j/src/test/java/org/apache/camel/component/resilience4j/ResilienceInputHitsTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.component.resilience4j;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ManagementStatisticsLevel;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.spi.RuntimeEndpointRegistry;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * The circuit breaker runs a copy of the exchange with its own unit of work:
the copy is no second input of the route.
+ */
+public class ResilienceInputHitsTest extends CamelTestSupport {
+
+ @Override
+ protected boolean useJmx() {
+ return true;
+ }
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+
context.getManagementStrategy().getManagementAgent().setStatisticsLevel(ManagementStatisticsLevel.Extended);
+ return context;
+ }
+
+ @Test
+ public void testOneInputPerMessage() throws Exception {
+ getMockEndpoint("mock:result").expectedBodiesReceived("in stock", "in
stock", "in stock");
+ for (int i = 0; i < 3; i++) {
+ template.sendBody("seda:check", "CAMEL-MUG");
+ }
+ MockEndpoint.assertIsSatisfied(context);
+
+ assertEquals(3, hits("check", "seda://check", "in"));
+ assertEquals(3, hits("supplier", "direct://supplier", "in"));
+ }
+
+ private long hits(String routeId, String uri, String direction) {
+ return
context.getRuntimeEndpointRegistry().getEndpointStatistics().stream()
+ .filter(s -> routeId.equals(s.getRouteId()) &&
uri.equals(s.getUri()) && direction.equals(s.getDirection()))
+ .mapToLong(RuntimeEndpointRegistry.Statistic::getHits).sum();
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("seda:check").routeId("check")
+ .circuitBreaker()
+ .to("direct:supplier")
+ .onFallback()
+ .transform().constant("not called")
+ .end()
+ .to("mock:result");
+
+
from("direct:supplier").routeId("supplier").transform().constant("in stock");
+ }
+ };
+ }
+}
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRuntimeEndpointRegistry.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRuntimeEndpointRegistry.java
index 0fc53cfee529..83e840dc5c91 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRuntimeEndpointRegistry.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRuntimeEndpointRegistry.java
@@ -28,10 +28,10 @@ import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
import org.apache.camel.ExchangePropertyKey;
import org.apache.camel.Message;
+import org.apache.camel.Route;
import org.apache.camel.StreamCache;
import org.apache.camel.spi.CamelEvent;
import org.apache.camel.spi.CamelEvent.ExchangeCompletedEvent;
-import org.apache.camel.spi.CamelEvent.ExchangeCreatedEvent;
import org.apache.camel.spi.CamelEvent.ExchangeFailedEvent;
import org.apache.camel.spi.CamelEvent.ExchangeSendingEvent;
import org.apache.camel.spi.CamelEvent.ExchangeSentEvent;
@@ -45,6 +45,7 @@ import org.apache.camel.support.EndpointSizeStatistics;
import org.apache.camel.support.EventNotifierSupport;
import org.apache.camel.support.ExchangeHelper;
import org.apache.camel.support.LRUCacheFactory;
+import org.apache.camel.support.RoutePolicySupport;
import org.apache.camel.support.service.ServiceHelper;
import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
@@ -65,6 +66,7 @@ public class DefaultRuntimeEndpointRegistry extends
EventNotifierSupport impleme
private EndpointUtilizationStatistics outputUtilization;
private EndpointSizeStatistics inputSizeStats;
private EndpointSizeStatistics outputSizeStats;
+ private final RoutePolicySupport inputHits = new InputHits();
@Override
public boolean isEnabled() {
@@ -212,6 +214,13 @@ public class DefaultRuntimeEndpointRegistry extends
EventNotifierSupport impleme
outputSizeStats = new EndpointSizeStatistics(limit);
}
if (extended) {
+ // the input hits of a route are counted when an exchange enters
it through its consumer, as the route
+ // counts its exchanges: the copies EIPs make inside a route
(circuit breaker, split, multicast, recipient
+ // list) have their own unit of work and ExchangeCreatedEvent but
are no input, and an exchange that comes
+ // from another route over direct is one without an
ExchangeCreatedEvent.
+ // One shared instance for all routes: InputHits keeps no
per-route state, the counts live in the
+ // thread-safe inputUtilization
+ getCamelContext().addRoutePolicyFactory((context, routeId, route)
-> inputHits);
LOG.debug(
"Runtime endpoint registry is in extended mode gathering
usage statistics of all incoming and outgoing endpoints (cache limit: {})",
limit);
@@ -285,31 +294,6 @@ public class DefaultRuntimeEndpointRegistry extends
EventNotifierSupport impleme
}
}
}
- } else if (extended && event instanceof ExchangeCreatedEvent ece) {
- // we only capture details in extended mode
- Exchange exchange = ece.getExchange();
- Endpoint endpoint = exchange.getFromEndpoint();
- if (endpoint != null) {
- String routeId = exchange.getFromRouteId();
- String uri = endpoint.getEndpointUri();
- String key = asUtilizationKey(routeId, uri);
- if (key != null) {
- inputUtilization.onHit(key);
- if (messageSizeEnabled) {
- Message message = exchange.getIn();
- MessageSizeStrategy strategy =
getCamelContext().getMessageSizeStrategy();
- long bodySize = strategy.computeBodySize(message);
- long headersSize =
strategy.computeHeadersSize(message);
- inputSizeStats.onHit(key, bodySize, headersSize);
-
exchange.setProperty(ExchangePropertyKey.MESSAGE_BODY_SIZE, bodySize);
-
exchange.setProperty(ExchangePropertyKey.MESSAGE_HEADERS_SIZE, headersSize);
- // reset stream cache so the body is re-readable
during routing
- if (message.getBody() instanceof StreamCache sc) {
- sc.reset();
- }
- }
- }
- }
} else if (event instanceof ExchangeSendingEvent ese) {
Endpoint endpoint = ese.getEndpoint();
Exchange exchange = ese.getExchange();
@@ -411,8 +395,7 @@ public class DefaultRuntimeEndpointRegistry extends
EventNotifierSupport impleme
@Override
public boolean isEnabled(CamelEvent event) {
- return enabled && event instanceof ExchangeCreatedEvent
- || event instanceof ExchangeSendingEvent
+ return event instanceof ExchangeSendingEvent
|| event instanceof ExchangeSentEvent
|| event instanceof ExchangeCompletedEvent
|| event instanceof ExchangeFailedEvent
@@ -430,6 +413,48 @@ public class DefaultRuntimeEndpointRegistry extends
EventNotifierSupport impleme
return null;
}
+ /**
+ * Counts an input hit of the route the exchange enters, under the URI of
the route's consumer (which differs from
+ * the endpoint of the route when a component delegates to another
consumer, as rest-openapi does to platform-http).
+ */
+ private void onRouteInput(Route route, Exchange exchange) {
+ Endpoint endpoint = route.getConsumer() != null ?
route.getConsumer().getEndpoint() : route.getEndpoint();
+ String key = endpoint != null ? asUtilizationKey(route.getRouteId(),
endpoint.getEndpointUri()) : null;
+ if (key == null || inputUtilization == null) {
+ return;
+ }
+ inputUtilization.onHit(key);
+ if (messageSizeEnabled) {
+ Message message = exchange.getIn();
+ MessageSizeStrategy strategy =
getCamelContext().getMessageSizeStrategy();
+ long bodySize = strategy.computeBodySize(message);
+ long headersSize = strategy.computeHeadersSize(message);
+ inputSizeStats.onHit(key, bodySize, headersSize);
+ exchange.setProperty(ExchangePropertyKey.MESSAGE_BODY_SIZE,
bodySize);
+ exchange.setProperty(ExchangePropertyKey.MESSAGE_HEADERS_SIZE,
headersSize);
+ // reset stream cache so the body is re-readable during routing
+ if (message.getBody() instanceof StreamCache sc) {
+ sc.reset();
+ }
+ }
+ }
+
+ /** The route policy of every route in extended mode: an exchange that
begins in a route is an input hit. */
+ private final class InputHits extends RoutePolicySupport {
+
+ @Override
+ public void onExchangeBegin(Route route, Exchange exchange) {
+ if (extended && enabled) {
+ onRouteInput(route, exchange);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "RuntimeEndpointRegistryInputHits";
+ }
+ }
+
private static String asUtilizationKey(String routeId, String uri) {
if (routeId == null || uri == null) {
return null;
diff --git
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedEndpointInputHitsTest.java
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedEndpointInputHitsTest.java
new file mode 100644
index 000000000000..f17da64fe0ee
--- /dev/null
+++
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedEndpointInputHitsTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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 org.apache.camel.CamelContext;
+import org.apache.camel.ManagementStatisticsLevel;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.spi.RuntimeEndpointRegistry;
+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;
+
+/**
+ * The input hits of a route are the messages its consumer received: the
sub-exchanges of a split, multicast or
+ * recipient list are no input of the route, and a route that consumes from
direct has an input hit per message it gets.
+ */
+@DisabledOnOs(OS.AIX)
+public class ManagedEndpointInputHitsTest extends ManagementTestSupport {
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext camelContext = super.createCamelContext();
+
camelContext.getManagementStrategy().getManagementAgent().setStatisticsLevel(ManagementStatisticsLevel.Extended);
+ return camelContext;
+ }
+
+ @Test
+ public void testSplitIsOneInput() throws Exception {
+ getMockEndpoint("mock:split").expectedMessageCount(6);
+ template.sendBody("seda:split", "a,b,c");
+ template.sendBody("seda:split", "d,e,f");
+ MockEndpoint.assertIsSatisfied(context);
+
+ assertEquals(2, hits("split", "seda://split", "in"));
+ }
+
+ @Test
+ public void testMulticastIsOneInputAndAnInputOfTheRouteItCallsOverDirect()
throws Exception {
+ getMockEndpoint("mock:a").expectedMessageCount(1);
+ getMockEndpoint("mock:sub").expectedMessageCount(1);
+ template.sendBody("seda:multicast", "Hello World");
+ MockEndpoint.assertIsSatisfied(context);
+
+ assertEquals(1, hits("multicast", "seda://multicast", "in"));
+ assertEquals(1, hits("multicast", "direct://sub", "out"));
+ assertEquals(1, hits("sub", "direct://sub", "in"));
+ }
+
+ @Test
+ public void
testRecipientListIsOneInputAndAnInputOfTheRouteItCallsOverDirect() throws
Exception {
+ getMockEndpoint("mock:b").expectedMessageCount(2);
+ getMockEndpoint("mock:sub").expectedMessageCount(2);
+ template.sendBodyAndHeader("seda:list", "Hello World", "to",
"mock:b,direct:sub");
+ template.sendBodyAndHeader("seda:list", "Bye World", "to",
"mock:b,direct:sub");
+ MockEndpoint.assertIsSatisfied(context);
+
+ assertEquals(2, hits("list", "seda://list", "in"));
+ assertEquals(2, hits("sub", "direct://sub", "in"));
+ }
+
+ @Test
+ public void testDirectIsAnInputOfTheRouteThatConsumesIt() throws Exception
{
+ getMockEndpoint("mock:sub").expectedMessageCount(3);
+ for (int i = 0; i < 3; i++) {
+ template.sendBody("seda:call", "Hello " + i);
+ }
+ MockEndpoint.assertIsSatisfied(context);
+
+ assertEquals(3, hits("call", "seda://call", "in"));
+ assertEquals(3, hits("call", "direct://sub", "out"));
+ assertEquals(3, hits("sub", "direct://sub", "in"));
+ }
+
+ private long hits(String routeId, String uri, String direction) {
+ return
context.getRuntimeEndpointRegistry().getEndpointStatistics().stream()
+ .filter(s -> routeId.equals(s.getRouteId()) &&
uri.equals(s.getUri()) && direction.equals(s.getDirection()))
+ .mapToLong(RuntimeEndpointRegistry.Statistic::getHits).sum();
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("seda:split").routeId("split").split(body().tokenize(",")).to("mock:split");
+
from("seda:multicast").routeId("multicast").multicast().to("mock:a",
"direct:sub");
+ from("seda:list").routeId("list").recipientList(header("to"));
+ from("seda:call").routeId("call").to("direct:sub");
+ from("direct:sub").routeId("sub").to("mock:sub");
+ }
+ };
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 3f82626e4ff3..3bd88a9ea434 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -273,6 +273,15 @@ before calling an endpoint). Before, such a recovery was
not counted. `ExchangeH
`true` when the error handler redelivered the exchange, without looking at the
header only. An exchange whose failure
is continued (`continued(true)`) no longer keeps its redelivery counter, as
with a handled failure.
+=== camel-management - the input hits of the runtime endpoint registry are the
messages a route received
+
+The runtime endpoint registry (in extended statistics mode, as the
`RuntimeEndpointRegistry` MBean, the endpoints dev
+console and the Camel CLI show it) now counts an input hit of a route when an
exchange enters the route through its
+consumer, as the route counts its exchanges. Before, it counted every created
exchange: the copy of a Circuit Breaker
+and the sub-exchanges of a Split, Multicast or Recipient List were counted as
input hits of the route as well, and a
+route that consumes from `direct` (or got a message sent by a
`ProducerTemplate` to its `direct` endpoint) had no input
+hits at all.
+
=== camel-core - a step after a redelivered step gets all its redeliveries
When a step of a route was redelivered and then succeeded, a later failure (of
another step of the route, or of a