This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch topology-external-endpoints in repository https://gitbox.apache.org/repos/asf/camel.git
commit ef9bff13b0863829007e9770fb3c527e054a4025 Author: Claus Ibsen <[email protected]> AuthorDate: Tue Jun 2 11:39:39 2026 +0200 Add per-endpoint metrics for external endpoints in topology External-in endpoints use route-level metrics (1 consumer per route). External-out endpoints use processor-level metrics from the matching send processor, giving the actual count of messages sent to that specific endpoint rather than the route total. Multiple send processors to the same destination are aggregated. Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../camel/diagram/TopologyAsciiRenderer.java | 13 ++-- .../org/apache/camel/diagram/TopologyHelper.java | 3 + .../camel/diagram/TopologyImageRenderer.java | 4 +- .../impl/console/RouteTopologyDevConsole.java | 70 ++++++++++++++++++++++ 4 files changed, 84 insertions(+), 6 deletions(-) diff --git a/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyAsciiRenderer.java b/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyAsciiRenderer.java index afdbe26d9856..398e08725d61 100644 --- a/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyAsciiRenderer.java +++ b/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyAsciiRenderer.java @@ -153,7 +153,7 @@ public class TopologyAsciiRenderer { } } - if (metrics && !isExternalNode(node)) { + if (metrics) { if (node.exchangesTotal > 0 || node.exchangesFailed > 0) { long ok = node.exchangesTotal - node.exchangesFailed; StringBuilder sb = new StringBuilder(); @@ -167,7 +167,7 @@ public class TopologyAsciiRenderer { sb.append(node.exchangesFailed).append("!"); } lines.add(sb.toString()); - } else { + } else if (!isExternalNode(node)) { lines.add(""); } } @@ -219,7 +219,8 @@ public class TopologyAsciiRenderer { // Track counter positions for ANSI coloring if (isExternalNode(node) && i == 0) { counterPositions.add(new CounterPos(r, textCol, text.length(), CounterType.EXTERNAL)); - } else if (metrics && !isExternalNode(node) && i == lines.size() - 1 && node.exchangesTotal > 0) { + } + if (metrics && i == lines.size() - 1 && node.exchangesTotal > 0) { long ok = node.exchangesTotal - node.exchangesFailed; if (ok > 0) { String okStr = "" + ok; @@ -304,7 +305,11 @@ public class TopologyAsciiRenderer { private int boxHeight(TopologyLayoutNode node) { if (isExternalNode(node)) { - return 3; + int lines = 1; // URI + if (metrics && node.exchangesTotal > 0) { + lines++; + } + return 2 + lines; } int lines = 3; // routeId + from (2 lines reserved) if (metrics) { diff --git a/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java b/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java index 4381666aae81..ce48c0d105a5 100644 --- a/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java +++ b/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java @@ -101,6 +101,9 @@ public final class TopologyHelper { int colonIdx = uri.indexOf(':'); node.description = colonIdx > 0 ? uri.substring(colonIdx + 1) : uri; + node.exchangesTotal = eo.getLongOrDefault("exchangesTotal", 0); + node.exchangesFailed = eo.getLongOrDefault("exchangesFailed", 0); + nodes.add(node); // Create an edge connecting this external endpoint to/from its route diff --git a/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyImageRenderer.java b/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyImageRenderer.java index af49497aae78..414e884e2fe2 100644 --- a/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyImageRenderer.java +++ b/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyImageRenderer.java @@ -179,8 +179,8 @@ public class TopologyImageRenderer { g.setFont(font); } - // Metrics (not shown for external nodes) - if (metrics && !isExternalNode(node) && node.exchangesTotal > 0) { + // Metrics + if (metrics && node.exchangesTotal > 0) { long ok = node.exchangesTotal - node.exchangesFailed; Font metricsFont = font.deriveFont((float) (fontSizeScaled * 0.75)); g.setFont(metricsFont); diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteTopologyDevConsole.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteTopologyDevConsole.java index 7714f174dde7..f341a78f4312 100644 --- a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteTopologyDevConsole.java +++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteTopologyDevConsole.java @@ -16,10 +16,13 @@ */ package org.apache.camel.impl.console; +import java.util.Collection; +import java.util.HashMap; import java.util.Map; import org.apache.camel.api.management.ManagedCamelContext; import org.apache.camel.api.management.mbean.ManagedRouteMBean; +import org.apache.camel.api.management.mbean.ManagedSendProcessorMBean; import org.apache.camel.spi.RouteTopologyDumper; import org.apache.camel.spi.RouteTopologyDumper.TopologyEdge; import org.apache.camel.spi.RouteTopologyDumper.TopologyExternalEndpoint; @@ -28,6 +31,7 @@ import org.apache.camel.spi.RouteTopologyDumper.TopologyResult; import org.apache.camel.spi.annotations.DevConsole; import org.apache.camel.support.PluginHelper; import org.apache.camel.support.console.AbstractDevConsole; +import org.apache.camel.util.URISupport; import org.apache.camel.util.json.JsonArray; import org.apache.camel.util.json.JsonObject; @@ -126,6 +130,9 @@ public class RouteTopologyDevConsole extends AbstractDevConsole { root.put("edges", edgesArr); if (external && !result.externalEndpoints().isEmpty()) { + // Collect per-endpoint metrics for producers (direction=out) + Map<String, long[]> endpointMetrics = collectEndpointMetrics(mcc, result); + JsonArray extArr = new JsonArray(); for (TopologyExternalEndpoint ep : result.externalEndpoints()) { JsonObject jo = new JsonObject(); @@ -134,6 +141,26 @@ public class RouteTopologyDevConsole extends AbstractDevConsole { jo.put("scheme", ep.scheme()); jo.put("direction", ep.direction()); jo.put("routeId", ep.routeId()); + + if (mcc != null) { + if ("in".equals(ep.direction())) { + // Consumer: use route-level metrics (route has exactly 1 consumer) + ManagedRouteMBean mrb = mcc.getManagedRoute(ep.routeId()); + if (mrb != null) { + jo.put("exchangesTotal", mrb.getExchangesTotal()); + jo.put("exchangesFailed", mrb.getExchangesFailed()); + } + } else { + // Producer: use processor-level metrics + String key = ep.routeId() + "|" + ep.uri(); + long[] stats = endpointMetrics.get(key); + if (stats != null) { + jo.put("exchangesTotal", stats[0]); + jo.put("exchangesFailed", stats[1]); + } + } + } + extArr.add(jo); } root.put("externalEndpoints", extArr); @@ -142,4 +169,47 @@ public class RouteTopologyDevConsole extends AbstractDevConsole { return root; } + /** + * Collects per-endpoint metrics for producer endpoints by iterating managed send processors. Returns a map keyed by + * "routeId|normalizedUri" with value [exchangesTotal, exchangesFailed]. + */ + private Map<String, long[]> collectEndpointMetrics(ManagedCamelContext mcc, TopologyResult result) { + Map<String, long[]> metrics = new HashMap<>(); + if (mcc == null) { + return metrics; + } + for (TopologyExternalEndpoint ep : result.externalEndpoints()) { + if (!"out".equals(ep.direction())) { + continue; + } + try { + ManagedRouteMBean mrb = mcc.getManagedRoute(ep.routeId()); + if (mrb == null) { + continue; + } + Collection<String> ids = mrb.processorIds(); + for (String pid : ids) { + ManagedSendProcessorMBean sp = mcc.getManagedProcessor(pid, ManagedSendProcessorMBean.class); + if (sp == null) { + continue; + } + String dest = URISupport.stripQuery(sp.getDestination()); + if (ep.uri().equals(dest)) { + String key = ep.routeId() + "|" + ep.uri(); + long[] existing = metrics.get(key); + if (existing != null) { + existing[0] += sp.getExchangesTotal(); + existing[1] += sp.getExchangesFailed(); + } else { + metrics.put(key, new long[] { sp.getExchangesTotal(), sp.getExchangesFailed() }); + } + } + } + } catch (Exception e) { + // ignore + } + } + return metrics; + } + }
