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 7b5d129971f0 CAMEL-25716: camel-management - remote exchange counters
per route, for camel get processor --remote (#27996)
7b5d129971f0 is described below
commit 7b5d129971f07a59d90e4498fc2911dca8e7d442
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 17 09:07:49 2026 +0200
CAMEL-25716: camel-management - remote exchange counters per route, for
camel get processor --remote (#27996)
The routes count the exchanges that came from a remote endpoint, as the
CamelContext does (CAMEL-20879): ManagedRouteMBean has RemoteExchangesTotal,
Completed, Failed and Inflight. A route with a remote consumer uses its own
counters; a route with an internal consumer counts the exchanges that came from
a remote endpoint. The route dev console has the remote counters, so camel get
processor --remote shows remote/total pairs; camel get route and camel top
route have --remote, and the J [...]
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../apache/camel/catalog/dev-consoles-openapi.json | 15 ++
.../apache/camel/catalog/dev-consoles/route.json | 15 ++
.../org/apache/camel/dev-console/route.json | 15 ++
.../apache/camel/impl/console/RouteDevConsole.java | 4 +
.../management/mbean/ManagedCamelContextMBean.java | 2 +-
.../api/management/mbean/ManagedRouteMBean.java | 32 ++++
.../management/mbean/ManagedCamelContext.java | 2 +-
.../camel/management/mbean/ManagedRoute.java | 72 +++++++++
.../ManagedRouteRemoteExchangesTest.java | 161 +++++++++++++++++++++
.../modules/ROOT/pages/camel-jbang-managing.adoc | 6 +
.../jbang-commands/camel-jbang-get-route.adoc | 1 +
.../jbang-commands/camel-jbang-top-route.adoc | 1 +
.../META-INF/camel-jbang-commands-metadata.json | 4 +-
.../core/commands/process/CamelContextStatus.java | 4 +
.../commands/process/CamelProcessorStatus.java | 4 +
.../core/commands/process/CamelRouteStatus.java | 27 ++++
.../commands/process/CamelContextStatusTest.java | 29 ++++
.../commands/process/CamelProcessorStatusTest.java | 17 +++
.../commands/process/CamelRouteStatusTest.java | 25 ++++
19 files changed, 432 insertions(+), 4 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
index f0ac82aaa95a..af65e7bcd600 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
@@ -5891,6 +5891,18 @@
"type": "integer",
"description": "Number of
inflight exchanges"
},
+
"remoteExchangesTotal": {
+
"type": "integer",
+
"description": "Total number of
exchanges that came from a remote endpoint"
+
},
+
"remoteExchangesFailed": {
+
"type": "integer",
+
"description": "Number of
failed exchanges that came from a remote endpoint"
+
},
+
"remoteExchangesInflight": {
+
"type": "integer",
+
"description": "Number of
inflight exchanges that came from a remote endpoint"
+
},
"meanProcessingTime": {
"type": "integer",
"description": "Mean processing
time in milliseconds"
@@ -5945,6 +5957,9 @@
"exchangesTotal",
"exchangesFailed",
"exchangesInflight",
+
"remoteExchangesTotal",
+
"remoteExchangesFailed",
+
"remoteExchangesInflight",
"meanProcessingTime",
"maxProcessingTime",
"minProcessingTime"
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route.json
index c95b21423df2..1bb1f2388b79 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route.json
@@ -212,6 +212,18 @@
"type": "integer",
"description": "Number of inflight exchanges"
},
+ "remoteExchangesTotal": {
+ "type": "integer",
+ "description": "Total number of exchanges that came from a
remote endpoint"
+ },
+ "remoteExchangesFailed": {
+ "type": "integer",
+ "description": "Number of failed exchanges that came from a
remote endpoint"
+ },
+ "remoteExchangesInflight": {
+ "type": "integer",
+ "description": "Number of inflight exchanges that came from
a remote endpoint"
+ },
"meanProcessingTime": {
"type": "integer",
"description": "Mean processing time in milliseconds"
@@ -266,6 +278,9 @@
"exchangesTotal",
"exchangesFailed",
"exchangesInflight",
+ "remoteExchangesTotal",
+ "remoteExchangesFailed",
+ "remoteExchangesInflight",
"meanProcessingTime",
"maxProcessingTime",
"minProcessingTime"
diff --git
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route.json
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route.json
index c95b21423df2..1bb1f2388b79 100644
---
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route.json
+++
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route.json
@@ -212,6 +212,18 @@
"type": "integer",
"description": "Number of inflight exchanges"
},
+ "remoteExchangesTotal": {
+ "type": "integer",
+ "description": "Total number of exchanges that came from a
remote endpoint"
+ },
+ "remoteExchangesFailed": {
+ "type": "integer",
+ "description": "Number of failed exchanges that came from a
remote endpoint"
+ },
+ "remoteExchangesInflight": {
+ "type": "integer",
+ "description": "Number of inflight exchanges that came from
a remote endpoint"
+ },
"meanProcessingTime": {
"type": "integer",
"description": "Mean processing time in milliseconds"
@@ -266,6 +278,9 @@
"exchangesTotal",
"exchangesFailed",
"exchangesInflight",
+ "remoteExchangesTotal",
+ "remoteExchangesFailed",
+ "remoteExchangesInflight",
"meanProcessingTime",
"maxProcessingTime",
"minProcessingTime"
diff --git
a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDevConsole.java
b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDevConsole.java
index 50fba785f6f9..1bcd7d302a5f 100644
---
a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDevConsole.java
+++
b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDevConsole.java
@@ -67,6 +67,9 @@ public class RouteDevConsole extends AbstractDevConsole {
@Metadata(description = "Total number of exchanges") long
exchangesTotal,
@Metadata(description = "Number of failed exchanges") long
exchangesFailed,
@Metadata(description = "Number of inflight exchanges") long
exchangesInflight,
+ @Metadata(description = "Total number of exchanges that came from
a remote endpoint") long remoteExchangesTotal,
+ @Metadata(description = "Number of failed exchanges that came from
a remote endpoint") long remoteExchangesFailed,
+ @Metadata(description = "Number of inflight exchanges that came
from a remote endpoint") long remoteExchangesInflight,
@Metadata(description = "Mean processing time in milliseconds")
long meanProcessingTime,
@Metadata(description = "Max processing time in milliseconds")
long maxProcessingTime,
@Metadata(description = "Min processing time in milliseconds")
long minProcessingTime,
@@ -544,6 +547,7 @@ public class RouteDevConsole extends AbstractDevConsole {
coverage, hasLoad ? load1 : null, hasLoad ? load5 : null,
hasLoad ? load15 : null,
thp.isEmpty() ? null : thp,
mrb.getIdleSince(), mrb.getExchangesTotal(),
mrb.getExchangesFailed(), mrb.getExchangesInflight(),
+ mrb.getRemoteExchangesTotal(), mrb.getRemoteExchangesFailed(),
mrb.getRemoteExchangesInflight(),
mrb.getMeanProcessingTime(), mrb.getMaxProcessingTime(),
mrb.getMinProcessingTime(),
p50, p95, p99, lastProcessingTime, deltaProcessingTime,
lastCreated, lastCompleted, lastFailureHandled, lastFailed);
diff --git
a/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedCamelContextMBean.java
b/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedCamelContextMBean.java
index 9a27de8483f7..8b2640d55ec5 100644
---
a/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedCamelContextMBean.java
+++
b/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedCamelContextMBean.java
@@ -144,7 +144,7 @@ public interface ManagedCamelContextMBean extends
ManagedPerformanceCounterMBean
@ManagedAttribute(description = "Failed number of exchanges processed from
remote endpoints only")
long getRemoteExchangesFailed();
- @ManagedAttribute(description = "Total number of exchanges inflight from
remote endpoints only")
+ @ManagedAttribute(description = "Number of inflight exchanges from remote
endpoints only")
long getRemoteExchangesInflight();
@ManagedAttribute(description = "Whether breadcrumbs is in use")
diff --git
a/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedRouteMBean.java
b/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedRouteMBean.java
index ec05aa6110c9..25b5c673b98b 100644
---
a/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedRouteMBean.java
+++
b/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedRouteMBean.java
@@ -209,4 +209,36 @@ public interface ManagedRouteMBean extends
ManagedPerformanceCounterMBean {
@ManagedAttribute(description = "Whether the consumer connects to remote
or local systems")
boolean isRemoteEndpoint();
+ /**
+ * Total number of exchanges the route processed that came from a remote
endpoint.
+ *
+ * @since 4.24
+ */
+ @ManagedAttribute(description = "Total number of exchanges processed from
remote endpoints only")
+ long getRemoteExchangesTotal();
+
+ /**
+ * Completed (success) number of exchanges the route processed that came
from a remote endpoint.
+ *
+ * @since 4.24
+ */
+ @ManagedAttribute(description = "Completed (success) number of exchanges
processed from remote endpoints only")
+ long getRemoteExchangesCompleted();
+
+ /**
+ * Failed number of exchanges the route processed that came from a remote
endpoint.
+ *
+ * @since 4.24
+ */
+ @ManagedAttribute(description = "Failed number of exchanges processed from
remote endpoints only")
+ long getRemoteExchangesFailed();
+
+ /**
+ * Number of exchanges inflight in the route that came from a remote
endpoint.
+ *
+ * @since 4.24
+ */
+ @ManagedAttribute(description = "Number of inflight exchanges from remote
endpoints only")
+ long getRemoteExchangesInflight();
+
}
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 7960bb32c55d..342212324621 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
@@ -100,7 +100,7 @@ public class ManagedCamelContext extends
ManagedPerformanceCounter implements Ma
remoteExchangesTotal.reset();
remoteExchangesCompleted.reset();
remoteExchangesFailed.reset();
- remoteExchangesInflight.reset();
+ // do not reset remoteExchangesInflight
}
@Override
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 ef8271313045..9a54c5331053 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
@@ -86,10 +86,18 @@ public class ManagedRoute extends ManagedPerformanceCounter
implements ManagedRo
protected final CamelContext context;
private final LoadTriplet load = new LoadTriplet();
private final String jmxDomain;
+ // whether the consumer of the route is remote, so all its exchanges came
from a remote endpoint
+ private final boolean remoteConsumer;
+ // the exchanges from a remote endpoint that a route with an internal
consumer processed
+ private final Statistic remoteExchangesTotal = new StatisticCounter();
+ private final Statistic remoteExchangesCompleted = new StatisticCounter();
+ private final Statistic remoteExchangesFailed = new StatisticCounter();
+ private final Statistic remoteExchangesInflight = new StatisticCounter();
public ManagedRoute(CamelContext context, Route route) {
this.route = route;
this.context = context;
+ this.remoteConsumer = route.getEndpoint() != null &&
route.getEndpoint().isRemote();
this.description = route.getDescription();
this.note = route.getNote();
this.configurationId = route.getConfigurationId();
@@ -793,6 +801,70 @@ public class ManagedRoute extends
ManagedPerformanceCounter implements ManagedRo
public void reset() {
super.reset();
load.reset();
+ remoteExchangesTotal.reset();
+ remoteExchangesCompleted.reset();
+ remoteExchangesFailed.reset();
+ // do not reset remoteExchangesInflight
+ }
+
+ @Override
+ public void processExchange(Exchange exchange, String type) {
+ super.processExchange(exchange, type);
+ if (fromRemote(exchange)) {
+ remoteExchangesInflight.increment();
+ }
+ }
+
+ @Override
+ public void completedExchange(Exchange exchange, long time) {
+ super.completedExchange(exchange, time);
+ if (fromRemote(exchange)) {
+ remoteExchangesInflight.decrement();
+ if (isStatisticsEnabled()) {
+ remoteExchangesTotal.increment();
+ remoteExchangesCompleted.increment();
+ }
+ }
+ }
+
+ @Override
+ public void failedExchange(Exchange exchange) {
+ super.failedExchange(exchange);
+ if (fromRemote(exchange)) {
+ remoteExchangesInflight.decrement();
+ if (isStatisticsEnabled()) {
+ remoteExchangesTotal.increment();
+ remoteExchangesFailed.increment();
+ }
+ }
+ }
+
+ /**
+ * Whether the exchange came from a remote endpoint, counted by a route
with an internal consumer only: all the
+ * exchanges of a route with a remote consumer came from it, so its own
counters are its remote counters.
+ */
+ private boolean fromRemote(Exchange exchange) {
+ return !remoteConsumer && exchange.getFromEndpoint() != null &&
exchange.getFromEndpoint().isRemote();
+ }
+
+ @Override
+ public long getRemoteExchangesTotal() {
+ return remoteConsumer ? getExchangesTotal() :
remoteExchangesTotal.getValue();
+ }
+
+ @Override
+ public long getRemoteExchangesCompleted() {
+ return remoteConsumer ? getExchangesCompleted() :
remoteExchangesCompleted.getValue();
+ }
+
+ @Override
+ public long getRemoteExchangesFailed() {
+ return remoteConsumer ? getExchangesFailed() :
remoteExchangesFailed.getValue();
+ }
+
+ @Override
+ public long getRemoteExchangesInflight() {
+ return remoteConsumer ? getExchangesInflight() :
remoteExchangesInflight.getValue();
}
@Override
diff --git
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteRemoteExchangesTest.java
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteRemoteExchangesTest.java
new file mode 100644
index 000000000000..3a61a518a308
--- /dev/null
+++
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteRemoteExchangesTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Endpoint;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.direct.DirectComponent;
+import org.apache.camel.component.direct.DirectEndpoint;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import static
org.apache.camel.management.DefaultManagementObjectNameStrategy.TYPE_ROUTE;
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The remote exchange counters of the routes: the exchanges a route processed
that came from a remote endpoint. Here
+ * the remote endpoint is a direct endpoint that says it is remote.
+ */
+@DisabledOnOs(OS.AIX)
+public class ManagedRouteRemoteExchangesTest extends ManagementTestSupport {
+
+ private final CountDownLatch held = new CountDownLatch(1);
+ private final CountDownLatch release = new CountDownLatch(1);
+
+ @Test
+ public void testRemoteExchangesOfTheRoutes() throws Exception {
+ getMockEndpoint("mock:result").expectedMessageCount(3);
+
+ template.sendBody("remote:orders", "order 1");
+ template.sendBody("remote:orders", "order 2");
+ template.sendBody("direct:internal", "tick");
+ assertThrows(Exception.class, () -> template.sendBody("remote:orders",
"boom"));
+
+ assertMockEndpointsSatisfied();
+
+ // a route with a remote consumer: all its exchanges came from a
remote endpoint
+ assertCounters("orders", 3, 2, 1, 0);
+ // a route with an internal consumer that a remote and an internal
route call: only some came from remote
+ assertExchangesTotal("audit", 4);
+ assertCounters("audit", 3, 2, 1, 0);
+ // a route with an internal consumer called internally
+ assertCounters("internal", 0, 0, 0, 0);
+ }
+
+ @Test
+ public void testResetTheRemoteExchanges() throws Exception {
+ template.sendBody("remote:orders", "order 1");
+
+ MBeanServer mbeanServer = getMBeanServer();
+ mbeanServer.invoke(getCamelObjectName(TYPE_ROUTE, "audit"), "reset",
null, null);
+
+ assertCounters("audit", 0, 0, 0, 0);
+ }
+
+ @Test
+ public void testResetKeepsTheRemoteExchangesInflight() throws Exception {
+ // an exchange from remote that is in the route while the counters are
reset
+ CompletableFuture<Object> reply =
template.asyncRequestBody("remote:held", "order");
+ assertTrue(held.await(10, TimeUnit.SECONDS));
+
+ MBeanServer mbeanServer = getMBeanServer();
+ mbeanServer.invoke(getCamelObjectName(TYPE_ROUTE, "hold"), "reset",
null, null);
+ mbeanServer.invoke(getContextObjectName(), "reset", null, null);
+
+ assertCounters("hold", 0, 0, 0, 1);
+ assertEquals(1L, (Long)
mbeanServer.getAttribute(getContextObjectName(), "RemoteExchangesInflight"));
+
+ release.countDown();
+ reply.get(10, TimeUnit.SECONDS);
+
+ // the inflight counters go back to 0, and not below
+ await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
+ assertCounters("hold", 1, 1, 0, 0);
+ assertEquals(0L, (Long)
mbeanServer.getAttribute(getContextObjectName(), "RemoteExchangesInflight"));
+ });
+ }
+
+ private void assertCounters(String routeId, long total, long completed,
long failed, long inflight)
+ throws Exception {
+ MBeanServer mbeanServer = getMBeanServer();
+ ObjectName on = getCamelObjectName(TYPE_ROUTE, routeId);
+ assertEquals(total, (Long) mbeanServer.getAttribute(on,
"RemoteExchangesTotal"), routeId + " total");
+ assertEquals(completed, (Long) mbeanServer.getAttribute(on,
"RemoteExchangesCompleted"), routeId + " completed");
+ assertEquals(failed, (Long) mbeanServer.getAttribute(on,
"RemoteExchangesFailed"), routeId + " failed");
+ assertEquals(inflight, (Long) mbeanServer.getAttribute(on,
"RemoteExchangesInflight"), routeId + " inflight");
+ }
+
+ private void assertExchangesTotal(String routeId, long total) throws
Exception {
+ ObjectName on = getCamelObjectName(TYPE_ROUTE, routeId);
+ assertEquals(total, (Long) getMBeanServer().getAttribute(on,
"ExchangesTotal"), routeId + " exchanges");
+ }
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+ // a direct component with remote endpoints, as a messaging broker or
an http server has
+ context.addComponent("remote", new DirectComponent() {
+ @Override
+ protected Endpoint createEndpoint(String uri, String remaining,
Map<String, Object> parameters)
+ throws Exception {
+ Endpoint endpoint = new DirectEndpoint(uri, this) {
+ @Override
+ public boolean isRemote() {
+ return true;
+ }
+ };
+ setProperties(endpoint, parameters);
+ return endpoint;
+ }
+ });
+ return context;
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("remote:orders").routeId("orders").to("direct:audit");
+ from("direct:internal").routeId("internal").to("direct:audit");
+ from("direct:audit").routeId("audit")
+ .filter(body().isEqualTo("boom")).throwException(new
IllegalArgumentException("Forced")).end()
+ .to("mock:result");
+ from("remote:held").routeId("held").to("direct:hold");
+ from("direct:hold").routeId("hold").process(exchange -> {
+ held.countDown();
+ if (!release.await(10, TimeUnit.SECONDS)) {
+ throw new IllegalStateException("The exchange was not
released");
+ }
+ });
+ }
+ };
+ }
+}
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
index d46af59be16d..9293339bb643 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
@@ -148,6 +148,12 @@ camel get route
63051 dude java timer://java?period=1000 Running 46s 46
0 0 0 0 9 0s/0s/-
----
+Use `--remote` to break down TOTAL, FAILED and INFLIGHT into remote/total
pairs: the exchanges that came from a remote
+endpoint, such as Kafka or HTTP, of all the exchanges of the route. A route
with a remote consumer has only remote
+exchanges, and a route such as `direct:audit` has the remote exchanges of the
routes that call it. The same works with
+`camel get processor --remote` and `camel top route --remote`, and the JSON of
these commands has the remote counters
+as `remoteTotal`, `remoteFailed` and `remoteInflight`.
+
TIP: Run `camel get --help` to see all available sub-commands.
== Top status
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-route.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-route.adoc
index 901d238b73f2..7e5f3770b2c4 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-route.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-route.adoc
@@ -28,6 +28,7 @@ camel get route [options]
| `--json` | Output in JSON Format | | boolean
| `--limit` | Filter routes by limiting to the given number of rows | | int
| `--note` | Include note in the ID column (if available) | | boolean
+| `--remote` | Break down counters into remote/total pairs | | boolean
| `--running` | Only include running routes | | boolean
| `--short-uri` | List endpoint URI without query parameters (short) | |
boolean
| `--show-group` | Include group column | | boolean
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-top-route.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-top-route.adoc
index 8e14f55227dc..889a4ec5d2b0 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-top-route.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-top-route.adoc
@@ -28,6 +28,7 @@ camel top route [options]
| `--json` | Output in JSON Format | | boolean
| `--limit` | Filter routes by limiting to the given number of rows | | int
| `--note` | Include note in the ID column (if available) | | boolean
+| `--remote` | Break down counters into remote/total pairs | | boolean
| `--running` | Only include running routes | | boolean
| `--short-uri` | List endpoint URI without query parameters (short) | |
boolean
| `--show-group` | Include group column | | boolean
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index ee4d49be23bb..b26b0f3e4961 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -15,7 +15,7 @@
{ "name": "eval", "fullName": "eval", "description": "Evaluate Camel
expressions and scripts", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.EvalCommand", "options": [ { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "expression",
"fullName": "eval expression", "description": "Evaluates Camel expression",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.EvalEx [...]
{ "name": "explain", "fullName": "explain", "description": "Explain what a
Camel route does using AI\/LLM", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Explain", "options": [ { "names":
"--api-key", "description": "API key for authentication. Also reads
ANTHROPIC_API_KEY, OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars",
"javaType": "java.lang.String", "type": "string" }, { "names": "--api-type",
"description": "API type: 'ollama', 'openai' (OpenAI-compatible), ' [...]
{ "name": "export", "fullName": "export", "description": "Export to other
runtimes (Camel Main, Spring Boot, or Quarkus)", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Export", "options": [ { "names":
"--build-property", "description": "Maven build properties, ex.
--build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, {
"names": "--camel-spring-boot-version", "description": "Camel version to use
with Spring Boot", "javaType": "java.lang.String", "ty [...]
- { "name": "get", "fullName": "get", "description": "Get status of Camel
integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ {
"names": "--watch", "description": "Execute periodically and showing output
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "activity",
"fullName": " [...]
+ { "name": "get", "fullName": "get", "description": "Get status of Camel
integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ {
"names": "--watch", "description": "Execute periodically and showing output
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "activity",
"fullName": " [...]
{ "name": "harden", "fullName": "harden", "description": "Suggest security
hardening for Camel routes using AI\/LLM", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Harden", "options": [ { "names":
"--api-key", "description": "API key for authentication. Also reads
OPENAI_API_KEY or LLM_API_KEY env vars", "javaType": "java.lang.String",
"type": "string" }, { "names": "--api-type", "description": "API type: 'ollama'
or 'openai' (OpenAI-compatible)", "defaultValue": "ollama", [...]
{ "name": "hawtio", "fullName": "hawtio", "description": "Launch Hawtio
web console", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.Hawtio", "options": [ {
"names": "--host", "description": "Hostname to bind the Hawtio web console to",
"defaultValue": "127.0.0.1", "javaType": "java.lang.String", "type": "string"
}, { "names": "--open-url,--openUrl", "description": "To automatic open Hawtio
web console in the web browser (without an interactive terminal the default
[...]
{ "name": "infra", "fullName": "infra", "description": "List and Run
external services for testing and prototyping", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.infra.InfraCommand", "options": [ {
"names": "--json", "description": "Output in JSON Format", "javaType":
"boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display
the help and sub-commands", "javaType": "boolean", "type": "boolean" } ],
"subcommands": [ { "name": "get", "fullName": "infra [...]
@@ -33,7 +33,7 @@
{ "name": "semantic", "fullName": "semantic", "description": "List
semantic definitions and expert contracts", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.action.CamelSemantic", "options": [ {
"names": "--expert", "description": "Show the operations and parameter contract
of this expert", "javaType": "java.lang.String", "type": "string" }, { "names":
"--json", "description": "Output a single JSON document", "javaType":
"boolean", "type": "boolean" }, { "names": "--timeou [...]
{ "name": "shell", "fullName": "shell", "description": "Interactive Camel
CLI shell.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Shell",
"options": [ { "names": "-h,--help", "description": "Display the help and
sub-commands", "javaType": "boolean", "type": "boolean" } ] },
{ "name": "stop", "fullName": "stop", "description": "Shuts down running
Camel integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.StopProcess", "options": [ {
"names": "--kill", "description": "To force killing the process (SIGKILL)",
"javaType": "boolean", "type": "boolean" }, { "names": "--no-wait",
"description": "Do not wait for the integration to stop", "javaType":
"boolean", "type": "boolean" }, { "names": "--timeout", "description": "How
long to wa [...]
- { "name": "top", "fullName": "top", "description": "Top status of Camel
integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.CamelTop", "options": [ {
"names": "--watch", "description": "Execute periodically and showing output
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "context",
"fullName": "top [...]
+ { "name": "top", "fullName": "top", "description": "Top status of Camel
integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.CamelTop", "options": [ {
"names": "--watch", "description": "Execute periodically and showing output
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "context",
"fullName": "top [...]
{ "name": "trace", "fullName": "trace", "description": "Tail message
traces from running Camel integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.action.CamelTraceAction", "options":
[ { "names": "--action", "description": "Action to start, stop, clear, list
status, or dump traces", "defaultValue": "status", "javaType":
"java.lang.String", "type": "string" }, { "names": "--ago", "description": "Use
ago instead of yyyy-MM-dd HH:mm:ss in timestamp.", "javaType": "b [...]
{ "name": "transform", "fullName": "transform", "description": "Transform
message or Camel routes", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.TransformCommand", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"dataweave", "fullName": "transform dataweave", "description": "Convert
DataWeave scripts to DataSonnet format", "sourceClass": "org.apache.camel.dsl.
[...]
{ "name": "update", "fullName": "update", "description": "Update Camel
project", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.update.UpdateCommand", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"list", "fullName": "update list", "description": "List available update
versions for Camel and its runtime variants", "sourceClass":
"org.apache.camel.dsl.jbang.cor [...]
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelContextStatus.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelContextStatus.java
index 2f95ab98c862..3677f21a0e37 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelContextStatus.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelContextStatus.java
@@ -194,6 +194,10 @@ public class CamelContextStatus extends
ProcessWatchCommand {
jo.put("total", JsonValues.number(r.total));
jo.put("failed", JsonValues.number(r.failed));
jo.put("inflight", JsonValues.number(r.inflight));
+ // the exchanges that came from a remote endpoint
+ jo.put("remoteTotal", JsonValues.number(r.totalRemote));
+ jo.put("remoteFailed", JsonValues.number(r.failedRemote));
+ jo.put("remoteInflight",
JsonValues.number(r.inflightRemote));
jo.put("last", JsonValues.number(r.last));
jo.put("sinceLastStarted",
JsonValues.text(r.sinceLastStarted));
jo.put("sinceLastCompleted",
JsonValues.text(r.sinceLastCompleted));
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorStatus.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorStatus.java
index 1b8a23a573f6..943440279ea1 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorStatus.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorStatus.java
@@ -334,6 +334,10 @@ public class CamelProcessorStatus extends
ProcessWatchCommand {
jo.put("total", JsonValues.number(r.total));
jo.put("failed", JsonValues.number(r.failed));
jo.put("inflight", JsonValues.number(r.inflight));
+ // the exchanges that came from a remote endpoint (of the
routes only)
+ jo.put("remoteTotal", JsonValues.number(r.totalRemote));
+ jo.put("remoteFailed", JsonValues.number(r.failedRemote));
+ jo.put("remoteInflight", JsonValues.number(r.inflightRemote));
// processing times in millis
jo.put("mean", JsonValues.number(r.mean));
jo.put("min", JsonValues.number(r.min));
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatus.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatus.java
index d8c7f3ce2027..76af869d1fbd 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatus.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatus.java
@@ -66,6 +66,10 @@ public class CamelRouteStatus extends ProcessWatchCommand {
description = "Prefer to display source filename/code
instead of IDs")
boolean source;
+ @CommandLine.Option(names = { "--remote" },
+ description = "Break down counters into remote/total
pairs")
+ boolean remote;
+
@CommandLine.Option(names = { "--short-uri" },
description = "List endpoint URI without query
parameters (short)")
boolean shortUri;
@@ -180,6 +184,10 @@ public class CamelRouteStatus extends ProcessWatchCommand {
row.total = stats.get("exchangesTotal").toString();
row.inflight = stats.get("exchangesInflight").toString();
row.failed = stats.get("exchangesFailed").toString();
+ // the exchanges that came from a remote endpoint (Camel
4.24 onwards)
+ row.totalRemote = text(stats.get("remoteExchangesTotal"));
+ row.failedRemote =
text(stats.get("remoteExchangesFailed"));
+ row.inflightRemote =
text(stats.get("remoteExchangesInflight"));
row.mean = stats.get("meanProcessingTime").toString();
if ("-1".equals(row.mean)) {
row.mean = null;
@@ -337,6 +345,9 @@ public class CamelRouteStatus extends ProcessWatchCommand {
jo.put("total", JsonValues.number(r.total));
jo.put("failed", JsonValues.number(r.failed));
jo.put("inflight", JsonValues.number(r.inflight));
+ jo.put("remoteTotal", JsonValues.number(r.totalRemote));
+ jo.put("remoteFailed", JsonValues.number(r.failedRemote));
+ jo.put("remoteInflight", JsonValues.number(r.inflightRemote));
// processing times in millis
jo.put("mean", JsonValues.number(r.mean));
jo.put("min", JsonValues.number(r.min));
@@ -512,17 +523,30 @@ public class CamelRouteStatus extends ProcessWatchCommand
{
}
protected String getTotal(Row r) {
+ if (remote && r.totalRemote != null) {
+ return r.totalRemote + "/" + r.total;
+ }
return r.total;
}
protected String getFailed(Row r) {
+ if (remote && r.failedRemote != null) {
+ return r.failedRemote + "/" + r.failed;
+ }
return r.failed;
}
protected String getInflight(Row r) {
+ if (remote && r.inflightRemote != null) {
+ return r.inflightRemote + "/" + r.inflight;
+ }
return r.inflight;
}
+ private static String text(Object value) {
+ return value != null ? value.toString() : null;
+ }
+
static class Row {
String pid;
String name;
@@ -539,8 +563,11 @@ public class CamelRouteStatus extends ProcessWatchCommand {
String coverage;
String throughput;
String total;
+ String totalRemote;
String failed;
+ String failedRemote;
String inflight;
+ String inflightRemote;
String mean;
String max;
String min;
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelContextStatusTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelContextStatusTest.java
index abbab72673bb..5aae966a9c97 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelContextStatusTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelContextStatusTest.java
@@ -21,6 +21,7 @@ import java.util.stream.Stream;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.MockedStatic;
@@ -145,4 +146,32 @@ class CamelContextStatusTest extends
ProcessCommandTestSupport {
assertTrue(output.contains("jsonApp"), "JSON should contain the
context name");
}
}
+
+ @Test
+ void testJsonHasTheRemoteCounters() throws Exception {
+ JsonObject status = buildContextStatus("jsonApp", 5);
+ JsonObject context = status.getMap("context");
+ JsonObject stats = context.getMap("statistics");
+ stats.put("remoteExchangesTotal", 3);
+ stats.put("remoteExchangesFailed", 1);
+ stats.put("remoteExchangesInflight", 0);
+ writeStatusFile(TEST_PID, status);
+
+ CamelContextStatus command = new CamelContextStatus(new
CamelJBangMain().withPrinter(printer));
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked =
mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv ->
Stream.of(ph));
+
+ assertEquals(0, command.doCall());
+ }
+
+ JsonObject row = Jsoner.deserialize(printer.getOutput(), (JsonArray)
null).getMap(0);
+ assertEquals(3L, row.getLong("remoteTotal"));
+ assertEquals(1L, row.getLong("remoteFailed"));
+ assertEquals(0L, row.getLong("remoteInflight"));
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorStatusTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorStatusTest.java
index fd869702d6b0..36b9318ffa82 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorStatusTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorStatusTest.java
@@ -93,6 +93,23 @@ class CamelProcessorStatusTest extends
ProcessCommandTestSupport {
assertThat(log.getLong("mean")).isEqualTo(5);
}
+ @Test
+ void remoteBreaksDownTheCountersOfTheRoutes() throws Exception {
+ JsonObject route = route("audit", "direct:audit", "Started", 25,
processor("audit-log1", "log", 1, 5));
+ JsonObject stats = route.getMap("statistics");
+ stats.put("remoteExchangesTotal", 6);
+ stats.put("remoteExchangesFailed", 0);
+ stats.put("remoteExchangesInflight", 0);
+ writeStatusFile(TEST_PID, integration(route));
+
+ assertThat(call(command("--remote"))).isZero();
+
+ String[] lines = printer.getOutput().split(System.lineSeparator());
+ assertThat(lines[1]).contains("audit", "6/10");
+ // a processor has no remote counters
+ assertThat(lines[2]).contains("log1", " 10 ").doesNotContain("/10");
+ }
+
@Test
void disabledProcessor() throws Exception {
JsonObject log = processor("orders-log1", "log", 1, 5);
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatusTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatusTest.java
index 109011a05f9a..40baa36fd3b5 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatusTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatusTest.java
@@ -270,6 +270,31 @@ class CamelRouteStatusTest extends
ProcessCommandTestSupport {
assertThat(lines[2]).contains("timer").doesNotContain(" x ");
}
+ @Test
+ void remoteBreaksDownTheCountersIntoRemoteAndTotal() throws Exception {
+ JsonObject stats = stats(10, 2, 1, 5);
+ stats.put("remoteExchangesTotal", 6);
+ stats.put("remoteExchangesFailed", 1);
+ stats.put("remoteExchangesInflight", 0);
+ writeStatusFile(TEST_PID, integration("myApp", route("audit",
"direct:audit", "Started", stats)));
+
+ assertThat(call(command())).isZero();
+ String[] lines = printer.getOutput().split(System.lineSeparator());
+ assertThat(lines[1]).contains(" 10 ").doesNotContain("6/10");
+
+ printer = new StringPrinter();
+ assertThat(call(command("--remote"))).isZero();
+ lines = printer.getOutput().split(System.lineSeparator());
+ assertThat(lines[1]).contains("6/10", "1/2", "0/1");
+
+ printer = new StringPrinter();
+ assertThat(call(command("--json"))).isZero();
+ JsonObject route = Jsoner.deserialize(printer.getOutput(), (JsonArray)
null).getMap(0);
+ assertThat(route.getLong("remoteTotal")).isEqualTo(6);
+ assertThat(route.getLong("remoteFailed")).isEqualTo(1);
+ assertThat(route.getLong("remoteInflight")).isZero();
+ }
+
@Test
void sortByNameOfTheIntegrations() throws Exception {
writeStatusFile(TEST_PID, integration("zeta", route("z1", "timer:z",
"Started", stats(0, 0, 0, -1))));