This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-23672 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 8771d4e43021897316085790e440eb7be66e4cc5 Author: Claus Ibsen <[email protected]> AuthorDate: Thu Jun 4 18:56:00 2026 +0200 CAMEL-23672: Add stub endpoint detection and direction arrows to BacklogTracer and TUI History Co-Authored-By: Claude <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../camel/spi/BacklogTracerEventMessage.java | 7 ++ .../apache/camel/impl/debugger/BacklogTracer.java | 10 ++- .../debugger/DefaultBacklogTracerEventMessage.java | 12 ++++ .../camel/impl/engine/CamelInternalProcessor.java | 24 +++++-- .../dsl/jbang/core/commands/tui/HistoryEntry.java | 2 + .../dsl/jbang/core/commands/tui/HistoryTab.java | 83 +++++++++++++++++----- .../dsl/jbang/core/commands/tui/StatusParser.java | 46 ++++++++++-- .../dsl/jbang/core/commands/tui/TraceEntry.java | 2 + 8 files changed, 154 insertions(+), 32 deletions(-) diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/BacklogTracerEventMessage.java b/core/camel-api/src/main/java/org/apache/camel/spi/BacklogTracerEventMessage.java index a87c08ec0b6a..95a1405d0982 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/BacklogTracerEventMessage.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/BacklogTracerEventMessage.java @@ -203,6 +203,13 @@ public interface BacklogTracerEventMessage extends BacklogEventMessage { */ boolean isRemoteEndpoint(); + /** + * Whether the endpoint is a stub endpoint. + * + * @since 4.21 + */ + boolean isStubEndpoint(); + /** * Gets the endpoint remote address such as URL, hostname, connection-string, or cloud region, that are component * specific. diff --git a/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/BacklogTracer.java b/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/BacklogTracer.java index 873e31e628d1..bda2cccb3516 100644 --- a/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/BacklogTracer.java +++ b/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/BacklogTracer.java @@ -26,6 +26,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicLong; import org.apache.camel.CamelContext; +import org.apache.camel.Endpoint; import org.apache.camel.Exchange; import org.apache.camel.ExchangePropertyKey; import org.apache.camel.NamedNode; @@ -178,7 +179,14 @@ public class BacklogTracer extends ServiceSupport implements org.apache.camel.sp if ((first || last) && fromRouteId != null) { Route route = camelContext.getRoute(fromRouteId); if (route != null && route.getConsumer() != null) { - event.setEndpointUri(route.getConsumer().getEndpoint().getEndpointUri()); + Endpoint ep = route.getConsumer().getEndpoint(); + String endpointUri = ep.getEndpointUri(); + event.setEndpointUri(endpointUri); + event.setRemoteEndpoint(ep.isRemote()); + if ((endpointUri != null && endpointUri.startsWith("stub:")) + || "StubEndpoint".equals(ep.getClass().getSimpleName())) { + event.setStubEndpoint(true); + } } } // synthetic events are snapshots, mark done immediately so elapsed doesn't keep growing diff --git a/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/DefaultBacklogTracerEventMessage.java b/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/DefaultBacklogTracerEventMessage.java index dc0dd0b8755f..515b6e3e517b 100644 --- a/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/DefaultBacklogTracerEventMessage.java +++ b/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/DefaultBacklogTracerEventMessage.java @@ -58,6 +58,7 @@ public final class DefaultBacklogTracerEventMessage implements BacklogTracerEven private final String threadName; private String endpointUri; private boolean remoteEndpoint; + private boolean stubEndpoint; private String endpointServiceUrl; private String endpointServiceProtocol; private Map<String, String> endpointServiceMetadata; @@ -292,6 +293,15 @@ public final class DefaultBacklogTracerEventMessage implements BacklogTracerEven this.remoteEndpoint = remoteEndpoint; } + @Override + public boolean isStubEndpoint() { + return stubEndpoint; + } + + public void setStubEndpoint(boolean stubEndpoint) { + this.stubEndpoint = stubEndpoint; + } + public void setEndpointUri(String endpointUri) { this.endpointUri = endpointUri; // dirty flag @@ -370,6 +380,7 @@ public final class DefaultBacklogTracerEventMessage implements BacklogTracerEven if (endpointUri != null) { sb.append(prefix).append(" <endpointUri>").append(endpointUri).append("</endpointUri>\n"); sb.append(prefix).append(" <remoteEndpoint>").append(remoteEndpoint).append("</remoteEndpoint>\n"); + sb.append(prefix).append(" <stubEndpoint>").append(stubEndpoint).append("</stubEndpoint>\n"); } if (toNode != null) { sb.append(prefix).append(" <toNode>").append(toNode).append("</toNode>\n"); @@ -562,6 +573,7 @@ public final class DefaultBacklogTracerEventMessage implements BacklogTracerEven if (endpointUri != null) { jo.put("endpointUri", endpointUri); jo.put("remoteEndpoint", remoteEndpoint); + jo.put("stubEndpoint", stubEndpoint); } if (routeId != null) { jo.put("routeId", routeId); diff --git a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/CamelInternalProcessor.java b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/CamelInternalProcessor.java index 530b8534c4d6..57de62d527e9 100644 --- a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/CamelInternalProcessor.java +++ b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/CamelInternalProcessor.java @@ -727,11 +727,12 @@ public class CamelInternalProcessor extends DelegateAsyncProcessor implements In String uri = null; boolean remote = true; Endpoint endpoint = null; + Endpoint consumerEndpoint = null; if ((data.isFirst() || data.isLast())) { if (route.getConsumer() != null) { - // get the actual resolved uri - uri = route.getConsumer().getEndpoint().getEndpointUri(); - remote = route.getConsumer().getEndpoint().isRemote(); + consumerEndpoint = route.getConsumer().getEndpoint(); + uri = consumerEndpoint.getEndpointUri(); + remote = consumerEndpoint.isRemote(); endpoint = route.getEndpoint(); } else { uri = routeDefinition.getEndpointUrl(); @@ -741,6 +742,11 @@ public class CamelInternalProcessor extends DelegateAsyncProcessor implements In data.setEndpointUri(uri); } data.setRemoteEndpoint(remote); + if ((uri != null && uri.startsWith("stub:")) + || (consumerEndpoint != null + && "StubEndpoint".equals(consumerEndpoint.getClass().getSimpleName()))) { + data.setStubEndpoint(true); + } if (endpoint instanceof EndpointServiceLocation esl) { data.setEndpointServiceUrl(esl.getServiceUrl()); data.setEndpointServiceProtocol(esl.getServiceProtocol()); @@ -1015,6 +1021,7 @@ public class CamelInternalProcessor extends DelegateAsyncProcessor implements In String uri = null; boolean remote = true; Endpoint endpoint = notifier.after(exchange); + Endpoint resolvedEndpoint = endpoint; if (endpoint != null) { uri = endpoint.getEndpointUri(); remote = endpoint.isRemote(); @@ -1023,9 +1030,9 @@ public class CamelInternalProcessor extends DelegateAsyncProcessor implements In // pseudo first/last event (the from in the route) Route route = camelContext.getRoute(routeDefinition.getRouteId()); if (route != null && route.getConsumer() != null) { - // get the actual resolved uri - uri = route.getConsumer().getEndpoint().getEndpointUri(); - remote = route.getConsumer().getEndpoint().isRemote(); + resolvedEndpoint = route.getConsumer().getEndpoint(); + uri = resolvedEndpoint.getEndpointUri(); + remote = resolvedEndpoint.isRemote(); } else { uri = routeDefinition.getEndpointUrl(); } @@ -1034,6 +1041,11 @@ public class CamelInternalProcessor extends DelegateAsyncProcessor implements In data.setEndpointUri(uri); } data.setRemoteEndpoint(remote); + if ((uri != null && uri.startsWith("stub:")) + || (resolvedEndpoint != null + && "StubEndpoint".equals(resolvedEndpoint.getClass().getSimpleName()))) { + data.setStubEndpoint(true); + } if (endpoint instanceof EndpointServiceLocation esl) { data.setEndpointServiceUrl(esl.getServiceUrl()); data.setEndpointServiceProtocol(esl.getServiceProtocol()); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryEntry.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryEntry.java index 33993a524481..638ac9c0d9d5 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryEntry.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryEntry.java @@ -34,6 +34,8 @@ class HistoryEntry { boolean first; boolean last; boolean failed; + boolean remoteEndpoint; + boolean stubEndpoint; int nodeLevel; long elapsed; long epochMs; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java index 9952e4b02dd8..52e1f4b69f86 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java @@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -80,6 +81,8 @@ class HistoryTab implements MonitorTab { private int traceDetailScroll; private int traceDetailHScroll; + private boolean showDescription; + private boolean showWaterfall; private int waterfallScroll; private final ScrollbarState waterfallScrollState = new ScrollbarState(); @@ -168,6 +171,11 @@ class HistoryTab implements MonitorTab { } } + if (ke.isCharIgnoreCase('n')) { + showDescription = !showDescription; + return true; + } + if (tracerActive && traceDetailView) { if (ke.isCharIgnoreCase('p')) { showTraceProperties = !showTraceProperties; @@ -398,6 +406,7 @@ class HistoryTab implements MonitorTab { if (!showWaterfall && !traceWordWrap) { hint(spans, "←→", "h-scroll"); } + hint(spans, "n", "description" + (showDescription ? " [on]" : "")); hint(spans, "g", "waterfall" + (showWaterfall ? " [on]" : "")); if (!showWaterfall) { hint(spans, "d", "diagram"); @@ -411,6 +420,7 @@ class HistoryTab implements MonitorTab { hint(spans, "Esc", "back"); hint(spans, "↑↓", "navigate"); hint(spans, "s", "sort"); + hint(spans, "n", "description" + (showDescription ? " [on]" : "")); hint(spans, "d", "diagram"); hint(spans, "Enter", "details"); hintLast(spans, "F5", "refresh"); @@ -421,6 +431,7 @@ class HistoryTab implements MonitorTab { if (!showWaterfall && !historyWordWrap) { hint(spans, "←→", "h-scroll"); } + hint(spans, "n", "description" + (showDescription ? " [on]" : "")); hint(spans, "g", "waterfall" + (showWaterfall ? " [on]" : "")); if (!showWaterfall) { hint(spans, "d", "diagram"); @@ -575,7 +586,7 @@ class HistoryTab implements MonitorTab { rows.add(Row.from( Cell.from(s.timestamp != null ? truncate(s.timestamp, 12) : ""), Cell.from(Span.styled( - s.routeId != null ? truncate(s.routeId, 15) : "", + s.routeId != null ? truncate(s.routeId, 20) : "", Style.EMPTY.fg(Color.CYAN))), Cell.from(Span.styled(s.status, statusStyle)), rightCell(s.elapsed + "ms", 10), @@ -618,16 +629,18 @@ class HistoryTab implements MonitorTab { .constraints(Constraint.length(10), Constraint.length(1), Constraint.fill()) .split(area); + Map<String, String> descMap = showDescription ? getRouteDescriptions() : Collections.emptyMap(); List<Row> rows = new ArrayList<>(); for (TraceEntry entry : steps) { + String desc = showDescription ? descMap.get(entry.routeId) : null; rows.add(buildStepRow( entry.direction, entry.first, entry.last, entry.failed, - entry.timestamp, entry.routeId, entry.nodeId, entry.processor, entry.elapsed)); + entry.timestamp, entry.routeId, entry.nodeId, entry.processor, desc, entry.elapsed)); } String stepTitle = String.format(" Trace [%s] ", truncate(traceSelectedExchangeId, 30)); frame.renderStatefulWidget( - buildStepTable(rows, stepTitle), chunks.get(0), traceStepTableState); + buildStepTable(rows, stepTitle, showDescription), chunks.get(0), traceStepTableState); if (showWaterfall) { renderWaterfall(frame, chunks.get(2), steps.stream().map(WaterfallStep::fromTrace).toList()); @@ -848,16 +861,18 @@ class HistoryTab implements MonitorTab { .constraints(Constraint.length(10), Constraint.length(1), Constraint.fill()) .split(area); + Map<String, String> descMap = showDescription ? getRouteDescriptions() : Collections.emptyMap(); List<Row> rows = new ArrayList<>(); for (HistoryEntry entry : current) { + String desc = showDescription ? descMap.get(entry.routeId) : null; rows.add(buildStepRow( entry.direction, entry.first, entry.last, entry.failed, - entry.timestamp, entry.routeId, entry.nodeId, entry.processor, entry.elapsed)); + entry.timestamp, entry.routeId, entry.nodeId, entry.processor, desc, entry.elapsed)); } Title historyTitle = buildHistoryTitle(current); frame.renderStatefulWidget( - buildStepTable(rows, historyTitle), chunks.get(0), historyTableState); + buildStepTable(rows, historyTitle, showDescription), chunks.get(0), historyTableState); if (showWaterfall) { renderWaterfall(frame, chunks.get(2), current.stream().map(WaterfallStep::fromHistory).toList()); @@ -908,6 +923,20 @@ class HistoryTab implements MonitorTab { historyDetailHScroll = hScroll[0]; } + private Map<String, String> getRouteDescriptions() { + IntegrationInfo info = ctx.findSelectedIntegration(); + if (info == null) { + return Collections.emptyMap(); + } + Map<String, String> map = new HashMap<>(); + for (RouteInfo ri : info.routes) { + if (ri.routeId != null && ri.description != null && !ri.description.isEmpty()) { + map.put(ri.routeId, ri.description); + } + } + return map; + } + // ---- Shared helpers ---- private List<String> getTraceExchangeIds() { @@ -951,32 +980,32 @@ class HistoryTab implements MonitorTab { private static Row buildStepRow( String direction, boolean first, boolean last, boolean failed, - String timestamp, String routeId, String nodeId, String processor, long elapsed) { + String timestamp, String routeId, String nodeId, String processor, + String description, long elapsed) { Style dirStyle; - if (first) { - dirStyle = Style.EMPTY.fg(Color.GREEN); - } else if (last) { + if (first || last || !direction.isBlank()) { dirStyle = failed ? Style.EMPTY.fg(Color.LIGHT_RED) : Style.EMPTY.fg(Color.GREEN); } else { dirStyle = failed ? Style.EMPTY.fg(Color.LIGHT_RED) : Style.EMPTY; } String elapsedStr = elapsed >= 0 ? elapsed + "ms" : ""; + String display = description != null ? description : (processor != null ? processor : ""); return Row.from( Cell.from(Span.styled(direction, dirStyle)), Cell.from(timestamp != null ? truncate(timestamp, 12) : ""), - Cell.from(Span.styled(routeId != null ? truncate(routeId, 15) : "", Style.EMPTY.fg(Color.CYAN))), + Cell.from(Span.styled(routeId != null ? truncate(routeId, 20) : "", Style.EMPTY.fg(Color.CYAN))), Cell.from(nodeId != null ? truncate(nodeId, 15) : ""), - Cell.from(processor != null ? processor : ""), + Cell.from(display), rightCell(elapsedStr, 10)); } - private static Table buildStepTable(List<Row> rows, Object title) { + private static Table buildStepTable(List<Row> rows, Object title, boolean descriptionMode) { Row header = Row.from( Cell.from(Span.styled("", Style.EMPTY.bold())), Cell.from(Span.styled("TIME", Style.EMPTY.bold())), Cell.from(Span.styled("ROUTE", Style.EMPTY.bold())), Cell.from(Span.styled("ID", Style.EMPTY.bold())), - Cell.from(Span.styled("PROCESSOR", Style.EMPTY.bold())), + Cell.from(Span.styled(descriptionMode ? "DESCRIPTION" : "PROCESSOR", Style.EMPTY.bold())), rightCell("ELAPSED", 10, Style.EMPTY.bold())); Block block = title instanceof Title t ? Block.builder().borderType(BorderType.ROUNDED).title(t).build() @@ -1322,18 +1351,34 @@ class HistoryTab implements MonitorTab { took: ``` - RouteId NodeId Processor Elapsed - timer-to-log timer1 from[timer:hello] 0ms - timer-to-log setBody1 setBody[simple] 0ms - timer-to-log choice1 choice 0ms - timer-to-log when1 when[simple] 0ms - timer-to-log log1 log[HIGH: ${body}] 0ms + RouteId NodeId Processor Elapsed + *-> timer-to-log timer1 from[timer:hello] 0ms + timer-to-log setBody1 setBody[simple] 0ms + timer-to-log choice1 choice 0ms + timer-to-log when1 when[simple] 0ms + ---> timer-to-log to1 to[kafka:orders] 2ms + timer-to-log log1 log[HIGH: ${body}] 0ms + <-* timer-to-log timer1 from[timer:hello] 3ms ``` This tells you the message entered via the timer, went through setBody, reached a choice node, matched the `when` condition, and was logged. The elapsed time for each step helps identify bottlenecks. + ## Direction Arrows + + The first column shows direction arrows that indicate the type + of each step: + + - `*-->` — First step of a route consuming from a **remote** endpoint (e.g., Kafka, HTTP) + - `*-> ` — First step of a route consuming from a **local** endpoint (e.g., timer, direct) + - `<--*` — Last step of a route with a **remote** consumer endpoint + - `<-* ` — Last step of a route with a **local** consumer endpoint + - `--->` — A step that sends to a **remote** endpoint (e.g., `to[kafka:orders]`) + - `~-->` — First step or send to a **stub** endpoint (running with `--stub` mode) + - `<--~` — Last step of a route with a **stub** consumer endpoint + - _(blank)_ — A regular processing step (log, setBody, choice, etc.) + **Exchange Content** — Toggle these sections to inspect the message: - `h` — **Headers**: Key-value pairs carried with the message (e.g., `Content-Type`, `CamelFileName`, custom headers) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java index 4932e49c4e74..386faca53ebb 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java @@ -553,12 +553,19 @@ final class StatusParser { entry.timestamp = tsObj.toString(); } + entry.remoteEndpoint = json.getBooleanOrDefault("remoteEndpoint", false); + entry.stubEndpoint = json.getBooleanOrDefault("stubEndpoint", false); + if (entry.first || entry.last) { entry.nodeLevel = Math.max(0, entry.nodeLevel - 1); } String indent = " ".repeat(entry.nodeLevel); if (entry.first) { - entry.direction = "-->"; + if (entry.stubEndpoint) { + entry.direction = "~-->"; + } else { + entry.direction = entry.remoteEndpoint ? "*-->" : "*-> "; + } String uri = json.getString("endpointUri"); if (uri != null) { entry.processor = indent + "from[" + uri + "]"; @@ -566,10 +573,20 @@ final class StatusParser { entry.processor = indent + (entry.nodeLabel != null ? entry.nodeLabel : ""); } } else if (entry.last) { - entry.direction = "<--"; + if (entry.stubEndpoint) { + entry.direction = "<--~"; + } else { + entry.direction = entry.remoteEndpoint ? "<--*" : "<-* "; + } entry.processor = indent + (entry.nodeLabel != null ? entry.nodeLabel : ""); } else { - entry.direction = " >"; + if (entry.stubEndpoint) { + entry.direction = "~-->"; + } else if (entry.remoteEndpoint) { + entry.direction = "--->"; + } else { + entry.direction = " "; + } entry.processor = indent + (entry.nodeLabel != null ? entry.nodeLabel : ""); } @@ -618,6 +635,9 @@ final class StatusParser { entry.failed = json.getBooleanOrDefault("failed", false); entry.nodeLevel = json.getIntegerOrDefault("nodeLevel", 0); + entry.remoteEndpoint = json.getBooleanOrDefault("remoteEndpoint", false); + entry.stubEndpoint = json.getBooleanOrDefault("stubEndpoint", false); + Object elapsedObj = json.get("elapsed"); if (elapsedObj instanceof Number n) { entry.elapsed = n.longValue(); @@ -626,11 +646,25 @@ final class StatusParser { } if (entry.first) { - entry.direction = "-->"; + if (entry.stubEndpoint) { + entry.direction = "~-->"; + } else { + entry.direction = entry.remoteEndpoint ? "*-->" : "*-> "; + } } else if (entry.last) { - entry.direction = "<--"; + if (entry.stubEndpoint) { + entry.direction = "<--~"; + } else { + entry.direction = entry.remoteEndpoint ? "<--*" : "<-* "; + } } else { - entry.direction = " >"; + if (entry.stubEndpoint) { + entry.direction = "~-->"; + } else if (entry.remoteEndpoint) { + entry.direction = "--->"; + } else { + entry.direction = " "; + } } if (entry.first || entry.last) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TraceEntry.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TraceEntry.java index bea88a7f8ee2..3c88ec2a3950 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TraceEntry.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TraceEntry.java @@ -35,6 +35,8 @@ class TraceEntry { boolean first; boolean last; boolean failed; + boolean remoteEndpoint; + boolean stubEndpoint; int nodeLevel; long elapsed; long epochMs;
