This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 4172b3d6762b CAMEL-23514: Add metric counters to ASCII/Unicode diagram
(#23206)
4172b3d6762b is described below
commit 4172b3d6762b4329b766c845587f1976a3985145
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu May 14 11:03:42 2026 +0200
CAMEL-23514: Add metric counters to ASCII/Unicode diagram (#23206)
* 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-23514: Pass metrics flag to ASCII renderer in TUI monitor
Parse statistics from route-structure JSON response and pass
metrics=true to RouteDiagramAsciiRenderer so counters are shown
in the text diagram view.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23514: Pass metrics flag to ASCII renderer in CLI route-diagram
command
The --metric flag was only passed to the image renderer. Now also
passed to RouteDiagramAsciiRenderer for ascii/unicode themes.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23514: Add metrics toggle to TUI diagram view
Add 'm' key to toggle metrics on/off in the diagram view. When
metrics are enabled and showing a text diagram, the diagram is
automatically refreshed on each tick so counters update in real time.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23514: Fix diagram flicker during live metric refresh
Only show the "Loading diagram..." state on initial load, not on
tick-based refreshes. Preserve scroll position when refreshing an
already-visible diagram. This prevents the visible flash between
the loading placeholder and the actual diagram content on each tick.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23514: Fix metrics toggle not taking effect immediately
Reset diagramLoading guard before reloading so the toggle is not
blocked by an in-progress tick-triggered load that captured the
old metrics flag.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23514: Remove refresh interval from footer
The refresh interval is not user-configurable and adds no value
to the footer display.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23514: Add metrics to image diagram with F5 manual refresh
Pass metrics flag to RouteDiagramRenderer for image diagrams so
counters are shown. Add F5 key to manually refresh the diagram
(both text and image modes) to update counters on demand. The
image diagram shows static counters from load time since terminal
image protocols don't support partial updates. Footer hints show
F5 refresh when metrics are enabled.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23514: Limit F5 refresh to image diagram only
Text diagram auto-refreshes on tick so F5 is redundant there.
Only show F5 hint in image diagram mode with metrics enabled.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23514: Remove redundant r=refresh key and hint
The overview tab auto-refreshes every 100ms via tick, making the
manual r=refresh key unnecessary.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23514: Add colored counters to text and image diagrams
Add counter position tracking to RouteDiagramAsciiRenderer so callers
can colorize success (green) and failure (red) counters.
- Renderer records CounterPos (row, col, length, type) for each counter
- renderDiagramAnsi() applies ANSI color codes for CLI terminal output
- TUI styleDiagramLine() uses positions to create colored Spans
- CLI route-diagram command uses ANSI-colored output
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23514: Fix counter color positions after empty line removal
Counter positions from the ASCII renderer use original grid row
numbers, but empty lines are filtered out when building diagramLines.
Remap the row indices to account for removed lines so counter
coloring matches the correct positions in the TUI.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23514: Fix TUI counter coloring not applied
The styleDiagramLine parser was outputting entire lines as WHITE when
no [tag] brackets were found, skipping the counter range check. Now
all text segments go through addStyledSegment which scans for counter
ranges. Also fixed findNextCounterRange to find counters starting at
or after the current position, not just those containing the position.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---------
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
---
.../camel/diagram/RouteDiagramAsciiRenderer.java | 97 ++++++++-
.../org/apache/camel/diagram/RouteDiagramTest.java | 81 ++++++++
.../commands/action/CamelRouteDiagramAction.java | 4 +-
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 219 ++++++++++++++-------
4 files changed, 326 insertions(+), 75 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..5d0f9bb0173a 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,22 +57,63 @@ public class RouteDiagramAsciiRenderer {
private final int nodeWidth;
private final int boxWidth;
private final boolean unicode;
+ private final boolean metrics;
+ private final List<CounterPos> counterPositions = new ArrayList<>();
+
+ public enum CounterType {
+ OK,
+ FAIL
+ }
+
+ public record CounterPos(int row, int col, int length, CounterType type) {
+ }
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() {
return boxWidth;
}
+ public List<CounterPos> getCounterPositions() {
+ return counterPositions;
+ }
+
+ public String renderDiagramAnsi(List<LayoutRoute> layoutRoutes, int
totalHeight) {
+ String plain = renderDiagram(layoutRoutes, totalHeight);
+ if (counterPositions.isEmpty()) {
+ return plain;
+ }
+ String[] lines = plain.split("\n", -1);
+ for (CounterPos cp : counterPositions) {
+ if (cp.row >= 0 && cp.row < lines.length) {
+ String line = lines[cp.row];
+ if (cp.col >= 0 && cp.col + cp.length <= line.length()) {
+ String before = line.substring(0, cp.col);
+ String counter = line.substring(cp.col, cp.col +
cp.length);
+ String after = line.substring(cp.col + cp.length);
+ String color = cp.type == CounterType.OK ? "\033[32m" :
"\033[31m";
+ lines[cp.row] = before + color + counter + "\033[0m" +
after;
+ }
+ }
+ }
+ return String.join("\n", lines);
+ }
+
public String renderDiagram(List<LayoutRoute> layoutRoutes, int
totalHeight) {
+ counterPositions.clear();
int maxPixelX = layoutRoutes.stream()
.mapToInt(lr -> lr.maxX).max().orElse(nodeWidth) + PADDING;
int gridWidth = toCol(maxPixelX) + boxWidth + 4;
@@ -166,7 +209,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 +223,53 @@ 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) {
+ String okStr = "" + ok;
+ int col = toCx + 2;
+ drawText(grid, toTop - 1, col, okStr);
+ counterPositions.add(new CounterPos(toTop - 1, col,
okStr.length(), CounterType.OK));
+ }
+ if (failed > 0) {
+ String failStr = "" + failed;
+ int col = toCx - 1 - failStr.length();
+ drawText(grid, toTop - 1, col, failStr);
+ counterPositions.add(new CounterPos(toTop - 1, col,
failStr.length(), CounterType.FAIL));
+ }
}
- 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;
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
index cde562c5235a..c29cb7b8df37 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
@@ -211,8 +211,8 @@ public class CamelRouteDiagramAction extends
ActionWatchCommand {
if (isTextTheme()) {
RouteDiagramAsciiRenderer asciiRenderer
- = new RouteDiagramAsciiRenderer(engine.getNodeWidth(),
isUnicodeTheme());
- String ascii = asciiRenderer.renderDiagram(layoutRoutes,
currentY);
+ = new RouteDiagramAsciiRenderer(engine.getNodeWidth(),
isUnicodeTheme(), pid > 0 && metric);
+ String ascii = asciiRenderer.renderDiagramAnsi(layoutRoutes,
currentY);
if (output != null) {
String fileName = output.endsWith(".png")
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 0984a3e65fff..f033cb79884a 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -57,6 +57,7 @@ import dev.tamboui.text.Text;
import dev.tamboui.tui.TuiConfig;
import dev.tamboui.tui.TuiRunner;
import dev.tamboui.tui.event.Event;
+import dev.tamboui.tui.event.KeyCode;
import dev.tamboui.tui.event.KeyEvent;
import dev.tamboui.tui.event.MouseEvent;
import dev.tamboui.tui.event.MouseEventKind;
@@ -176,6 +177,8 @@ public class CamelMonitor extends CamelCommand {
// Diagram state
private boolean showDiagram;
private boolean diagramTextMode;
+ private boolean diagramMetrics = true;
+ private List<RouteDiagramAsciiRenderer.CounterPos> diagramCounterPositions
= Collections.emptyList();
private List<String> diagramLines = Collections.emptyList();
private int diagramScroll;
private int diagramScrollX;
@@ -260,11 +263,6 @@ public class CamelMonitor extends CamelCommand {
runner.quit();
return true;
}
- if (ke.isChar('r')) {
- refreshData();
- return true;
- }
-
// Tab switching with number keys
if (ke.isChar('1')) {
return handleTabKey(TAB_OVERVIEW);
@@ -400,6 +398,18 @@ public class CamelMonitor extends CamelCommand {
return true;
}
+ if (tab == TAB_ROUTES && showDiagram && ke.isCharIgnoreCase('m')) {
+ diagramMetrics = !diagramMetrics;
+ diagramLoading.set(false);
+ loadDiagramForSelectedRoute();
+ return true;
+ }
+ if (tab == TAB_ROUTES && showDiagram && !diagramTextMode &&
ke.isKey(KeyCode.F5)) {
+ diagramLoading.set(false);
+ loadDiagramForSelectedRoute();
+ return true;
+ }
+
// Health tab: DOWN filter
if (tab == TAB_HEALTH && ke.isCharIgnoreCase('d')) {
showOnlyDown = !showOnlyDown;
@@ -475,6 +485,9 @@ public class CamelMonitor extends CamelCommand {
long interval = showDiagram ? Math.max(refreshInterval, 1000) :
refreshInterval;
if (now - lastRefresh >= interval) {
refreshData();
+ if (showDiagram && diagramTextMode && diagramMetrics) {
+ loadDiagramForSelectedRoute();
+ }
return true;
}
// Skip re-render when showing image diagram to prevent flicker
@@ -1042,7 +1055,7 @@ public class CamelMonitor extends CamelCommand {
if (diagramScrollX > 0) {
line = diagramScrollX < line.length() ?
line.substring(diagramScrollX) : "";
}
- lines.add(styleDiagramLine(line));
+ lines.add(styleDiagramLine(line, i, diagramScrollX));
}
// Layout: outer block wraps everything, inner splits content +
scrollbars
@@ -1163,22 +1176,37 @@ public class CamelMonitor extends CamelCommand {
}
}
- private Line styleDiagramLine(String text) {
+ private Line styleDiagramLine(String text, int row, int scrollX) {
+ // Build counter color ranges for this row
+ List<int[]> counterRanges = new ArrayList<>();
+ for (RouteDiagramAsciiRenderer.CounterPos cp :
diagramCounterPositions) {
+ if (cp.row() == row) {
+ int start = cp.col() - scrollX;
+ int end = start + cp.length();
+ if (end > 0 && start < text.length()) {
+ start = Math.max(0, start);
+ end = Math.min(end, text.length());
+ int colorFlag = cp.type() ==
RouteDiagramAsciiRenderer.CounterType.OK ? 1 : 2;
+ counterRanges.add(new int[] { start, end, colorFlag });
+ }
+ }
+ }
+
List<Span> spans = new ArrayList<>();
int idx = 0;
while (idx < text.length()) {
int open = text.indexOf('[', idx);
if (open < 0) {
- spans.add(Span.styled(text.substring(idx),
Style.EMPTY.fg(Color.WHITE)));
+ addStyledSegment(spans, text, idx, text.length(),
counterRanges, Color.WHITE);
break;
}
int close = text.indexOf(']', open);
if (close < 0) {
- spans.add(Span.styled(text.substring(idx),
Style.EMPTY.fg(Color.WHITE)));
+ addStyledSegment(spans, text, idx, text.length(),
counterRanges, Color.WHITE);
break;
}
if (open > idx) {
- spans.add(Span.styled(text.substring(idx, open),
Style.EMPTY.fg(Color.GRAY)));
+ addStyledSegment(spans, text, idx, open, counterRanges,
Color.GRAY);
}
String tag = text.substring(open + 1, close);
Color tagColor = getDiagramNodeColor(tag);
@@ -1186,16 +1214,48 @@ public class CamelMonitor extends CamelCommand {
int afterTag = close + 1;
int nextOpen = text.indexOf('[', afterTag);
- int nextNewline = text.length();
- int labelEnd = nextOpen >= 0 ? nextOpen : nextNewline;
+ int labelEnd = nextOpen >= 0 ? nextOpen : text.length();
if (afterTag < labelEnd) {
- spans.add(Span.styled(text.substring(afterTag, labelEnd),
Style.EMPTY.fg(Color.WHITE)));
+ addStyledSegment(spans, text, afterTag, labelEnd,
counterRanges, Color.WHITE);
}
idx = labelEnd;
}
return Line.from(spans);
}
+ private void addStyledSegment(
+ List<Span> spans, String text, int from, int to, List<int[]>
counterRanges, Color defaultColor) {
+ int pos = from;
+ while (pos < to) {
+ int[] cr = findNextCounterRange(counterRanges, pos, to);
+ if (cr != null) {
+ if (pos < cr[0]) {
+ spans.add(Span.styled(text.substring(pos, cr[0]),
Style.EMPTY.fg(defaultColor)));
+ }
+ int counterEnd = Math.min(cr[1], to);
+ Color counterColor = cr[2] == 1 ? Color.GREEN : Color.RED;
+ spans.add(Span.styled(text.substring(cr[0], counterEnd),
Style.EMPTY.fg(counterColor).bold()));
+ pos = counterEnd;
+ } else {
+ spans.add(Span.styled(text.substring(pos, to),
Style.EMPTY.fg(defaultColor)));
+ pos = to;
+ }
+ }
+ }
+
+ private static int[] findNextCounterRange(List<int[]> ranges, int pos, int
limit) {
+ int[] best = null;
+ for (int[] range : ranges) {
+ if (range[1] > pos && range[0] < limit) {
+ int start = Math.max(range[0], pos);
+ if (best == null || start < best[0]) {
+ best = new int[] { start, range[1], range[2] };
+ }
+ }
+ }
+ return best;
+ }
+
private Color getDiagramNodeColor(String type) {
if (type == null) {
return Color.GRAY;
@@ -1246,27 +1306,30 @@ public class CamelMonitor extends CamelCommand {
// Capture state needed by the background thread
String pid = selectedPid;
boolean textMode = diagramTextMode;
+ boolean showMetrics = diagramMetrics;
String routeId = selectedRoute.routeId;
- // Show loading state immediately
- diagramRouteId = routeId;
- diagramLines = List.of("(Loading diagram...)");
- diagramImageData = null;
- diagramFullImageData = null;
- showDiagram = true;
- diagramScroll = 0;
- diagramScrollX = 0;
+ boolean initialLoad = !showDiagram;
+ if (initialLoad) {
+ diagramRouteId = routeId;
+ diagramLines = List.of("(Loading diagram...)");
+ diagramImageData = null;
+ diagramFullImageData = null;
+ showDiagram = true;
+ diagramScroll = 0;
+ diagramScrollX = 0;
+ }
runner.scheduler().execute(() -> {
try {
- loadDiagramInBackground(pid, textMode, routeId);
+ loadDiagramInBackground(pid, textMode, routeId, showMetrics);
} finally {
diagramLoading.set(false);
}
});
}
- private void loadDiagramInBackground(String pid, boolean textMode, String
routeId) {
+ private void loadDiagramInBackground(String pid, boolean textMode, String
routeId, boolean metrics) {
Path outputFile = getOutputFile(pid);
PathUtils.deleteFile(outputFile);
@@ -1310,6 +1373,13 @@ public class CamelMonitor extends CamelCommand {
node.code = Jsoner.unescape(objToString(line.get("code")));
Integer level = line.getInteger("level");
node.level = level != null ? level : 0;
+ JsonObject stats = (JsonObject) line.get("statistics");
+ if (stats != null) {
+ RouteDiagramLayoutEngine.StatInfo stat = new
RouteDiagramLayoutEngine.StatInfo();
+ stat.exchangesTotal =
stats.getLongOrDefault("exchangesTotal", 0);
+ stat.exchangesFailed =
stats.getLongOrDefault("exchangesFailed", 0);
+ node.stat = stat;
+ }
route.nodes.add(node);
}
}
@@ -1317,14 +1387,42 @@ public class CamelMonitor extends CamelCommand {
}
if (textMode) {
- String ascii = renderAscii(diagramRoutes,
RouteDiagramLayoutEngine.DEFAULT_BOX_WIDTH, "CODE", true);
+ RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine(
+ RouteDiagramLayoutEngine.DEFAULT_BOX_WIDTH,
RouteDiagramLayoutEngine.DEFAULT_FONT_SIZE,
+ RouteDiagramLayoutEngine.NodeLabelMode.CODE);
+ List<RouteDiagramLayoutEngine.LayoutRoute> layoutRoutes = new
ArrayList<>();
+ int currentY = RouteDiagramLayoutEngine.PADDING;
+ for (RouteDiagramLayoutEngine.RouteInfo r : diagramRoutes) {
+ RouteDiagramLayoutEngine.LayoutRoute lr =
engine.layoutRoute(r, currentY);
+ layoutRoutes.add(lr);
+ currentY = lr.maxY + RouteDiagramLayoutEngine.V_GAP;
+ }
+ RouteDiagramAsciiRenderer asciiRenderer = new
RouteDiagramAsciiRenderer(
+ RouteDiagramLayoutEngine.DEFAULT_BOX_WIDTH *
RouteDiagramLayoutEngine.SCALE, true, metrics);
+ String ascii = asciiRenderer.renderDiagram(layoutRoutes, currentY);
+ List<RouteDiagramAsciiRenderer.CounterPos> origPositions =
asciiRenderer.getCounterPositions();
+
+ // Build result lines, remapping counter positions to account for
removed empty lines
+ String[] rawLines = ascii.split("\n", -1);
List<String> result = new ArrayList<>();
- for (String line : ascii.split("\n", -1)) {
- if (!line.isEmpty()) {
- result.add(line);
+ int[] rowMapping = new int[rawLines.length];
+ int newRow = 0;
+ for (int i = 0; i < rawLines.length; i++) {
+ if (!rawLines[i].isEmpty()) {
+ rowMapping[i] = newRow++;
+ result.add(rawLines[i]);
+ } else {
+ rowMapping[i] = -1;
+ }
+ }
+ List<RouteDiagramAsciiRenderer.CounterPos> positions = new
ArrayList<>();
+ for (RouteDiagramAsciiRenderer.CounterPos cp : origPositions) {
+ if (cp.row() >= 0 && cp.row() < rowMapping.length &&
rowMapping[cp.row()] >= 0) {
+ positions.add(new RouteDiagramAsciiRenderer.CounterPos(
+ rowMapping[cp.row()], cp.col(), cp.length(),
cp.type()));
}
}
- applyDiagramResult(routeId, result, null, null, null);
+ applyDiagramResult(routeId, result, null, null, null, positions);
} else {
TerminalImageCapabilities caps =
TerminalImageCapabilities.detect();
if (caps.supportsNativeImages()) {
@@ -1336,7 +1434,9 @@ public class CamelMonitor extends CamelCommand {
layoutRoutes.add(lr);
totalHeight = lr.maxY;
}
- RouteDiagramRenderer renderer = new RouteDiagramRenderer();
+ RouteDiagramRenderer renderer = new RouteDiagramRenderer(
+ engine.getNodeWidth(),
RouteDiagramLayoutEngine.DEFAULT_FONT_SIZE * RouteDiagramLayoutEngine.SCALE,
+ metrics);
RouteDiagramRenderer.DiagramColors colors =
RouteDiagramRenderer.DiagramColors.parse("transparent");
java.awt.image.BufferedImage image =
renderer.renderDiagram(layoutRoutes, totalHeight, colors);
ImageData fullImage = ImageData.fromBufferedImage(image);
@@ -1353,44 +1453,35 @@ public class CamelMonitor extends CamelCommand {
private void applyDiagramResult(
String routeId, List<String> lines, ImageData imageData, ImageData
fullImageData, ImageProtocol protocol) {
+ applyDiagramResult(routeId, lines, imageData, fullImageData, protocol,
Collections.emptyList());
+ }
+
+ private void applyDiagramResult(
+ String routeId, List<String> lines, ImageData imageData, ImageData
fullImageData, ImageProtocol protocol,
+ List<RouteDiagramAsciiRenderer.CounterPos> positions) {
if (runner == null) {
return;
}
runner.runOnRenderThread(() -> {
+ boolean wasShowing = showDiagram;
diagramRouteId = routeId;
diagramLines = lines;
+ diagramCounterPositions = positions;
diagramImageData = imageData;
diagramFullImageData = fullImageData;
diagramProtocol = protocol;
- diagramScroll = 0;
- diagramScrollX = 0;
- diagramCropX = -1;
- diagramCropY = -1;
- diagramCropW = -1;
- diagramCropH = -1;
+ if (!wasShowing) {
+ diagramScroll = 0;
+ diagramScrollX = 0;
+ diagramCropX = -1;
+ diagramCropY = -1;
+ diagramCropW = -1;
+ diagramCropH = -1;
+ }
showDiagram = true;
});
}
- private static String renderAscii(
- List<RouteDiagramLayoutEngine.RouteInfo> routes, int nodeWidth,
String nodeLabel, boolean unicode) {
- RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine(
- nodeWidth, RouteDiagramLayoutEngine.DEFAULT_FONT_SIZE,
-
RouteDiagramLayoutEngine.NodeLabelMode.valueOf(nodeLabel.toUpperCase()));
-
- List<RouteDiagramLayoutEngine.LayoutRoute> layoutRoutes = new
ArrayList<>();
- int currentY = RouteDiagramLayoutEngine.PADDING;
- for (RouteDiagramLayoutEngine.RouteInfo route : routes) {
- RouteDiagramLayoutEngine.LayoutRoute lr =
engine.layoutRoute(route, currentY);
- layoutRoutes.add(lr);
- currentY = lr.maxY + RouteDiagramLayoutEngine.V_GAP;
- }
-
- RouteDiagramAsciiRenderer renderer = new RouteDiagramAsciiRenderer(
- nodeWidth * RouteDiagramLayoutEngine.SCALE, unicode);
- return renderer.renderDiagram(layoutRoutes, currentY);
- }
-
private static JsonObject pollJsonResponse(Path outputFile, long timeout) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeout) {
@@ -1979,17 +2070,21 @@ public class CamelMonitor extends CamelCommand {
if (tab == TAB_OVERVIEW) {
hint(spans, "q", "quit");
- hint(spans, "r", "refresh");
hint(spans, "\u2191\u2193", "navigate");
hint(spans, "Enter", "details");
hint(spans, "1-6", "tabs");
- hintRefresh(spans);
} else if (tab == TAB_ROUTES && showDiagram) {
String closeKey = diagramTextMode ? "D" : "d";
hint(spans, closeKey + "/Esc", "close");
hint(spans, "\u2191\u2193\u2190\u2192", "scroll");
hint(spans, "PgUp/PgDn", "page");
- hintLast(spans, "Home/End", "top/bottom");
+ hint(spans, "Home/End", "top/bottom");
+ if (diagramMetrics && !diagramTextMode) {
+ hint(spans, "m", "metrics [on]");
+ hintLast(spans, "F5", "refresh counters");
+ } else {
+ hintLast(spans, "m", "metrics" + (diagramMetrics ? " [on]" : "
[off]"));
+ }
} else if (tab == TAB_ROUTES) {
hint(spans, "Esc", "back");
hint(spans, "\u2191\u2193", "navigate");
@@ -1997,13 +2092,11 @@ public class CamelMonitor extends CamelCommand {
hint(spans, "d", "diagram");
hint(spans, "D", "text diagram");
hint(spans, "1-6", "tabs");
- hintRefresh(spans);
} else if (tab == TAB_HEALTH) {
hint(spans, "Esc", "back");
hint(spans, "\u2191\u2193", "navigate");
hint(spans, "d", "toggle DOWN");
hint(spans, "1-6", "tabs");
- hintRefresh(spans);
} else if (tab == TAB_LOG) {
hint(spans, "Esc", "back");
hint(spans, "\u2191\u2193", "scroll");
@@ -2021,7 +2114,6 @@ public class CamelMonitor extends CamelCommand {
hint(spans, "Esc", "back");
hint(spans, "\u2191\u2193", "navigate");
hint(spans, "1-6", "tabs");
- hintRefresh(spans);
}
frame.renderWidget(Paragraph.from(Line.from(spans)), area);
@@ -2039,13 +2131,6 @@ public class CamelMonitor extends CamelCommand {
spans.add(Span.raw(" " + label));
}
- private void hintRefresh(List<Span> spans) {
- String refreshLabel = refreshInterval >= 1000
- ? (refreshInterval / 1000) + "s"
- : refreshInterval + "ms";
- spans.add(Span.styled("Refresh: " + refreshLabel, Style.EMPTY.dim()));
- }
-
// ---- Data Loading ----
private void refreshData() {