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;
+    }
+
 }

Reply via email to