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