You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/05/14 07:58:22 UTC
(camel) 01/10: CAMEL-23514: Add metric counters to ASCII/Unicode diagram
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 <cl...@gmail.com>
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) <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../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;
+ }
}