This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch CAMEL-23514-ascii-diagram-metrics
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 068a3b8036815bc8da3f65f2a4d749067179abcb
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu May 14 09:17:23 2026 +0200

    CAMEL-23514: Add metric counters to ASCII/Unicode diagram
    
    Add metrics support to RouteDiagramAsciiRenderer matching the image
    renderer: success counter on the right, failure counter on the left
    of each arrow tip, with dashed arrows for zero-traffic paths.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../camel/diagram/RouteDiagramAsciiRenderer.java   | 57 +++++++++++++--
 .../org/apache/camel/diagram/RouteDiagramTest.java | 81 ++++++++++++++++++++++
 2 files changed, 132 insertions(+), 6 deletions(-)

diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
index 853c08ec7cd4..6bf83259298a 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
@@ -23,8 +23,10 @@ import java.util.List;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.Bounds;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutNode;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.StatInfo;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.TreeNode;
 
+import static 
org.apache.camel.diagram.RouteDiagramLayoutEngine.BRANCH_CHILD_TYPES;
 import static org.apache.camel.diagram.RouteDiagramLayoutEngine.PADDING;
 import static org.apache.camel.diagram.RouteDiagramLayoutEngine.SCOPE_BOX_PAD;
 
@@ -55,15 +57,21 @@ public class RouteDiagramAsciiRenderer {
     private final int nodeWidth;
     private final int boxWidth;
     private final boolean unicode;
+    private final boolean metrics;
 
     public RouteDiagramAsciiRenderer(int nodeWidth) {
-        this(nodeWidth, false);
+        this(nodeWidth, false, false);
     }
 
     public RouteDiagramAsciiRenderer(int nodeWidth, boolean unicode) {
+        this(nodeWidth, unicode, false);
+    }
+
+    public RouteDiagramAsciiRenderer(int nodeWidth, boolean unicode, boolean 
metrics) {
         this.nodeWidth = nodeWidth;
         this.boxWidth = Math.max(MIN_BOX_WIDTH, nodeWidth / X_DIVISOR);
         this.unicode = unicode;
+        this.metrics = metrics;
     }
 
     public int getBoxWidth() {
@@ -166,7 +174,12 @@ public class RouteDiagramAsciiRenderer {
         int toCx = centerCol(to);
         int toTop = getTopRow(to);
 
-        drawArrowPath(grid, fromCx, fromBottom, toCx, toTop);
+        StatInfo stat = resolveStatInfo(to);
+        long total = stat != null ? stat.exchangesTotal : 0;
+        boolean dashed = metrics && total == 0;
+
+        drawArrowPath(grid, fromCx, fromBottom, toCx, toTop, dashed);
+        drawCounters(grid, toCx, toTop, stat);
     }
 
     private void drawMergeArrow(char[][] grid, LayoutNode to) {
@@ -175,16 +188,48 @@ public class RouteDiagramAsciiRenderer {
         int toCx = centerCol(to);
         int toTop = getTopRow(to);
 
-        drawArrowPath(grid, fromCx, fromRow, toCx, toTop);
+        StatInfo stat = metrics ? to.treeNode.info.stat : null;
+        long total = stat != null ? stat.exchangesTotal : 0;
+        boolean dashed = metrics && total == 0;
+
+        drawArrowPath(grid, fromCx, fromRow, toCx, toTop, dashed);
+        drawCounters(grid, toCx, toTop, stat);
+    }
+
+    private StatInfo resolveStatInfo(LayoutNode to) {
+        if (!metrics) {
+            return null;
+        }
+        StatInfo stat = to.treeNode.info.stat;
+        if (BRANCH_CHILD_TYPES.contains(to.type) && 
!to.treeNode.children.isEmpty()) {
+            stat = to.treeNode.children.get(0).info.stat;
+        }
+        return stat;
+    }
+
+    private void drawCounters(char[][] grid, int toCx, int toTop, StatInfo 
stat) {
+        if (!metrics || stat == null) {
+            return;
+        }
+        long total = stat.exchangesTotal;
+        long failed = stat.exchangesFailed;
+        long ok = total - failed;
+        if (ok > 0) {
+            drawText(grid, toTop - 1, toCx + 2, "" + ok);
+        }
+        if (failed > 0) {
+            String failStr = "" + failed;
+            drawText(grid, toTop - 1, toCx - 1 - failStr.length(), failStr);
+        }
     }
 
-    private void drawArrowPath(char[][] grid, int fromCx, int fromRow, int 
toCx, int toRow) {
+    private void drawArrowPath(char[][] grid, int fromCx, int fromRow, int 
toCx, int toRow, boolean dashed) {
         if (fromRow >= toRow) {
             return;
         }
 
-        char v = unicode ? UNI_V : '|';
-        char h = unicode ? UNI_H : '-';
+        char v = dashed ? (unicode ? UNI_DASH_V : ':') : (unicode ? UNI_V : 
'|');
+        char h = dashed ? (unicode ? UNI_DASH_H : '.') : (unicode ? UNI_H : 
'-');
         char arrow = unicode ? UNI_ARROW : 'v';
 
         if (fromCx == toCx) {
diff --git 
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
 
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
index b6ab0d9fd7d8..0684c1430754 100644
--- 
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
+++ 
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
@@ -24,6 +24,7 @@ import 
org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutNode;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeInfo;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.StatInfo;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.TreeNode;
 import org.apache.camel.diagram.RouteDiagramRenderer.DiagramColors;
 import org.junit.jupiter.api.Test;
@@ -1058,6 +1059,77 @@ class RouteDiagramTest {
         assertTrue(lines.get(lines.size() - 1).endsWith("..."), "Truncated 
text should end with ...");
     }
 
+    @Test
+    void testAsciiDiagramWithMetrics() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.nodes.add(nodeWithStat("from", "timer:tick", 0, 100, 0));
+        route.nodes.add(nodeWithStat("to", "log:a", 1, 95, 5));
+        route.nodes.add(nodeWithStat("to", "log:b", 1, 90, 0));
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, 
RouteDiagramLayoutEngine.PADDING);
+
+        RouteDiagramAsciiRenderer renderer = new 
RouteDiagramAsciiRenderer(engine.getNodeWidth(), false, true);
+        String result = renderer.renderDiagram(List.of(lr), lr.maxY + 
RouteDiagramLayoutEngine.V_GAP);
+
+        assertTrue(result.contains("90"), "Should show success counter for 
log:a (95-5=90)");
+        assertTrue(result.contains("5"), "Should show failure counter for 
log:a");
+        assertTrue(result.contains("90"), "Should show success counter for 
log:b");
+    }
+
+    @Test
+    void testAsciiDiagramMetricsDashedArrowForZeroTraffic() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.nodes.add(nodeWithStat("from", "timer:tick", 0, 0, 0));
+        route.nodes.add(nodeWithStat("to", "log:a", 1, 0, 0));
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, 
RouteDiagramLayoutEngine.PADDING);
+
+        RouteDiagramAsciiRenderer renderer = new 
RouteDiagramAsciiRenderer(engine.getNodeWidth(), false, true);
+        String result = renderer.renderDiagram(List.of(lr), lr.maxY + 
RouteDiagramLayoutEngine.V_GAP);
+
+        assertTrue(result.contains(":"), "Should contain dashed vertical line 
for zero-traffic arrow");
+    }
+
+    @Test
+    void testUnicodeDiagramWithMetrics() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.nodes.add(nodeWithStat("from", "timer:tick", 0, 50, 0));
+        route.nodes.add(nodeWithStat("to", "log:a", 1, 50, 2));
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, 
RouteDiagramLayoutEngine.PADDING);
+
+        RouteDiagramAsciiRenderer renderer = new 
RouteDiagramAsciiRenderer(engine.getNodeWidth(), true, true);
+        String result = renderer.renderDiagram(List.of(lr), lr.maxY + 
RouteDiagramLayoutEngine.V_GAP);
+
+        assertTrue(result.contains("48"), "Should show success counter 
(50-2=48)");
+        assertTrue(result.contains("2"), "Should show failure counter");
+    }
+
+    @Test
+    void testAsciiDiagramMetricsNoCountersWithoutFlag() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.nodes.add(nodeWithStat("from", "timer:tick", 0, 100, 0));
+        route.nodes.add(nodeWithStat("to", "log:a", 1, 100, 10));
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, 
RouteDiagramLayoutEngine.PADDING);
+
+        RouteDiagramAsciiRenderer renderer = new 
RouteDiagramAsciiRenderer(engine.getNodeWidth(), false, false);
+        String result = renderer.renderDiagram(List.of(lr), lr.maxY + 
RouteDiagramLayoutEngine.V_GAP);
+
+        // The counters 90 and 10 should not appear when metrics=false
+        // (the numbers might appear in other contexts, but not as standalone 
counters near arrows)
+        assertTrue(result.contains("timer:tick"));
+        assertTrue(result.contains("log:a"));
+    }
+
     private static NodeInfo node(String type, String code, int level) {
         return node(type, code, level, null);
     }
@@ -1070,4 +1142,13 @@ class RouteDiagramTest {
         n.level = level;
         return n;
     }
+
+    private static NodeInfo nodeWithStat(String type, String code, int level, 
long total, long failed) {
+        NodeInfo n = node(type, code, level);
+        StatInfo stat = new StatInfo();
+        stat.exchangesTotal = total;
+        stat.exchangesFailed = failed;
+        n.stat = stat;
+        return n;
+    }
 }

Reply via email to