From f970ba4f173ba8cc3e8631c0c58d4ccf5548cbc4 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Tue, 19 May 2026 12:36:56 +0200 Subject: [PATCH 1/3] 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 --- .../jbang/core/commands/tui/CamelMonitor.java | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) 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 7bd985e0b4aae..f236ab73396b5 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 @@ private void renderTabs(Frame frame, Rect area) { } 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; From eb45eca7c5eb283402527ca0c177d19f5ad1f963 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Tue, 19 May 2026 13:33:50 +0200 Subject: [PATCH 2/3] 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 --- .../DefaultRuntimeEndpointRegistry.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) 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 ed8044f5ad52f..8070612e94a88 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.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 void notify(CamelEvent event) throws Exception { 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 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 boolean isDisabled() { 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; } From 5e51a2870c326497fca6a73e01a9c58940ed5f48 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Tue, 19 May 2026 14:07:47 +0200 Subject: [PATCH 3/3] CAMEL-23555: Per-operation HTTP metrics for REST services in RestRegistry Co-Authored-By: Claude Opus 4.6 --- .../rest/openapi/RestOpenApiProcessor.java | 6 ++ .../component/rest/DefaultRestRegistry.java | 97 ++++++++++++++++--- .../org/apache/camel/spi/RestRegistry.java | 17 ++++ .../camel/impl/console/RestDevConsole.java | 4 + .../jbang/core/commands/tui/CamelMonitor.java | 13 ++- 5 files changed, 124 insertions(+), 13 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 a40a91f034a7e..e46114cd54bdd 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 boolean process(Exchange exchange, AsyncCallback callback) { // 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 void afterPropertiesConfigured(CamelContext camelContext) { // 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 a29283973f64a..2a305a2fc9592 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.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> registry = new LinkedHashMap<>(); private final Map> specs = new LinkedHashMap<>(); + private final Map> routeIdIndex = new HashMap<>(); private transient Producer apiProducer; @Override @@ -57,6 +60,9 @@ public void addRestService( outType, routeId, operationId, specificationUri, description); List 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 void addRestSpecification( 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 removed = registry.remove(consumer); + if (removed != null) { + for (RestService rs : removed) { + RestServiceEntry entry = (RestServiceEntry) rs; + if (entry.routeId != null) { + List 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 int size() { @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 String apiDocAsJson() { } @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 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 @@ private static final class RestServiceEntry implements RestService { 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 String getSpecificationUri() { 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 f5468065001d9..8dbb5a539304e 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 @@ interface RestService { @Nullable String getSpecificationUri(); + /** + * Number of requests processed by this REST service. + * + * @since 4.21 + */ + long getHits(); + } /** @@ -166,6 +173,16 @@ void addRestService( 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-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 dba1d20bc5179..ea25263b0540c 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 @@ protected Map doCallJson(Map options) { 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 f236ab73396b5..858d53f51b648 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 @@ -3657,9 +3657,11 @@ private void renderHttpTable(Frame frame, Rect area, List visi 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, @@ -3675,6 +3677,7 @@ private void renderHttpTable(Frame frame, Rect area, List visi 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"))), @@ -3686,8 +3689,9 @@ private void renderHttpTable(Frame frame, Rect area, List visi .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()) @@ -5679,6 +5683,10 @@ private IntegrationInfo parseIntegration(ProcessHandle ph, JsonObject root) { 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); @@ -6011,6 +6019,7 @@ static class HttpEndpointInfo { String url; // full URL including server String consumes; String produces; + long hits; // per-operation request count // REST DSL only boolean fromRest; boolean contractFirst;