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 28a23f375f12 TUI and REST metrics improvements (#23329)
28a23f375f12 is described below
commit 28a23f375f12279c98e571315beb1e0a5bf16b66
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue May 19 20:24:42 2026 +0200
TUI and REST metrics improvements (#23329)
* camel-jbang - Fix tab badge counters mapped to wrong tab indices
Badge counters for HTTP, Health, Inspect, and Circuit Breaker tabs were
off-by-one because HTTP was placed last instead of at its correct position.
Use TAB_* constants instead of raw integers to prevent future mismatches.
Co-Authored-By: Claude <[email protected]>
* CAMEL-23554: camel-core - Track InOut consumer replies as "out" hits in
endpoint statistics
InOut consumers like platform-http send a response back to the client,
but only the inbound request was tracked. Now when an InOut exchange
completes (or fails), an "out" hit is recorded on the consumer's
fromEndpoint so the endpoint tab and flow diagram show both directions.
Co-Authored-By: Claude <[email protected]>
* CAMEL-23555: Per-operation HTTP metrics for REST services in RestRegistry
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude <[email protected]>
---
.../rest/openapi/RestOpenApiProcessor.java | 6 ++
.../camel/component/rest/DefaultRestRegistry.java | 97 +++++++++++++++++++---
.../java/org/apache/camel/spi/RestRegistry.java | 17 ++++
.../engine/DefaultRuntimeEndpointRegistry.java | 27 ++++++
.../apache/camel/impl/console/RestDevConsole.java | 4 +
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 47 ++++++-----
6 files changed, 167 insertions(+), 31 deletions(-)
diff --git
a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java
b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java
index a40a91f034a7..e46114cd54bd 100644
---
a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java
+++
b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java
@@ -50,6 +50,7 @@ public class RestOpenApiProcessor extends
AsyncProcessorSupport implements Camel
private PlatformHttpConsumerAware platformHttpConsumer;
private Consumer consumer;
private OpenApiUtils openApiUtils;
+ private RestRegistry restRegistry;
public RestOpenApiProcessor(RestOpenApiEndpoint endpoint, OpenAPI openAPI,
String basePath, String apiContextPath,
RestOpenapiProcessorStrategy
restOpenapiProcessorStrategy) {
@@ -111,6 +112,10 @@ public class RestOpenApiProcessor extends
AsyncProcessorSupport implements Camel
// map path-parameters from operation to camel headers
HttpHelper.evalPlaceholders(exchange.getMessage().getHeaders(),
path, consumerPath);
+ if (restRegistry != null) {
+ restRegistry.hit(verb, basePath, consumerPath);
+ }
+
// process the incoming request
return restOpenapiProcessorStrategy.process(openAPI, o, verb,
path, rcp.getBinding(), exchange, callback);
}
@@ -150,6 +155,7 @@ public class RestOpenApiProcessor extends
AsyncProcessorSupport implements Camel
// this is required to build the paths with all the details
this.openApiUtils = new OpenApiUtils(camelContext,
endpoint.getBindingPackageScan(), openAPI.getComponents());
+ this.restRegistry = PluginHelper.getRestRegistry(camelContext);
// register all openapi paths
for (var e : openAPI.getPaths().entrySet()) {
String path = e.getKey(); // path
diff --git
a/components/camel-rest/src/main/java/org/apache/camel/component/rest/DefaultRestRegistry.java
b/components/camel-rest/src/main/java/org/apache/camel/component/rest/DefaultRestRegistry.java
index a29283973f64..2a305a2fc959 100644
---
a/components/camel-rest/src/main/java/org/apache/camel/component/rest/DefaultRestRegistry.java
+++
b/components/camel-rest/src/main/java/org/apache/camel/component/rest/DefaultRestRegistry.java
@@ -17,12 +17,13 @@
package org.apache.camel.component.rest;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
import org.apache.camel.CamelContext;
-import org.apache.camel.CamelContextAware;
import org.apache.camel.Consumer;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
@@ -32,18 +33,20 @@ import org.apache.camel.RuntimeCamelException;
import org.apache.camel.Service;
import org.apache.camel.ServiceStatus;
import org.apache.camel.StatefulService;
+import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.spi.CamelEvent.ExchangeCreatedEvent;
import org.apache.camel.spi.NormalizedEndpointUri;
import org.apache.camel.spi.RestConfiguration;
import org.apache.camel.spi.RestRegistry;
+import org.apache.camel.support.EventNotifierSupport;
import org.apache.camel.support.LifecycleStrategySupport;
-import org.apache.camel.support.service.ServiceSupport;
import org.apache.camel.util.ObjectHelper;
-public class DefaultRestRegistry extends ServiceSupport implements
RestRegistry, CamelContextAware {
+public class DefaultRestRegistry extends EventNotifierSupport implements
RestRegistry {
- private CamelContext camelContext;
private final Map<Consumer, List<RestService>> registry = new
LinkedHashMap<>();
private final Map<Consumer, List<RestService>> specs = new
LinkedHashMap<>();
+ private final Map<String, List<RestServiceEntry>> routeIdIndex = new
HashMap<>();
private transient Producer apiProducer;
@Override
@@ -57,6 +60,9 @@ public class DefaultRestRegistry extends ServiceSupport
implements RestRegistry,
outType, routeId, operationId, specificationUri, description);
List<RestService> list = registry.computeIfAbsent(consumer, c -> new
ArrayList<>());
list.add(entry);
+ if (routeId != null) {
+ routeIdIndex.computeIfAbsent(routeId, k -> new
ArrayList<>()).add(entry);
+ }
}
@Override
@@ -70,9 +76,55 @@ public class DefaultRestRegistry extends ServiceSupport
implements RestRegistry,
list.add(entry);
}
+ @Override
+ public void hit(String method, String basePath, String path) {
+ for (var list : registry.values()) {
+ for (var rs : list) {
+ RestServiceEntry entry = (RestServiceEntry) rs;
+ if (entry.method.equalsIgnoreCase(method) &&
matchesPath(entry, basePath, path)) {
+ entry.hits.incrementAndGet();
+ return;
+ }
+ }
+ }
+ }
+
+ private static boolean matchesPath(RestServiceEntry entry, String
basePath, String path) {
+ if (entry.basePath != null && entry.basePath.equals(basePath)) {
+ if (path != null && path.equals(entry.uriTemplate)) {
+ return true;
+ }
+ // contract-first stores the OpenAPI path in baseUrl with
uriTemplate=null
+ if (entry.uriTemplate == null && path != null &&
path.equals(entry.baseUrl)) {
+ return true;
+ }
+ String entryPath = entry.basePath + (entry.uriTemplate != null ?
entry.uriTemplate : "");
+ return path != null && path.equals(entryPath);
+ }
+ String entryPath = entry.basePath != null ? entry.basePath : "";
+ if (entry.uriTemplate != null) {
+ entryPath += entry.uriTemplate;
+ }
+ return path != null && path.equals(entryPath);
+ }
+
@Override
public void removeRestService(Consumer consumer) {
- registry.remove(consumer);
+ List<RestService> removed = registry.remove(consumer);
+ if (removed != null) {
+ for (RestService rs : removed) {
+ RestServiceEntry entry = (RestServiceEntry) rs;
+ if (entry.routeId != null) {
+ List<RestServiceEntry> entries =
routeIdIndex.get(entry.routeId);
+ if (entries != null) {
+ entries.remove(entry);
+ if (entries.isEmpty()) {
+ routeIdIndex.remove(entry.routeId);
+ }
+ }
+ }
+ }
+ }
specs.remove(consumer);
}
@@ -106,6 +158,7 @@ public class DefaultRestRegistry extends ServiceSupport
implements RestRegistry,
@Override
public String apiDocAsJson() {
// see if there is a rest-api endpoint which would be the case if rest
api-doc has been explicit enabled
+ CamelContext camelContext = getCamelContext();
if (apiProducer == null) {
Endpoint restApiEndpoint = null;
Endpoint restEndpoint = null;
@@ -164,26 +217,42 @@ public class DefaultRestRegistry extends ServiceSupport
implements RestRegistry,
}
@Override
- public CamelContext getCamelContext() {
- return camelContext;
+ public void notify(CamelEvent event) throws Exception {
+ if (event instanceof ExchangeCreatedEvent ece) {
+ String routeId = ece.getExchange().getFromRouteId();
+ if (routeId != null) {
+ List<RestServiceEntry> entries = routeIdIndex.get(routeId);
+ if (entries != null) {
+ for (RestServiceEntry entry : entries) {
+ if (!entry.contractFirst) {
+ entry.hits.incrementAndGet();
+ }
+ }
+ }
+ }
+ }
}
@Override
- public void setCamelContext(CamelContext camelContext) {
- this.camelContext = camelContext;
+ public boolean isEnabled(CamelEvent event) {
+ return event instanceof ExchangeCreatedEvent;
}
@Override
protected void doStart() throws Exception {
- ObjectHelper.notNull(camelContext, "camelContext", this);
+ ObjectHelper.notNull(getCamelContext(), "camelContext", this);
// add a lifecycle so we can keep track when consumers is being
removed, so we can unregister them from our registry
- camelContext.addLifecycleStrategy(new
RemoveRestServiceLifecycleStrategy());
+ getCamelContext().addLifecycleStrategy(new
RemoveRestServiceLifecycleStrategy());
+ // register as event notifier so we receive ExchangeCreatedEvent for
code-first hit tracking
+ getCamelContext().getManagementStrategy().addEventNotifier(this);
}
@Override
protected void doStop() throws Exception {
+ getCamelContext().getManagementStrategy().removeEventNotifier(this);
registry.clear();
specs.clear();
+ routeIdIndex.clear();
}
/**
@@ -207,6 +276,7 @@ public class DefaultRestRegistry extends ServiceSupport
implements RestRegistry,
private final String operationId;
private final String specificationUri;
private final String description;
+ private final AtomicLong hits = new AtomicLong();
private RestServiceEntry(Consumer consumer, boolean specification,
boolean contractFirst, String url, String baseUrl,
String basePath,
@@ -324,6 +394,11 @@ public class DefaultRestRegistry extends ServiceSupport
implements RestRegistry,
public String getDescription() {
return description;
}
+
+ @Override
+ public long getHits() {
+ return hits.get();
+ }
}
/**
diff --git
a/core/camel-api/src/main/java/org/apache/camel/spi/RestRegistry.java
b/core/camel-api/src/main/java/org/apache/camel/spi/RestRegistry.java
index f5468065001d..8dbb5a539304 100644
--- a/core/camel-api/src/main/java/org/apache/camel/spi/RestRegistry.java
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/RestRegistry.java
@@ -138,6 +138,13 @@ public interface RestRegistry extends StaticService {
@Nullable
String getSpecificationUri();
+ /**
+ * Number of requests processed by this REST service.
+ *
+ * @since 4.21
+ */
+ long getHits();
+
}
/**
@@ -166,6 +173,16 @@ public interface RestRegistry extends StaticService {
String routeId, @Nullable String operationId, @Nullable String
specificationUri,
@Nullable String description);
+ /**
+ * Records a hit on the REST service matching the given HTTP method and
path.
+ *
+ * @param method the HTTP method (GET, POST, etc.)
+ * @param basePath the base path
+ * @param path the URI path or template (e.g. /users/{id})
+ * @since 4.21
+ */
+ void hit(String method, String basePath, String path);
+
/**
* Removes the REST service from the registry
*
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 ed8044f5ad52..8070612e94a8 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
@@ -25,8 +25,11 @@ import java.util.Map;
import java.util.Set;
import org.apache.camel.Endpoint;
+import org.apache.camel.Exchange;
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.RouteAddedEvent;
import org.apache.camel.spi.CamelEvent.RouteRemovedEvent;
@@ -295,6 +298,28 @@ public class DefaultRuntimeEndpointRegistry extends
EventNotifierSupport impleme
outputUtilization.onHit(key);
}
}
+ } else if (event instanceof ExchangeCompletedEvent || event instanceof
ExchangeFailedEvent) {
+ // InOut consumers send a reply back when the exchange completes;
+ // record this as an "out" hit on the consumer's fromEndpoint
+ CamelEvent.ExchangeEvent ee = (CamelEvent.ExchangeEvent) event;
+ Exchange exchange = ee.getExchange();
+ if (exchange.getPattern() != null &&
exchange.getPattern().isOutCapable()) {
+ Endpoint endpoint = exchange.getFromEndpoint();
+ if (endpoint != null) {
+ String routeId = exchange.getFromRouteId();
+ String uri = endpoint.getEndpointUri();
+ Map<String, String> uris = outputs.get(routeId);
+ if (uris != null) {
+ uris.putIfAbsent(uri, uri);
+ }
+ if (extended) {
+ String key = asUtilizationKey(routeId, uri);
+ if (key != null) {
+ outputUtilization.onHit(key);
+ }
+ }
+ }
+ }
}
}
@@ -307,6 +332,8 @@ public class DefaultRuntimeEndpointRegistry extends
EventNotifierSupport impleme
public boolean isEnabled(CamelEvent event) {
return enabled && event instanceof ExchangeCreatedEvent
|| event instanceof ExchangeSendingEvent
+ || event instanceof ExchangeCompletedEvent
+ || event instanceof ExchangeFailedEvent
|| event instanceof RouteAddedEvent
|| event instanceof RouteRemovedEvent;
}
diff --git
a/core/camel-console/src/main/java/org/apache/camel/impl/console/RestDevConsole.java
b/core/camel-console/src/main/java/org/apache/camel/impl/console/RestDevConsole.java
index dba1d20bc517..ea25263b0540 100644
---
a/core/camel-console/src/main/java/org/apache/camel/impl/console/RestDevConsole.java
+++
b/core/camel-console/src/main/java/org/apache/camel/impl/console/RestDevConsole.java
@@ -120,6 +120,10 @@ public class RestDevConsole extends AbstractDevConsole {
if (rs.getDescription() != null) {
jo.put("description", rs.getDescription());
}
+ long hits = rs.getHits();
+ if (hits > 0) {
+ jo.put("hits", hits);
+ }
list.add(jo);
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 7bd985e0b4aa..858d53f51b64 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -1221,39 +1221,37 @@ public class CamelMonitor extends CamelCommand {
}
if (activeCount > 0) {
- badgeTexts[0] = "(" + activeCount + ")";
+ badgeTexts[TAB_OVERVIEW] = "(" + activeCount + ")";
}
- // tab 1 (Log) — no badge
if (routeCount > 0) {
- badgeTexts[2] = "(" + routeCount + ")";
+ badgeTexts[TAB_ROUTES] = "(" + routeCount + ")";
}
if (consumerCount > 0) {
- badgeTexts[3] = "(" + consumerCount + ")";
+ badgeTexts[TAB_CONSUMERS] = "(" + consumerCount + ")";
}
if (endpointCount > 0) {
- badgeTexts[4] = "(" + endpointCount + ")";
+ badgeTexts[TAB_ENDPOINTS] = "(" + endpointCount + ")";
+ }
+ if (httpCount > 0) {
+ badgeTexts[TAB_HTTP] = "(" + httpCount + ")";
}
if (healthDownCount > 0) {
- badgeTexts[5] = "(" + healthDownCount + " DOWN)";
- badgeStyles[5] = red;
+ badgeTexts[TAB_HEALTH] = "(" + healthDownCount + " DOWN)";
+ badgeStyles[TAB_HEALTH] = red;
} else if (healthCount > 0) {
- badgeTexts[5] = "(" + healthCount + ")";
+ badgeTexts[TAB_HEALTH] = "(" + healthCount + ")";
}
- // Inspect tab (7): show tracer active (*) in cyan, or history
count in yellow
if (hasTraces) {
- badgeTexts[6] = "(*)";
- badgeStyles[6] = cyan;
+ badgeTexts[TAB_HISTORY] = "(*)";
+ badgeStyles[TAB_HISTORY] = cyan;
} else if (historyCount > 0) {
- badgeTexts[6] = "(" + historyCount + ")";
+ badgeTexts[TAB_HISTORY] = "(" + historyCount + ")";
}
if (cbOpenCount > 0) {
- badgeTexts[7] = "(" + cbOpenCount + " OPEN)";
- badgeStyles[7] = red;
+ badgeTexts[TAB_CIRCUIT_BREAKER] = "(" + cbOpenCount + " OPEN)";
+ badgeStyles[TAB_CIRCUIT_BREAKER] = red;
} else if (cbCount > 0) {
- badgeTexts[7] = "(" + cbCount + ")";
- }
- if (httpCount > 0) {
- badgeTexts[8] = "(" + httpCount + ")";
+ badgeTexts[TAB_CIRCUIT_BREAKER] = "(" + cbCount + ")";
}
int tabX = 0;
@@ -3659,9 +3657,11 @@ public class CamelMonitor extends CamelCommand {
source = "HTTP";
}
String state = ep.state != null ? ep.state : "";
+ String hitsStr = ep.hits > 0 ? String.valueOf(ep.hits) : "";
rows.add(Row.from(
Cell.from(Span.styled(method, methodStyle(method))),
Cell.from(path),
+ rightCell(hitsStr, 8),
Cell.from(consumes),
Cell.from(produces),
Cell.from(Span.styled(source,
@@ -3677,6 +3677,7 @@ public class CamelMonitor extends CamelCommand {
Row header = Row.from(
Cell.from(Span.styled(httpSortLabel("METHOD", "method"),
httpSortStyle("method"))),
Cell.from(Span.styled(httpSortLabel("PATH", "path"),
httpSortStyle("path"))),
+ rightCell("TOTAL", 8, Style.EMPTY.bold()),
Cell.from(Span.styled(httpSortLabel("CONSUMES", "consumes"),
httpSortStyle("consumes"))),
Cell.from(Span.styled(httpSortLabel("PRODUCES", "produces"),
httpSortStyle("produces"))),
Cell.from(Span.styled(httpSortLabel("SOURCE", "source"),
httpSortStyle("source"))),
@@ -3688,8 +3689,9 @@ public class CamelMonitor extends CamelCommand {
.widths(
Constraint.length(12),
Constraint.fill(),
- Constraint.length(35),
- Constraint.length(35),
+ Constraint.length(8),
+ Constraint.length(30),
+ Constraint.length(30),
Constraint.length(15),
Constraint.length(8))
.highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
@@ -5681,6 +5683,10 @@ public class CamelMonitor extends CamelCommand {
ep.state = rj.getString("state");
ep.inType = rj.getString("inType");
ep.outType = rj.getString("outType");
+ Long h = rj.getLong("hits");
+ if (h != null) {
+ ep.hits = h;
+ }
// derive path from url (strip scheme+host+port)
ep.path = extractPath(ep.url);
info.httpEndpoints.add(ep);
@@ -6013,6 +6019,7 @@ public class CamelMonitor extends CamelCommand {
String url; // full URL including server
String consumes;
String produces;
+ long hits; // per-operation request count
// REST DSL only
boolean fromRest;
boolean contractFirst;