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;

Reply via email to