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-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; } 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 7bd985e0b4aae..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 @@ -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; @@ -3659,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, @@ -3677,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"))), @@ -3688,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()) @@ -5681,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); @@ -6013,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;