This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-23518-tui-trace-refactor in repository https://gitbox.apache.org/repos/asf/camel.git
commit b8c1d91a0e3cbb96703189b94dcba11fbc8fcf0b Author: Claus Ibsen <[email protected]> AuthorDate: Thu May 14 22:19:06 2026 +0200 CAMEL-23518: Refactor TUI trace tab with two-level exchange drill-down Level 1 - Exchange list: Shows unique exchange IDs with summary (timestamp, route, status, elapsed, steps, ago). Follow mode with 'f' key. Press Enter to drill into an exchange. Level 2 - Exchange detail: Shows step-by-step trace for the selected exchange, identical to History tab layout: - Direction arrows with tree indentation via nodeLevel - Detail panel with properties (p), variables (v), headers (h), body (b) toggles - all with type prefixes using shortTypeName - Scrollbar and word wrap (w) support for detail panel - PgUp/PgDn for scrolling - Press Esc to go back to exchange list Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../dsl/jbang/core/commands/tui/CamelMonitor.java | 617 ++++++++++++++++----- 1 file changed, 484 insertions(+), 133 deletions(-) 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 99bb52adc022..ccee189722eb 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 @@ -173,10 +173,20 @@ public class CamelMonitor extends CamelCommand { // Trace state private final AtomicReference<List<TraceEntry>> traces = new AtomicReference<>(Collections.emptyList()); private final TableState traceTableState = new TableState(); + private final TableState traceStepTableState = new TableState(); private final Map<String, Long> traceFilePositions = new ConcurrentHashMap<>(); + private static final String[] TRACE_SORT_COLUMNS = { "time", "route", "elapsed", "exchange" }; + private String traceSort = "time"; + private int traceSortIndex; + private boolean traceDetailView; + private volatile List<String> traceSortedExchangeIds = Collections.emptyList(); + private String traceSelectedExchangeId; + private boolean showTraceProperties; + private boolean showTraceVariables; private boolean showTraceHeaders = true; private boolean showTraceBody = true; - private boolean traceFollowMode = true; + private boolean traceWordWrap; + private int traceDetailScroll; // History state private volatile List<HistoryEntry> historyEntries = Collections.emptyList(); @@ -259,6 +269,12 @@ public class CamelMonitor extends CamelCommand { diagramFullImageData = null; return true; } + if (traceDetailView) { + traceDetailView = false; + traceSelectedExchangeId = null; + traceDetailScroll = 0; + return true; + } // If in a detail tab, go back to overview first if (tabsState.selected() != TAB_OVERVIEW) { tabsState.select(TAB_OVERVIEW); @@ -331,6 +347,8 @@ public class CamelMonitor extends CamelCommand { } } else if (tab == TAB_HISTORY) { historyDetailScroll = Math.max(0, historyDetailScroll - 5); + } else if (tab == TAB_TRACE && traceDetailView) { + traceDetailScroll = Math.max(0, traceDetailScroll - 5); } return true; } @@ -343,6 +361,8 @@ public class CamelMonitor extends CamelCommand { } } else if (tab == TAB_HISTORY) { historyDetailScroll += 5; + } else if (tab == TAB_TRACE && traceDetailView) { + traceDetailScroll += 5; } return true; } @@ -477,25 +497,60 @@ public class CamelMonitor extends CamelCommand { } } - // Trace tab: headers/body toggle and follow mode + // Trace tab if (tab == TAB_TRACE) { - if (ke.isCharIgnoreCase('h')) { - showTraceHeaders = !showTraceHeaders; - return true; - } - if (ke.isCharIgnoreCase('b')) { - showTraceBody = !showTraceBody; - return true; - } - if (ke.isCharIgnoreCase('f')) { - traceFollowMode = !traceFollowMode; - if (traceFollowMode) { - List<TraceEntry> current = traces.get(); - if (!current.isEmpty()) { - traceTableState.select(current.size() - 1); + if (traceDetailView) { + if (ke.isCancel()) { + traceDetailView = false; + traceSelectedExchangeId = null; + traceDetailScroll = 0; + return true; + } + if (ke.isCharIgnoreCase('p')) { + showTraceProperties = !showTraceProperties; + return true; + } + if (ke.isCharIgnoreCase('v')) { + showTraceVariables = !showTraceVariables; + return true; + } + if (ke.isCharIgnoreCase('h')) { + showTraceHeaders = !showTraceHeaders; + return true; + } + if (ke.isCharIgnoreCase('b')) { + showTraceBody = !showTraceBody; + return true; + } + if (ke.isCharIgnoreCase('w')) { + traceWordWrap = !traceWordWrap; + traceDetailScroll = 0; + return true; + } + } else { + if (ke.isCharIgnoreCase('s')) { + traceSortIndex = (traceSortIndex + 1) % TRACE_SORT_COLUMNS.length; + traceSort = TRACE_SORT_COLUMNS[traceSortIndex]; + return true; + } + if (ke.isConfirm()) { + Integer sel = traceTableState.selected(); + if (sel != null && sel >= 0 && sel < traceSortedExchangeIds.size()) { + traceSelectedExchangeId = traceSortedExchangeIds.get(sel); + traceDetailView = true; + traceStepTableState.select(0); + traceDetailScroll = 0; } + return true; + } + if (ke.isKey(KeyCode.F5)) { + if (selectedPid != null) { + traceFilePositions.clear(); + traces.set(Collections.emptyList()); + refreshTraceData(List.of(Long.parseLong(selectedPid))); + } + return true; } - return true; } } @@ -558,6 +613,17 @@ public class CamelMonitor extends CamelCommand { historyTableState.select(0); } } + if (tab == TAB_TRACE && selectedPid != null) { + traceFilePositions.clear(); + traces.set(Collections.emptyList()); + refreshTraceData(List.of(Long.parseLong(selectedPid))); + traceDetailView = false; + traceSelectedExchangeId = null; + List<String> ids = getTraceExchangeIds(); + if (!ids.isEmpty()) { + traceTableState.select(0); + } + } tabsState.select(tab); return true; } @@ -586,8 +652,12 @@ public class CamelMonitor extends CamelCommand { logTableState.selectPrevious(); } case TAB_TRACE -> { - traceFollowMode = false; - traceTableState.selectPrevious(); + if (traceDetailView) { + traceStepTableState.selectPrevious(); + traceDetailScroll = 0; + } else { + traceTableState.selectPrevious(); + } } case TAB_HISTORY -> { historyTableState.selectPrevious(); @@ -614,8 +684,14 @@ public class CamelMonitor extends CamelCommand { } case TAB_LOG -> logTableState.selectNext(filteredLogEntries.size()); case TAB_TRACE -> { - List<TraceEntry> current = traces.get(); - traceTableState.selectNext(current.size()); + if (traceDetailView) { + List<TraceEntry> steps = getTraceSteps(traceSelectedExchangeId); + traceStepTableState.selectNext(steps.size()); + traceDetailScroll = 0; + } else { + List<String> exchangeIds = getTraceExchangeIds(); + traceTableState.selectNext(exchangeIds.size()); + } } case TAB_HISTORY -> { historyTableState.selectNext(historyEntries.size()); @@ -649,12 +725,17 @@ public class CamelMonitor extends CamelCommand { String camelVersion = VersionHelper.extractCamelVersion(); long activeCount = infos.stream().filter(i -> !i.vanishing).count(); - Line titleLine = Line.from( - Span.styled(" Camel Monitor", Style.EMPTY.fg(Color.rgb(0xF6, 0x91, 0x23)).bold()), - Span.raw(" "), - Span.styled(camelVersion != null ? "v" + camelVersion : "", Style.EMPTY.fg(Color.GREEN)), - Span.raw(" "), - Span.styled(activeCount + " integration(s)", Style.EMPTY.fg(Color.CYAN))); + List<Span> titleSpans = new ArrayList<>(); + titleSpans.add(Span.styled(" Camel Monitor", Style.EMPTY.fg(Color.rgb(0xF6, 0x91, 0x23)).bold())); + titleSpans.add(Span.raw(" ")); + titleSpans.add(Span.styled(camelVersion != null ? "v" + camelVersion : "", Style.EMPTY.fg(Color.GREEN))); + titleSpans.add(Span.raw(" ")); + titleSpans.add(Span.styled(activeCount + " integration(s)", Style.EMPTY.fg(Color.CYAN))); + if (selectedPid != null) { + titleSpans.add(Span.raw(" ")); + titleSpans.add(Span.styled("selected: " + selectedName(), Style.EMPTY.fg(Color.YELLOW))); + } + Line titleLine = Line.from(titleSpans); Block headerBlock = Block.builder() .borderType(BorderType.ROUNDED) @@ -667,16 +748,15 @@ public class CamelMonitor extends CamelCommand { } private void renderTabs(Frame frame, Rect area) { - String sel = selectedPid != null ? " [" + selectedName() + "]" : ""; Tabs tabs = Tabs.builder() .titles( " 1 Overview ", - " 2 Routes" + sel + " ", - " 3 Log" + sel + " ", - " 4 Endpoints" + sel + " ", - " 5 Health" + sel + " ", - " 6 History" + sel + " ", - " 7 Trace" + sel + " ") + " 2 Routes ", + " 3 Log ", + " 4 Endpoints ", + " 5 Health ", + " 6 History ", + " 7 Trace ") .highlightStyle(Style.EMPTY.fg(Color.rgb(0xF6, 0x91, 0x23)).bold()) .divider(Span.styled(" | ", Style.EMPTY.dim())) .build(); @@ -1003,6 +1083,16 @@ public class CamelMonitor extends CamelCommand { }; } + private String traceSortLabel(String label, String column) { + return traceSort.equals(column) ? label + "▼" : label; + } + + private Style traceSortStyle(String column) { + return traceSort.equals(column) + ? Style.EMPTY.fg(Color.YELLOW).bold() + : Style.EMPTY.bold(); + } + private String routeSortLabel(String label, String column) { return routeSort.equals(column) ? label + "\u25BC" : label; } @@ -1981,126 +2071,279 @@ public class CamelMonitor extends CamelCommand { return; } - List<TraceEntry> current = traces.get(); + if (traceDetailView && traceSelectedExchangeId != null) { + renderTraceExchangeDetail(frame, area); + } else { + renderTraceExchangeList(frame, area); + } + } - // Layout: trace list (50%) + detail panel (50%) - List<Rect> chunks = Layout.vertical() - .constraints(Constraint.percentage(50), Constraint.fill()) - .split(area); + private void renderTraceExchangeList(Frame frame, Rect area) { + List<String> exchangeIds = getTraceExchangeIds(); - // Auto-follow: select last entry - if (traceFollowMode && !current.isEmpty()) { - traceTableState.select(current.size() - 1); + // Build exchange summaries for sorting + List<TraceEntry> current = traces.get(); + record ExchangeSummary(String exchangeId, String timestamp, long epochMs, String routeId, + String status, long elapsed, int steps) { + } + List<ExchangeSummary> summaries = new ArrayList<>(); + for (String exchangeId : exchangeIds) { + TraceEntry first = null; + TraceEntry lastEntry = null; + TraceEntry latestEntry = null; + int stepCount = 0; + for (TraceEntry e : current) { + if (exchangeId.equals(e.exchangeId)) { + if (first == null) { + first = e; + } + latestEntry = e; + if (e.last) { + lastEntry = e; + } + stepCount++; + } + } + if (first != null) { + TraceEntry doneEntry = lastEntry != null ? lastEntry : latestEntry; + String status = doneEntry.status != null ? doneEntry.status : "Processing"; + long elapsed = doneEntry.elapsed; + summaries.add(new ExchangeSummary( + exchangeId, first.timestamp, first.epochMs, + first.routeId, status, elapsed, stepCount)); + } } - // Trace list + // Sort + summaries.sort((a, b) -> switch (traceSort) { + case "time" -> Long.compare(b.epochMs, a.epochMs); + case "route" -> { + String ra = a.routeId != null ? a.routeId : ""; + String rb = b.routeId != null ? b.routeId : ""; + yield ra.compareToIgnoreCase(rb); + } + case "elapsed" -> Long.compare(b.elapsed, a.elapsed); + case "exchange" -> { + yield a.exchangeId.compareTo(b.exchangeId); + } + default -> 0; + }); + + traceSortedExchangeIds = summaries.stream().map(ExchangeSummary::exchangeId).toList(); + List<Row> rows = new ArrayList<>(); - for (TraceEntry entry : current) { - String status = entry.status != null ? entry.status : ""; - Style statusStyle = switch (status) { + for (ExchangeSummary s : summaries) { + Style statusStyle = switch (s.status) { case "Done" -> Style.EMPTY.fg(Color.GREEN); case "Failed" -> Style.EMPTY.fg(Color.RED); case "Processing" -> Style.EMPTY.fg(Color.YELLOW); - default -> Style.EMPTY.fg(Color.WHITE); + default -> Style.EMPTY; }; + rows.add(Row.from( + Cell.from(s.timestamp != null ? truncate(s.timestamp, 12) : ""), + Cell.from(Span.styled( + s.routeId != null ? truncate(s.routeId, 15) : "", + Style.EMPTY.fg(Color.CYAN))), + Cell.from(Span.styled(s.status, statusStyle)), + Cell.from(String.format("%7s", s.elapsed + "ms")), + Cell.from(String.format("%6s", s.steps)), + Cell.from(s.exchangeId))); + } - String bodyPreview = entry.bodyPreview != null ? truncate(entry.bodyPreview, 40) : ""; + Row header = Row.from( + Cell.from(Span.styled(traceSortLabel("TIME", "time"), traceSortStyle("time"))), + Cell.from(Span.styled(traceSortLabel("ROUTE", "route"), traceSortStyle("route"))), + Cell.from(Span.styled("STATUS", Style.EMPTY.bold())), + Cell.from(Span.styled(traceSortLabel("ELAPSED", "elapsed"), traceSortStyle("elapsed"))), + Cell.from(Span.styled("STEPS", Style.EMPTY.bold())), + Cell.from(Span.styled(traceSortLabel("EXCHANGE", "exchange"), traceSortStyle("exchange")))); + + String traceTitle = String.format(" Traces [%d] sort:%s ", summaries.size(), traceSort); + + Table table = Table.builder() + .rows(rows) + .header(header) + .widths( + Constraint.length(12), + Constraint.length(15), + Constraint.length(12), + Constraint.length(10), + Constraint.length(6), + Constraint.fill()) + .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue()) + .highlightSpacing(Table.HighlightSpacing.ALWAYS) + .block(Block.builder().borderType(BorderType.ROUNDED).title(traceTitle).build()) + .build(); + + frame.renderStatefulWidget(table, area, traceTableState); + } + + private void renderTraceExchangeDetail(Frame frame, Rect area) { + List<TraceEntry> steps = getTraceSteps(traceSelectedExchangeId); + + // Layout: step table (fixed 10 rows) + detail panel (fill) + List<Rect> chunks = Layout.vertical() + .constraints(Constraint.length(10), Constraint.fill()) + .split(area); + + // Step table (like History) + List<Row> rows = new ArrayList<>(); + for (TraceEntry entry : steps) { + Style dirStyle; + if (entry.first) { + dirStyle = Style.EMPTY.fg(Color.GREEN); + } else if (entry.last) { + dirStyle = entry.failed ? Style.EMPTY.fg(Color.RED) : Style.EMPTY.fg(Color.GREEN); + } else { + dirStyle = Style.EMPTY; + } + String elapsed = entry.elapsed >= 0 ? entry.elapsed + "ms" : ""; rows.add(Row.from( + Cell.from(Span.styled(entry.direction, dirStyle)), Cell.from(entry.timestamp != null ? truncate(entry.timestamp, 12) : ""), - Cell.from(entry.pid != null ? entry.pid : ""), Cell.from(Span.styled( entry.routeId != null ? truncate(entry.routeId, 15) : "", Style.EMPTY.fg(Color.CYAN))), Cell.from(entry.nodeId != null ? truncate(entry.nodeId, 15) : ""), - Cell.from(Span.styled(status, statusStyle)), - Cell.from(entry.elapsed + "ms"), - Cell.from(bodyPreview))); + Cell.from(entry.processor != null ? entry.processor : ""), + Cell.from(elapsed))); } Row header = Row.from( + Cell.from(Span.styled("", Style.EMPTY.bold())), Cell.from(Span.styled("TIME", Style.EMPTY.bold())), - Cell.from(Span.styled("PID", Style.EMPTY.bold())), Cell.from(Span.styled("ROUTE", Style.EMPTY.bold())), - Cell.from(Span.styled("NODE", Style.EMPTY.bold())), - Cell.from(Span.styled("STATUS", Style.EMPTY.bold())), - Cell.from(Span.styled("ELAPSED", Style.EMPTY.bold())), - Cell.from(Span.styled("BODY", Style.EMPTY.bold()))); + Cell.from(Span.styled("ID", Style.EMPTY.bold())), + Cell.from(Span.styled("PROCESSOR", Style.EMPTY.bold())), + Cell.from(Span.styled("ELAPSED", Style.EMPTY.bold()))); - String traceTitle = String.format(" Traces [%d] %s ", - current.size(), - traceFollowMode ? "[FOLLOW]" : "[SCROLL]"); + String stepTitle = String.format(" Trace [%s] ", truncate(traceSelectedExchangeId, 30)); Table table = Table.builder() .rows(rows) .header(header) .widths( + Constraint.length(4), Constraint.length(12), - Constraint.length(8), Constraint.length(15), Constraint.length(15), - Constraint.length(12), - Constraint.length(10), - Constraint.fill()) + Constraint.fill(), + Constraint.length(10)) .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue()) .highlightSpacing(Table.HighlightSpacing.ALWAYS) - .block(Block.builder().borderType(BorderType.ROUNDED).title(traceTitle).build()) + .block(Block.builder().borderType(BorderType.ROUNDED).title(stepTitle).build()) .build(); - frame.renderStatefulWidget(table, chunks.get(0), traceTableState); + frame.renderStatefulWidget(table, chunks.get(0), traceStepTableState); - // Detail panel - renderTraceDetail(frame, chunks.get(1), current); + // Detail panel for selected step + renderTraceStepDetail(frame, chunks.get(1), steps); } - private void renderTraceDetail(Frame frame, Rect area, List<TraceEntry> current) { - Integer sel = traceTableState.selected(); + private void renderTraceStepDetail(Frame frame, Rect area, List<TraceEntry> steps) { + Integer sel = traceStepTableState.selected(); - if (sel == null || sel < 0 || sel >= current.size()) { + if (sel == null || sel < 0 || sel >= steps.size()) { frame.renderWidget( Paragraph.builder() .text(Text.from(Line.from( - Span.styled(" Select a trace entry to view details", + Span.styled(" Select a trace step to view details", Style.EMPTY.dim())))) - .block(Block.builder().borderType(BorderType.ROUNDED) - .title(" Detail ").build()) + .block(Block.builder().borderType(BorderType.ROUNDED).build()) .build(), area); return; } - TraceEntry entry = current.get(sel); + TraceEntry entry = steps.get(sel); List<Line> lines = new ArrayList<>(); // Exchange info lines.add(Line.from( Span.styled(" Exchange: ", Style.EMPTY.fg(Color.YELLOW).bold()), Span.raw(entry.exchangeId != null ? entry.exchangeId : ""))); - lines.add(Line.from( - Span.styled(" UID: ", Style.EMPTY.fg(Color.YELLOW).bold()), - Span.raw(entry.uid != null ? entry.uid : ""))); - lines.add(Line.from( - Span.styled(" Location: ", Style.EMPTY.fg(Color.YELLOW).bold()), - Span.raw(entry.location != null ? entry.location : ""))); lines.add(Line.from( Span.styled(" Route: ", Style.EMPTY.fg(Color.YELLOW).bold()), Span.raw(entry.routeId != null ? entry.routeId : ""), - Span.raw(" Node: "), + Span.styled(" Node: ", Style.EMPTY.fg(Color.YELLOW).bold()), Span.raw(entry.nodeId != null ? entry.nodeId : ""), Span.raw(entry.nodeLabel != null ? " (" + entry.nodeLabel + ")" : ""))); lines.add(Line.from( - Span.styled(" Status: ", Style.EMPTY.fg(Color.YELLOW).bold()), - Span.raw(entry.status != null ? entry.status : ""), - Span.raw(" Elapsed: "), - Span.raw(entry.elapsed + "ms"))); + Span.styled(" Location: ", Style.EMPTY.fg(Color.YELLOW).bold()), + Span.raw(entry.location != null ? entry.location : ""))); + lines.add(Line.from( + Span.styled(" Elapsed: ", Style.EMPTY.fg(Color.YELLOW).bold()), + Span.raw(entry.elapsed >= 0 ? entry.elapsed + "ms" : ""), + Span.styled(" Thread: ", Style.EMPTY.fg(Color.YELLOW).bold()), + Span.raw(entry.threadName != null ? entry.threadName : ""))); + if (entry.failed) { + lines.add(Line.from( + Span.styled(" Status: ", Style.EMPTY.fg(Color.YELLOW).bold()), + Span.styled("Failed", Style.EMPTY.fg(Color.RED).bold()))); + } lines.add(Line.from(Span.raw(""))); + // Exchange Properties + if (showTraceProperties && entry.exchangeProperties != null && !entry.exchangeProperties.isEmpty()) { + lines.add(Line.from(Span.styled(" Exchange Properties:", Style.EMPTY.fg(Color.GREEN).bold()))); + for (Map.Entry<String, Object> p : entry.exchangeProperties.entrySet()) { + String type = entry.exchangePropertyTypes != null ? entry.exchangePropertyTypes.get(p.getKey()) : null; + String typeLabel; + if (type != null) { + String t = "(" + type + ")"; + t = truncate(t, 20); + typeLabel = String.format("%-20s ", t); + } else { + typeLabel = String.format("%-21s", ""); + } + lines.add(Line.from( + Span.styled(" " + typeLabel, Style.EMPTY.dim()), + Span.styled(p.getKey(), Style.EMPTY.fg(Color.CYAN)), + Span.raw(" = "), + Span.raw(p.getValue() != null ? p.getValue().toString() : "null"))); + } + lines.add(Line.from(Span.raw(""))); + } + + // Exchange Variables + if (showTraceVariables && entry.exchangeVariables != null && !entry.exchangeVariables.isEmpty()) { + lines.add(Line.from(Span.styled(" Exchange Variables:", Style.EMPTY.fg(Color.GREEN).bold()))); + for (Map.Entry<String, Object> v : entry.exchangeVariables.entrySet()) { + String type = entry.exchangeVariableTypes != null ? entry.exchangeVariableTypes.get(v.getKey()) : null; + String typeLabel; + if (type != null) { + String t = "(" + type + ")"; + t = truncate(t, 20); + typeLabel = String.format("%-20s ", t); + } else { + typeLabel = String.format("%-21s", ""); + } + lines.add(Line.from( + Span.styled(" " + typeLabel, Style.EMPTY.dim()), + Span.styled(v.getKey(), Style.EMPTY.fg(Color.CYAN)), + Span.raw(" = "), + Span.raw(v.getValue() != null ? v.getValue().toString() : "null"))); + } + lines.add(Line.from(Span.raw(""))); + } + // Headers if (showTraceHeaders && entry.headers != null && !entry.headers.isEmpty()) { lines.add(Line.from(Span.styled(" Headers:", Style.EMPTY.fg(Color.GREEN).bold()))); for (Map.Entry<String, Object> h : entry.headers.entrySet()) { + String type = entry.headerTypes != null ? entry.headerTypes.get(h.getKey()) : null; + String typeLabel; + if (type != null) { + String t = "(" + type + ")"; + t = truncate(t, 20); + typeLabel = String.format("%-20s ", t); + } else { + typeLabel = String.format("%-21s", ""); + } lines.add(Line.from( - Span.styled(" " + h.getKey(), Style.EMPTY.fg(Color.CYAN)), + Span.styled(" " + typeLabel, Style.EMPTY.dim()), + Span.styled(h.getKey(), Style.EMPTY.fg(Color.CYAN)), Span.raw(" = "), Span.raw(h.getValue() != null ? h.getValue().toString() : "null"))); } @@ -2108,47 +2351,97 @@ public class CamelMonitor extends CamelCommand { } // Body - if (showTraceBody && entry.body != null) { - lines.add(Line.from(Span.styled(" Body:", Style.EMPTY.fg(Color.GREEN).bold()))); - String[] bodyLines = entry.body.split("\n"); - for (String bl : bodyLines) { - lines.add(Line.from(Span.raw(" " + bl))); + if (showTraceBody) { + if (entry.body != null) { + if (entry.bodyType != null) { + lines.add(Line.from( + Span.styled(" Body: ", Style.EMPTY.fg(Color.GREEN).bold()), + Span.styled("(" + entry.bodyType + ")", Style.EMPTY.dim()))); + } else { + lines.add(Line.from(Span.styled(" Body:", Style.EMPTY.fg(Color.GREEN).bold()))); + } + String[] bodyLines = entry.body.split("\n"); + for (String bl : bodyLines) { + lines.add(Line.from(Span.raw(" " + bl))); + } + } else { + lines.add(Line.from(Span.styled(" Body is null", Style.EMPTY.fg(Color.GREEN).bold()))); } lines.add(Line.from(Span.raw(""))); } - // Exchange properties - if (entry.exchangeProperties != null && !entry.exchangeProperties.isEmpty()) { - lines.add(Line.from(Span.styled(" Exchange Properties:", Style.EMPTY.fg(Color.GREEN).bold()))); - for (Map.Entry<String, Object> p : entry.exchangeProperties.entrySet()) { - lines.add(Line.from( - Span.styled(" " + p.getKey(), Style.EMPTY.fg(Color.CYAN)), - Span.raw(" = "), - Span.raw(p.getValue() != null ? p.getValue().toString() : "null"))); - } - lines.add(Line.from(Span.raw(""))); - } + Block block = Block.builder().borderType(BorderType.ROUNDED).build(); + frame.renderWidget(block, area); - // Exchange variables - if (entry.exchangeVariables != null && !entry.exchangeVariables.isEmpty()) { - lines.add(Line.from(Span.styled(" Exchange Variables:", Style.EMPTY.fg(Color.GREEN).bold()))); - for (Map.Entry<String, Object> v : entry.exchangeVariables.entrySet()) { - lines.add(Line.from( - Span.styled(" " + v.getKey(), Style.EMPTY.fg(Color.CYAN)), - Span.raw(" = "), - Span.raw(v.getValue() != null ? v.getValue().toString() : "null"))); + Rect inner = block.inner(area); + int visibleHeight = Math.max(1, inner.height()); + int contentHeight; + if (traceWordWrap) { + int width = Math.max(1, inner.width() - 1); + contentHeight = 0; + for (Line l : lines) { + int w = l.width(); + contentHeight += Math.max(1, (w + width - 1) / width); } + } else { + contentHeight = lines.size(); + } + int maxScroll = Math.max(0, contentHeight - visibleHeight); + if (traceDetailScroll > maxScroll) { + traceDetailScroll = maxScroll; } - String title = String.format(" Detail [%s] ", entry.exchangeId != null ? truncate(entry.exchangeId, 30) : ""); + List<Rect> hChunks = Layout.horizontal() + .constraints(Constraint.fill(), Constraint.length(1)) + .split(inner); Paragraph detail = Paragraph.builder() .text(Text.from(lines)) - .overflow(Overflow.CLIP) - .block(Block.builder().borderType(BorderType.ROUNDED).title(title).build()) + .overflow(traceWordWrap ? Overflow.WRAP_WORD : Overflow.CLIP) + .scroll(traceDetailScroll) .build(); + frame.renderWidget(detail, hChunks.get(0)); - frame.renderWidget(detail, area); + if (lines.size() > visibleHeight) { + ScrollbarState scrollState = new ScrollbarState(); + scrollState.contentLength(lines.size()); + scrollState.viewportContentLength(visibleHeight); + scrollState.position(traceDetailScroll); + frame.renderStatefulWidget( + Scrollbar.builder().build(), + hChunks.get(1), scrollState); + } + } + + private List<String> getTraceExchangeIds() { + List<TraceEntry> current = traces.get(); + List<String> ids = new ArrayList<>(); + for (TraceEntry e : current) { + if (e.exchangeId != null && !ids.contains(e.exchangeId)) { + ids.add(e.exchangeId); + } + } + return ids; + } + + private List<TraceEntry> getTraceSteps(String exchangeId) { + List<TraceEntry> current = traces.get(); + List<TraceEntry> steps = new ArrayList<>(); + for (TraceEntry e : current) { + if (exchangeId != null && exchangeId.equals(e.exchangeId)) { + steps.add(e); + } + } + steps.sort((a, b) -> { + String ua = a.uid != null ? a.uid : ""; + String ub = b.uid != null ? b.uid : ""; + try { + return Long.compare(Long.parseLong(ua), Long.parseLong(ub)); + } catch (NumberFormatException e) { + return ua.compareTo(ub); + } + }); + return steps; } // ---- Tab 7: History ---- @@ -2489,12 +2782,21 @@ public class CamelMonitor extends CamelCommand { hint(spans, "Home/End", "top/end"); hint(spans, "t/d/i/w/e", "levels"); hintLast(spans, "f", "follow"); - } else if (tab == TAB_TRACE) { + } else if (tab == TAB_TRACE && traceDetailView) { hint(spans, "Esc", "back"); hint(spans, "\u2191\u2193", "navigate"); + hint(spans, "PgUp/PgDn", "scroll detail"); + hint(spans, "p", "properties" + (showTraceProperties ? " [on]" : " [off]")); + hint(spans, "v", "variables" + (showTraceVariables ? " [on]" : " [off]")); hint(spans, "h", "headers" + (showTraceHeaders ? " [on]" : " [off]")); hint(spans, "b", "body" + (showTraceBody ? " [on]" : " [off]")); - hintLast(spans, "f", "follow" + (traceFollowMode ? " [on]" : " [off]")); + hintLast(spans, "w", "wrap" + (traceWordWrap ? " [on]" : " [off]")); + } else if (tab == TAB_TRACE) { + hint(spans, "Esc", "back"); + hint(spans, "\u2191\u2193", "navigate"); + hint(spans, "s", "sort"); + hint(spans, "Enter", "details"); + hintLast(spans, "F5", "refresh"); } else if (tab == TAB_HISTORY) { hint(spans, "Esc", "back"); hint(spans, "\u2191\u2193", "navigate"); @@ -2779,17 +3081,22 @@ public class CamelMonitor extends CamelCommand { entry.exchangeId = json.getString("exchangeId"); entry.routeId = json.getString("routeId"); entry.nodeId = json.getString("nodeId"); + entry.nodeShortName = json.getString("nodeShortName"); entry.location = json.getString("location"); entry.nodeLabel = json.getString("nodeLabel"); + entry.threadName = json.getString("threadName"); + entry.first = json.getBooleanOrDefault("first", false); + entry.last = json.getBooleanOrDefault("last", false); + entry.nodeLevel = json.getIntegerOrDefault("nodeLevel", 0); // timestamp is epoch millis (number) Object tsObj = json.get("timestamp"); if (tsObj instanceof Number n) { long epochMs = n.longValue(); + entry.epochMs = epochMs; entry.timestamp = Instant.ofEpochMilli(epochMs) .atZone(ZoneId.systemDefault()) .toLocalTime().toString(); - // Truncate to HH:mm:ss.SSS if (entry.timestamp.length() > 12) { entry.timestamp = entry.timestamp.substring(0, 12); } @@ -2800,7 +3107,8 @@ public class CamelMonitor extends CamelCommand { // Derive status from done/failed booleans Boolean done = (Boolean) json.get("done"); Boolean failed = (Boolean) json.get("failed"); - if (Boolean.TRUE.equals(failed)) { + entry.failed = Boolean.TRUE.equals(failed); + if (entry.failed) { entry.status = "Failed"; } else if (Boolean.TRUE.equals(done)) { entry.status = "Done"; @@ -2819,29 +3127,51 @@ public class CamelMonitor extends CamelCommand { } } + // Compute direction and processor label + if (entry.first || entry.last) { + entry.nodeLevel = Math.max(0, entry.nodeLevel - 1); + } + String indent = " ".repeat(entry.nodeLevel); + if (entry.first) { + entry.direction = "-->"; + String uri = json.getString("endpointUri"); + entry.processor = indent + "from[" + (uri != null ? uri : "") + "]"; + } else if (entry.last) { + entry.direction = "<--"; + entry.processor = indent + (entry.nodeLabel != null ? entry.nodeLabel : ""); + } else { + entry.direction = " >"; + entry.processor = indent + (entry.nodeLabel != null ? entry.nodeLabel : ""); + } + // Parse message object Object msgObj = json.get("message"); if (msgObj instanceof JsonObject message) { - // Headers: can be a list of {key, type, value} or a map + // Headers Object headersObj = message.get("headers"); if (headersObj instanceof List<?> headerList) { entry.headers = new LinkedHashMap<>(); + entry.headerTypes = new LinkedHashMap<>(); for (Object h : headerList) { if (h instanceof JsonObject hObj) { - entry.headers.put( - String.valueOf(hObj.get("key")), - hObj.get("value")); + String key = String.valueOf(hObj.get("key")); + entry.headers.put(key, hObj.get("value")); + Object type = hObj.get("type"); + if (type != null) { + entry.headerTypes.put(key, TuiHelper.shortTypeName(type.toString())); + } } } } else if (headersObj instanceof Map) { entry.headers = new LinkedHashMap<>((Map<String, Object>) headersObj); } - // Body: can be {type, value} or a plain string + // Body Object bodyObj = message.get("body"); if (bodyObj instanceof JsonObject bodyJson) { Object val = bodyJson.get("value"); - entry.body = val != null ? val.toString() : bodyJson.toString(); + entry.body = val != null ? val.toString() : null; + entry.bodyType = TuiHelper.shortTypeName(bodyJson.getString("type")); } else if (bodyObj != null) { entry.body = bodyObj.toString(); } @@ -2849,30 +3179,38 @@ public class CamelMonitor extends CamelCommand { entry.bodyPreview = entry.body.replace("\n", " ").replace("\r", ""); } - // Exchange properties: can be a list of {key, type, value} or a map + // Exchange properties Object propsObj = message.get("exchangeProperties"); if (propsObj instanceof List<?> propList) { entry.exchangeProperties = new LinkedHashMap<>(); + entry.exchangePropertyTypes = new LinkedHashMap<>(); for (Object p : propList) { if (p instanceof JsonObject pObj) { - entry.exchangeProperties.put( - String.valueOf(pObj.get("key")), - pObj.get("value")); + String key = String.valueOf(pObj.get("key")); + entry.exchangeProperties.put(key, pObj.get("value")); + Object type = pObj.get("type"); + if (type != null) { + entry.exchangePropertyTypes.put(key, TuiHelper.shortTypeName(type.toString())); + } } } } else if (propsObj instanceof Map) { entry.exchangeProperties = new LinkedHashMap<>((Map<String, Object>) propsObj); } - // Exchange variables: can be a list of {key, type, value} or a map + // Exchange variables Object varsObj = message.get("exchangeVariables"); if (varsObj instanceof List<?> varList) { entry.exchangeVariables = new LinkedHashMap<>(); + entry.exchangeVariableTypes = new LinkedHashMap<>(); for (Object v : varList) { if (v instanceof JsonObject vObj) { - entry.exchangeVariables.put( - String.valueOf(vObj.get("key")), - vObj.get("value")); + String key = String.valueOf(vObj.get("key")); + entry.exchangeVariables.put(key, vObj.get("value")); + Object type = vObj.get("type"); + if (type != null) { + entry.exchangeVariableTypes.put(key, TuiHelper.shortTypeName(type.toString())); + } } } } else if (varsObj instanceof Map) { @@ -3411,15 +3749,28 @@ public class CamelMonitor extends CamelCommand { String timestamp; String routeId; String nodeId; + String nodeShortName; String nodeLabel; String location; + String processor; + String direction; String status; + String threadName; + boolean first; + boolean last; + boolean failed; + int nodeLevel; long elapsed; + long epochMs; String body; + String bodyType; String bodyPreview; Map<String, Object> headers; + Map<String, String> headerTypes; Map<String, Object> exchangeProperties; + Map<String, String> exchangePropertyTypes; Map<String, Object> exchangeVariables; + Map<String, String> exchangeVariableTypes; } static class LogEntry {
