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 d0eba277e089 CAMEL-24259: camel-jbang - TUI consumer tab extract
quartz cron schedule
d0eba277e089 is described below
commit d0eba277e08979940d0d56895d2d3d509aa148f6
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Jul 26 21:00:01 2026 +0200
CAMEL-24259: camel-jbang - TUI consumer tab extract quartz cron schedule
CAMEL-24259: camel-jbang - TUI add Network Services tab
Add a Network Services tab that surfaces EndpointServiceRegistry data
showing network-facing endpoints (HTTP, Kafka, database, etc.) with
direction, protocol, hosted flag, and hit counts. Extract FlowHelper
to share flow diagram and sparkline chart rendering between
EndpointsTab and NetworkTab.
CAMEL-24259: camel-jbang - TUI payload size visibility for Network and
Endpoints tabs
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../camel/component/quartz/QuartzConsole.java | 35 ++
.../impl/engine/DefaultMessageSizeStrategy.java | 42 ++
.../engine/DefaultRuntimeEndpointRegistry.java | 35 +-
.../dsl/jbang/core/commands/tui/ConsumerInfo.java | 1 +
.../dsl/jbang/core/commands/tui/ConsumersTab.java | 4 +
.../core/commands/tui/DataRefreshService.java | 1 +
.../dsl/jbang/core/commands/tui/EndpointsTab.java | 265 ++---------
.../dsl/jbang/core/commands/tui/FlowHelper.java | 205 ++++++++
.../jbang/core/commands/tui/IntegrationInfo.java | 1 +
.../jbang/core/commands/tui/MetricsCollector.java | 117 +++++
.../dsl/jbang/core/commands/tui/NetworkTab.java | 516 +++++++++++++++++++++
.../tui/{ConsumerInfo.java => ServiceInfo.java} | 23 +-
.../dsl/jbang/core/commands/tui/StatusParser.java | 49 ++
.../dsl/jbang/core/commands/tui/TabRegistry.java | 3 +
.../dsl/jbang/core/commands/tui/TuiIcons.java | 1 +
.../core/commands/tui/ConsumersTabRenderTest.java | 21 +
.../jbang/core/commands/tui/TabRegistryTest.java | 4 +-
17 files changed, 1071 insertions(+), 252 deletions(-)
diff --git
a/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/QuartzConsole.java
b/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/QuartzConsole.java
index 04f677dca925..62079cba0ac0 100644
---
a/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/QuartzConsole.java
+++
b/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/QuartzConsole.java
@@ -20,14 +20,18 @@ import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import org.apache.camel.spi.annotations.DevConsole;
import org.apache.camel.support.console.AbstractDevConsole;
import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
+import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
+import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerMetaData;
+import org.quartz.impl.matchers.GroupMatcher;
@DevConsole(name = "quartz", description = "Quartz Scheduler")
public class QuartzConsole extends AbstractDevConsole {
@@ -192,6 +196,37 @@ public class QuartzConsole extends AbstractDevConsole {
arr.add(jo);
}
}
+
+ // all scheduled triggers (for TUI consumer schedule display)
+ Set<JobKey> jobKeys =
scheduler.getJobKeys(GroupMatcher.anyGroup());
+ if (!jobKeys.isEmpty()) {
+ JsonArray arr = new JsonArray();
+ root.put("triggers", arr);
+ for (JobKey jobKey : jobKeys) {
+ JobDetail job = scheduler.getJobDetail(jobKey);
+ if (job != null) {
+ JsonObject jo = new JsonObject();
+ String routeId = (String)
job.getJobDataMap().get("routeId");
+ if (routeId != null) {
+ jo.put("routeId", routeId);
+ }
+ String type = (String)
job.getJobDataMap().get(QuartzConstants.QUARTZ_TRIGGER_TYPE);
+ if (type != null) {
+ jo.put("triggerType", type);
+ }
+ String cron = (String)
job.getJobDataMap().get(QuartzConstants.QUARTZ_TRIGGER_CRON_EXPRESSION);
+ if (cron != null) {
+ jo.put("cron", cron);
+ }
+ String interval
+ = (String)
job.getJobDataMap().get(QuartzConstants.QUARTZ_TRIGGER_SIMPLE_REPEAT_INTERVAL);
+ if (interval != null) {
+ jo.put("repeatInterval", interval);
+ }
+ arr.add(jo);
+ }
+ }
+ }
} catch (Exception e) {
// ignore
}
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultMessageSizeStrategy.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultMessageSizeStrategy.java
index e909a29022b1..94e10db706ca 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultMessageSizeStrategy.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultMessageSizeStrategy.java
@@ -20,6 +20,7 @@ import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Collection;
import java.util.Map;
import org.apache.camel.CamelContext;
@@ -83,6 +84,12 @@ public class DefaultMessageSizeStrategy extends
ServiceSupport implements CamelC
if (body instanceof Path p) {
return Files.size(p);
}
+ if (body instanceof Collection<?> col) {
+ return estimateCollectionSize(col);
+ }
+ if (body instanceof Map<?, ?> map) {
+ return estimateMapSize(map);
+ }
// fallback to Content-Length header (e.g. HTTP components where
body is a stream)
Long cl = message.getHeader(Exchange.CONTENT_LENGTH, Long.class);
if (cl != null && cl >= 0) {
@@ -122,4 +129,39 @@ public class DefaultMessageSizeStrategy extends
ServiceSupport implements CamelC
}
return -1;
}
+
+ private long estimateCollectionSize(Collection<?> col) {
+ long total = 0;
+ for (Object item : col) {
+ if (item instanceof Map<?, ?> map) {
+ total += estimateMapSize(map);
+ } else if (item instanceof String str) {
+ total += str.getBytes(StandardCharsets.UTF_8).length;
+ } else if (item instanceof byte[] bytes) {
+ total += bytes.length;
+ } else if (item != null) {
+ total +=
item.toString().getBytes(StandardCharsets.UTF_8).length;
+ }
+ }
+ return total;
+ }
+
+ private long estimateMapSize(Map<?, ?> map) {
+ long total = 0;
+ for (Map.Entry<?, ?> entry : map.entrySet()) {
+ Object key = entry.getKey();
+ if (key != null) {
+ total +=
key.toString().getBytes(StandardCharsets.UTF_8).length;
+ }
+ Object value = entry.getValue();
+ if (value instanceof String str) {
+ total += str.getBytes(StandardCharsets.UTF_8).length;
+ } else if (value instanceof byte[] bytes) {
+ total += bytes.length;
+ } else if (value != null) {
+ total +=
value.toString().getBytes(StandardCharsets.UTF_8).length;
+ }
+ }
+ return total;
+ }
}
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRuntimeEndpointRegistry.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRuntimeEndpointRegistry.java
index 8f77ff2ed4de..0fc53cfee529 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRuntimeEndpointRegistry.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRuntimeEndpointRegistry.java
@@ -34,6 +34,7 @@ import org.apache.camel.spi.CamelEvent.ExchangeCompletedEvent;
import org.apache.camel.spi.CamelEvent.ExchangeCreatedEvent;
import org.apache.camel.spi.CamelEvent.ExchangeFailedEvent;
import org.apache.camel.spi.CamelEvent.ExchangeSendingEvent;
+import org.apache.camel.spi.CamelEvent.ExchangeSentEvent;
import org.apache.camel.spi.CamelEvent.RouteAddedEvent;
import org.apache.camel.spi.CamelEvent.RouteRemovedEvent;
import org.apache.camel.spi.EndpointUtilizationStatistics;
@@ -324,11 +325,16 @@ public class DefaultRuntimeEndpointRegistry extends
EventNotifierSupport impleme
if (key != null) {
outputUtilization.onHit(key);
if (messageSizeEnabled) {
+ // Record the request body size being sent to the
endpoint.
+ // ExchangeSentEvent will also fire after the producer
completes
+ // and record the max of request vs response body size.
Message message = exchange.getIn();
MessageSizeStrategy strategy =
getCamelContext().getMessageSizeStrategy();
long bodySize = strategy.computeBodySize(message);
long headersSize =
strategy.computeHeadersSize(message);
- outputSizeStats.onHit(key, bodySize, headersSize);
+ // Store the request body size so ExchangeSentEvent
can compare
+ exchange.setProperty("CamelMessageBodySizeSending",
bodySize);
+ exchange.setProperty("CamelMessageHeadersSizeSending",
headersSize);
// reset stream cache so the body is re-readable when
sending
if (message.getBody() instanceof StreamCache sc) {
sc.reset();
@@ -336,6 +342,32 @@ public class DefaultRuntimeEndpointRegistry extends
EventNotifierSupport impleme
}
}
}
+ } else if (extended && messageSizeEnabled && event instanceof
ExchangeSentEvent ese) {
+ // Record the max of request and response body size.
+ // For SQL SELECT the response (List<Map>) is the meaningful
payload.
+ // For SQL INSERT the request (the data being inserted) is the
meaningful payload.
+ // Taking the max ensures the most relevant size is reported in
both cases.
+ Endpoint endpoint = ese.getEndpoint();
+ Exchange exchange = ese.getExchange();
+ String routeId = ExchangeHelper.getRouteId(exchange);
+ String uri = endpoint.getEndpointUri();
+ String key = asUtilizationKey(routeId, uri);
+ if (key != null) {
+ MessageSizeStrategy strategy =
getCamelContext().getMessageSizeStrategy();
+ long responseBodySize =
strategy.computeBodySize(exchange.getMessage());
+ long responseHeadersSize =
strategy.computeHeadersSize(exchange.getMessage());
+ if (exchange.getMessage().getBody() instanceof StreamCache sc)
{
+ sc.reset();
+ }
+ Long requestBodySize =
exchange.getProperty("CamelMessageBodySizeSending", Long.class);
+ Long requestHeadersSize =
exchange.getProperty("CamelMessageHeadersSizeSending", Long.class);
+ long bodySize = Math.max(responseBodySize, requestBodySize !=
null ? requestBodySize : 0);
+ long headersSize = Math.max(responseHeadersSize,
requestHeadersSize != null ? requestHeadersSize : 0);
+ outputSizeStats.onHit(key, bodySize, headersSize);
+ // cleanup temporary properties
+ exchange.removeProperty("CamelMessageBodySizeSending");
+ exchange.removeProperty("CamelMessageHeadersSizeSending");
+ }
} else if (event instanceof ExchangeCompletedEvent || event instanceof
ExchangeFailedEvent) {
// InOut consumers send a reply back when the exchange completes;
// record this as an "out" hit on the consumer's fromEndpoint
@@ -381,6 +413,7 @@ public class DefaultRuntimeEndpointRegistry extends
EventNotifierSupport impleme
public boolean isEnabled(CamelEvent event) {
return enabled && event instanceof ExchangeCreatedEvent
|| event instanceof ExchangeSendingEvent
+ || event instanceof ExchangeSentEvent
|| event instanceof ExchangeCompletedEvent
|| event instanceof ExchangeFailedEvent
|| event instanceof RouteAddedEvent
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java
index e9e1ad6f959b..17ce5aa06c36 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java
@@ -27,6 +27,7 @@ class ConsumerInfo {
Long totalCounter;
Long delay;
Long period;
+ String schedule;
String sinceLastStarted;
String sinceLastCompleted;
String sinceLastFailed;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
index c3152dac3e23..74bd30692436 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
@@ -207,6 +207,10 @@ class ConsumersTab extends AbstractTableTab {
}
static String consumerSchedule(ConsumerInfo ci) {
+ // Prefer server-provided schedule (e.g., from quartz dev console)
+ if (ci.schedule != null) {
+ return ci.schedule;
+ }
// Try to extract cron expression from URI for cron/quartz components
String cron = extractCronFromUri(ci.uri);
if (cron != null) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
index bb635f6b27ad..efe068e5d8c5 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
@@ -264,6 +264,7 @@ class DataRefreshService {
infos.add(info);
metrics.updateThroughputHistory(info);
metrics.updateEndpointHistory(info);
+ metrics.updateServiceHistory(info);
metrics.updateCbHistory(info);
metrics.updateHeapHistory(info);
metrics.updateLoadMetrics(ph, info);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
index 089af5847cc2..e68a0b6d292b 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
@@ -23,7 +23,6 @@ import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -34,7 +33,6 @@ import dev.tamboui.markdown.MarkdownView;
import dev.tamboui.style.Style;
import dev.tamboui.terminal.Frame;
import dev.tamboui.text.CharWidth;
-import dev.tamboui.text.Line;
import dev.tamboui.text.Span;
import dev.tamboui.tui.event.KeyCode;
import dev.tamboui.tui.event.KeyEvent;
@@ -43,9 +41,6 @@ import dev.tamboui.tui.event.MouseEventKind;
import dev.tamboui.widgets.block.Block;
import dev.tamboui.widgets.block.BorderType;
import dev.tamboui.widgets.block.Borders;
-import dev.tamboui.widgets.block.Title;
-import dev.tamboui.widgets.paragraph.Paragraph;
-import dev.tamboui.widgets.sparkline.DualSparkline;
import dev.tamboui.widgets.table.Cell;
import dev.tamboui.widgets.table.Row;
import dev.tamboui.widgets.table.Table;
@@ -78,6 +73,8 @@ class EndpointsTab extends AbstractTableTab {
private final Map<String, LinkedList<Long>> endpointOutSizeHistory;
private final Map<String, LinkedList<Long>> perEndpointInHistory;
private final Map<String, LinkedList<Long>> perEndpointOutHistory;
+ private final Map<String, LinkedList<Long>> perEndpointInSizeHistory;
+ private final Map<String, LinkedList<Long>> perEndpointOutSizeHistory;
private int filter;
private int chartMode = CHART_ALL;
@@ -106,6 +103,8 @@ class EndpointsTab extends AbstractTableTab {
this.endpointOutSizeHistory = metrics.getEndpointOutSizeHistory();
this.perEndpointInHistory = metrics.getPerEndpointInHistory();
this.perEndpointOutHistory = metrics.getPerEndpointOutHistory();
+ this.perEndpointInSizeHistory = metrics.getPerEndpointInSizeHistory();
+ this.perEndpointOutSizeHistory =
metrics.getPerEndpointOutSizeHistory();
}
@Override
@@ -236,8 +235,8 @@ class EndpointsTab extends AbstractTableTab {
cells.add(Cell.from(Span.styled(arrow + dir, dirStyle)));
cells.add(rightCell(ep.hits > 0 ? String.valueOf(ep.hits) : "",
8));
if (hasSize) {
- cells.add(rightCell(sizeToString(ep.meanBodySize), 10));
- cells.add(rightCell(sizeToString(ep.meanHeadersSize), 10));
+ cells.add(rightCell(FlowHelper.sizeToString(ep.meanBodySize),
10));
+
cells.add(rightCell(FlowHelper.sizeToString(ep.meanHeadersSize), 10));
}
cells.add(centerCell(ep.stub ? "x" : "", 6));
cells.add(centerCell(ep.remote ? "x" : "", 8));
@@ -291,8 +290,9 @@ class EndpointsTab extends AbstractTableTab {
boolean hasSizeHistory;
try {
- hasSizeHistory = !endpointInSizeHistory.isEmpty()
- && endpointInSizeHistory.values().stream()
+ hasSizeHistory = endpointInSizeHistory.values().stream()
+ .anyMatch(h -> new ArrayList<>(h).stream().anyMatch(v -> v
> 0))
+ || endpointOutSizeHistory.values().stream()
.anyMatch(h -> new
ArrayList<>(h).stream().anyMatch(v -> v > 0));
} catch (java.util.ConcurrentModificationException e) {
hasSizeHistory = false;
@@ -442,57 +442,7 @@ class EndpointsTab extends AbstractTableTab {
.split(area);
hSplit.setBorderPos(hParts.get(1).x());
- int w = Math.max(10, hParts.get(0).width() - 2);
-
- String label = name != null ? name : "INTEGRATION";
- if (CharWidth.of(label) > 20) {
- label = CharWidth.truncateWithEllipsis(label, 20,
CharWidth.TruncatePosition.END);
- }
- String box = "[ " + label + " ]";
- int boxLen = CharWidth.of(box);
-
- int sideLen = Math.max(4, (w - boxLen - 2) / 2);
- String arm = "─".repeat(Math.max(1, sideLen - 1));
- String arrowStr = arm + TuiIcons.POINTER;
-
- String inStr = String.valueOf(inTotal);
- String outStr = String.valueOf(outTotal);
-
- int inPad = Math.max(0, sideLen - inStr.length());
- int centerGap = boxLen + 2;
- int outPad = Math.max(0, sideLen - outStr.length());
-
- int inLabelPad = (sideLen - 2) / 2;
- int outLabelPad = (sideLen - 3) / 2;
- String inLabelStr = " ".repeat(inLabelPad) + "in" + " ".repeat(sideLen
- inLabelPad - 2);
- String outLabelStr = " ".repeat(outLabelPad) + "out";
-
- Style inStyle = Theme.success();
- Style outStyle = Style.EMPTY.fg(Theme.accent());
- Style dimStyle = Style.EMPTY.dim();
-
- List<Line> flowLines = new ArrayList<>();
- flowLines.add(Line.from(Span.raw("")));
- flowLines.add(Line.from(Span.raw("")));
- flowLines.add(Line.from(
- Span.styled(" ".repeat(inPad) + inStr, inTotal > 0 ? inStyle :
dimStyle),
- Span.raw(" ".repeat(centerGap)),
- Span.styled(outStr + " ".repeat(outPad), outTotal > 0 ?
outStyle : dimStyle)));
- flowLines.add(Line.from(
- Span.styled(arrowStr, inStyle),
- Span.raw(" "),
- Span.styled(box, Theme.label().bold()),
- Span.raw(" "),
- Span.styled(arrowStr, outStyle)));
- flowLines.add(Line.from(
- Span.styled(inLabelStr, inStyle.dim()),
- Span.raw(" ".repeat(centerGap)),
- Span.styled(outLabelStr, outStyle.dim())));
-
- frame.renderWidget(Paragraph.builder()
- .text(dev.tamboui.text.Text.from(flowLines))
-
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title("
Flow ").build())
- .build(), hParts.get(0));
+ FlowHelper.renderFlowPanel(frame, hParts.get(0), inTotal, outTotal,
name);
Map<String, LinkedList<Long>> inHistMap = switch (filter) {
case 1 -> endpointRemoteInHistory;
@@ -507,40 +457,7 @@ class EndpointsTab extends AbstractTableTab {
LinkedList<Long> inHist = inHistMap.getOrDefault(pid, new
LinkedList<>());
LinkedList<Long> outHist = outHistMap.getOrDefault(pid, new
LinkedList<>());
- int renderPoints = Math.max(20, (Math.min(MAX_CHART_POINTS,
hParts.get(1).width() - 6) / 20) * 20);
- long[] inArr = new long[renderPoints];
- long[] outArr = new long[renderPoints];
- for (int i = 0; i < renderPoints; i++) {
- int idx = inHist.size() - renderPoints + i;
- if (idx >= 0) {
- inArr[i] = unbox(inHist.get(idx));
- }
- idx = outHist.size() - renderPoints + i;
- if (idx >= 0) {
- outArr[i] = unbox(outHist.get(idx));
- }
- }
- long curIn = inArr[renderPoints - 1];
- long curOut = outArr[renderPoints - 1];
-
- Line chartTitle = Line.from(
- Span.styled("▬", Theme.success()),
- Span.raw(String.format(" in:%-4s ",
MetricsCollector.formatThroughput(curIn))),
- Span.styled("▬", Style.EMPTY.fg(Theme.accent())),
- Span.raw(String.format(" out:%-4s msg/s ",
MetricsCollector.formatThroughput(curOut))));
-
- Rect rightArea = hParts.get(1);
- frame.renderWidget(DualSparkline.builder()
- .topData(inArr)
- .bottomData(outArr)
- .topStyle(Theme.success())
- .bottomStyle(Style.EMPTY.fg(Theme.accent()))
- .showYAxis(true)
- .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 /
4) + "s",
- "-" + (renderPoints / 2) + "s", "-" + (renderPoints /
4) + "s", "now")
-
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(Title.from(chartTitle)).build())
- .build(), rightArea);
+ FlowHelper.renderThroughputChart(frame, hParts.get(1), inHist,
outHist);
}
private void renderSingleEndpointChart(Frame frame, Rect area, String
selectedUri, IntegrationInfo info) {
@@ -559,139 +476,37 @@ class EndpointsTab extends AbstractTableTab {
.split(area);
hSplit.setBorderPos(hParts.get(1).x());
- // Flow diagram with endpoint URI as label
- int w = Math.max(10, hParts.get(0).width() - 2);
- String label = selectedUri;
- if (CharWidth.of(label) > 20) {
- label = CharWidth.truncateWithEllipsis(label, 20,
CharWidth.TruncatePosition.END);
- }
- String box = "[ " + label + " ]";
- int boxLen = CharWidth.of(box);
- int sideLen = Math.max(4, (w - boxLen - 2) / 2);
- String arm = "─".repeat(Math.max(1, sideLen - 1));
- String arrowStr = arm + TuiIcons.POINTER;
- String inStr = String.valueOf(inTotal);
- String outStr = String.valueOf(outTotal);
- int inPad = Math.max(0, sideLen - inStr.length());
- int centerGap = boxLen + 2;
- int outPad = Math.max(0, sideLen - outStr.length());
- int inLabelPad = (sideLen - 2) / 2;
- int outLabelPad = (sideLen - 3) / 2;
- String inLabelStr = " ".repeat(inLabelPad) + "in" + " ".repeat(sideLen
- inLabelPad - 2);
- String outLabelStr = " ".repeat(outLabelPad) + "out";
-
- Style inStyle = Theme.success();
- Style outStyle = Style.EMPTY.fg(Theme.accent());
- Style dimStyle = Style.EMPTY.dim();
-
- List<Line> flowLines = new ArrayList<>();
- flowLines.add(Line.from(Span.raw("")));
- flowLines.add(Line.from(Span.raw("")));
- flowLines.add(Line.from(
- Span.styled(" ".repeat(inPad) + inStr, inTotal > 0 ? inStyle :
dimStyle),
- Span.raw(" ".repeat(centerGap)),
- Span.styled(outStr + " ".repeat(outPad), outTotal > 0 ?
outStyle : dimStyle)));
- flowLines.add(Line.from(
- Span.styled(arrowStr, inStyle),
- Span.raw(" "),
- Span.styled(box, Theme.label().bold()),
- Span.raw(" "),
- Span.styled(arrowStr, outStyle)));
- flowLines.add(Line.from(
- Span.styled(inLabelStr, inStyle.dim()),
- Span.raw(" ".repeat(centerGap)),
- Span.styled(outLabelStr, outStyle.dim())));
-
- frame.renderWidget(Paragraph.builder()
- .text(dev.tamboui.text.Text.from(flowLines))
-
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title("
Flow ").build())
- .build(), hParts.get(0));
-
- // Per-endpoint sparkline
+ FlowHelper.renderFlowPanel(frame, hParts.get(0), inTotal, outTotal,
selectedUri);
+
String key = info.pid + "|" + selectedUri;
LinkedList<Long> inHist = perEndpointInHistory.getOrDefault(key, new
LinkedList<>());
LinkedList<Long> outHist = perEndpointOutHistory.getOrDefault(key, new
LinkedList<>());
- int renderPoints = Math.max(20, (Math.min(MAX_CHART_POINTS,
hParts.get(1).width() - 6) / 20) * 20);
- long[] inArr = new long[renderPoints];
- long[] outArr = new long[renderPoints];
- for (int i = 0; i < renderPoints; i++) {
- int idx = inHist.size() - renderPoints + i;
- if (idx >= 0) {
- inArr[i] = unbox(inHist.get(idx));
- }
- idx = outHist.size() - renderPoints + i;
- if (idx >= 0) {
- outArr[i] = unbox(outHist.get(idx));
- }
+ LinkedList<Long> inSizeHist =
perEndpointInSizeHistory.getOrDefault(key, new LinkedList<>());
+ LinkedList<Long> outSizeHist =
perEndpointOutSizeHistory.getOrDefault(key, new LinkedList<>());
+ boolean hasSizeData;
+ try {
+ hasSizeData = new ArrayList<>(inSizeHist).stream().anyMatch(v -> v
> 0)
+ || new ArrayList<>(outSizeHist).stream().anyMatch(v -> v >
0);
+ } catch (java.util.ConcurrentModificationException e) {
+ hasSizeData = false;
}
- long curIn = inArr[renderPoints - 1];
- long curOut = outArr[renderPoints - 1];
- String uriLabel = selectedUri;
- if (CharWidth.of(uriLabel) > 30) {
- uriLabel = CharWidth.truncateWithEllipsis(uriLabel, 30,
CharWidth.TruncatePosition.END);
+ if (hasSizeData) {
+ List<Rect> chartSplit = Layout.horizontal()
+ .constraints(Constraint.percentage(50),
Constraint.percentage(50))
+ .split(hParts.get(1));
+ FlowHelper.renderThroughputChart(frame, chartSplit.get(0), inHist,
outHist, selectedUri);
+ FlowHelper.renderPayloadSizeChart(frame, chartSplit.get(1),
inSizeHist, outSizeHist);
+ } else {
+ FlowHelper.renderThroughputChart(frame, hParts.get(1), inHist,
outHist, selectedUri);
}
-
- Line chartTitle = Line.from(
- Span.raw(" ["),
- Span.styled(uriLabel, Theme.label().bold()),
- Span.raw("] "),
- Span.styled("▬", Theme.success()),
- Span.raw(String.format(" in:%-4s ",
MetricsCollector.formatThroughput(curIn))),
- Span.styled("▬", Style.EMPTY.fg(Theme.accent())),
- Span.raw(String.format(" out:%-4s msg/s ",
MetricsCollector.formatThroughput(curOut))));
-
- frame.renderWidget(DualSparkline.builder()
- .topData(inArr)
- .bottomData(outArr)
- .topStyle(Theme.success())
- .bottomStyle(Style.EMPTY.fg(Theme.accent()))
- .showYAxis(true)
- .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 /
4) + "s",
- "-" + (renderPoints / 2) + "s", "-" + (renderPoints /
4) + "s", "now")
-
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(Title.from(chartTitle)).build())
- .build(), hParts.get(1));
}
private void renderPayloadSizeChart(Frame frame, Rect area, String pid) {
LinkedList<Long> inHist = endpointInSizeHistory.getOrDefault(pid, new
LinkedList<>());
LinkedList<Long> outHist = endpointOutSizeHistory.getOrDefault(pid,
new LinkedList<>());
-
- int renderPoints = Math.max(20, (Math.min(MAX_CHART_POINTS,
area.width() - 6) / 20) * 20);
- long[] inArr = new long[renderPoints];
- long[] outArr = new long[renderPoints];
- for (int i = 0; i < renderPoints; i++) {
- int idx = inHist.size() - renderPoints + i;
- if (idx >= 0) {
- inArr[i] = unbox(inHist.get(idx));
- }
- idx = outHist.size() - renderPoints + i;
- if (idx >= 0) {
- outArr[i] = unbox(outHist.get(idx));
- }
- }
- long curIn = inArr[renderPoints - 1];
- long curOut = outArr[renderPoints - 1];
-
- Line chartTitle = Line.from(
- Span.styled("▬", Theme.label()),
- Span.raw(String.format(" in:%-8s ", sizeToString(curIn))),
- Span.styled("▬", Theme.notice()),
- Span.raw(String.format(" out:%-8s avg body ",
sizeToString(curOut))));
-
- frame.renderWidget(DualSparkline.builder()
- .topData(inArr)
- .bottomData(outArr)
- .topStyle(Theme.label())
- .bottomStyle(Theme.notice())
- .showYAxis(true)
- .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 /
4) + "s",
- "-" + (renderPoints / 2) + "s", "-" + (renderPoints /
4) + "s", "now")
-
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(Title.from(chartTitle)).build())
- .build(), area);
+ FlowHelper.renderPayloadSizeChart(frame, area, inHist, outHist);
}
private void renderDetail(Frame frame, Rect area, List<EndpointInfo>
sortedEndpoints, IntegrationInfo info) {
@@ -848,26 +663,6 @@ class EndpointsTab extends AbstractTableTab {
}
}
- private static long unbox(Long value) {
- return value != null ? value : 0L;
- }
-
- static String sizeToString(long size) {
- if (size < 0) {
- return "-";
- }
- if (size == 0) {
- return "0 B";
- }
- if (size < 1024) {
- return size + " B";
- } else if (size < 1024 * 1024) {
- return String.format(Locale.US, "%.1f KB", size / 1024.0);
- } else {
- return String.format(Locale.US, "%.1f MB", size / (1024.0 *
1024.0));
- }
- }
-
@Override
public SelectionContext getSelectionContext() {
IntegrationInfo info = ctx.findSelectedIntegration();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FlowHelper.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FlowHelper.java
new file mode 100644
index 000000000000..3246c16ae52c
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FlowHelper.java
@@ -0,0 +1,205 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.CharWidth;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.sparkline.DualSparkline;
+
+final class FlowHelper {
+
+ static final int MAX_CHART_POINTS = 300;
+
+ private FlowHelper() {
+ }
+
+ static void renderFlowPanel(Frame frame, Rect area, long inTotal, long
outTotal, String label) {
+ int w = Math.max(10, area.width() - 2);
+
+ String name = label != null ? label : "INTEGRATION";
+ if (CharWidth.of(name) > 20) {
+ name = CharWidth.truncateWithEllipsis(name, 20,
CharWidth.TruncatePosition.END);
+ }
+ String box = "[ " + name + " ]";
+ int boxLen = CharWidth.of(box);
+
+ int sideLen = Math.max(4, (w - boxLen - 2) / 2);
+ String arm = "─".repeat(Math.max(1, sideLen - 1));
+ String arrowStr = arm + TuiIcons.POINTER;
+
+ String inStr = String.valueOf(inTotal);
+ String outStr = String.valueOf(outTotal);
+
+ int inPad = Math.max(0, sideLen - inStr.length());
+ int centerGap = boxLen + 2;
+ int outPad = Math.max(0, sideLen - outStr.length());
+
+ int inLabelPad = (sideLen - 2) / 2;
+ int outLabelPad = (sideLen - 3) / 2;
+ String inLabelStr = " ".repeat(inLabelPad) + "in" + " ".repeat(sideLen
- inLabelPad - 2);
+ String outLabelStr = " ".repeat(outLabelPad) + "out";
+
+ Style inStyle = Theme.success();
+ Style outStyle = Style.EMPTY.fg(Theme.accent());
+ Style dimStyle = Style.EMPTY.dim();
+
+ List<Line> flowLines = new ArrayList<>();
+ flowLines.add(Line.from(Span.raw("")));
+ flowLines.add(Line.from(Span.raw("")));
+ flowLines.add(Line.from(
+ Span.styled(" ".repeat(inPad) + inStr, inTotal > 0 ? inStyle :
dimStyle),
+ Span.raw(" ".repeat(centerGap)),
+ Span.styled(outStr + " ".repeat(outPad), outTotal > 0 ?
outStyle : dimStyle)));
+ flowLines.add(Line.from(
+ Span.styled(arrowStr, inStyle),
+ Span.raw(" "),
+ Span.styled(box, Theme.label().bold()),
+ Span.raw(" "),
+ Span.styled(arrowStr, outStyle)));
+ flowLines.add(Line.from(
+ Span.styled(inLabelStr, inStyle.dim()),
+ Span.raw(" ".repeat(centerGap)),
+ Span.styled(outLabelStr, outStyle.dim())));
+
+ frame.renderWidget(Paragraph.builder()
+ .text(dev.tamboui.text.Text.from(flowLines))
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title("
Flow ").build())
+ .build(), area);
+ }
+
+ static void renderThroughputChart(
+ Frame frame, Rect area, LinkedList<Long> inHist, LinkedList<Long>
outHist) {
+ renderThroughputChart(frame, area, inHist, outHist, null);
+ }
+
+ static void renderThroughputChart(
+ Frame frame, Rect area, LinkedList<Long> inHist, LinkedList<Long>
outHist, String chartLabel) {
+ int renderPoints = Math.max(20, (Math.min(MAX_CHART_POINTS,
area.width() - 6) / 20) * 20);
+ long[] inArr = new long[renderPoints];
+ long[] outArr = new long[renderPoints];
+ for (int i = 0; i < renderPoints; i++) {
+ int idx = inHist.size() - renderPoints + i;
+ if (idx >= 0) {
+ inArr[i] = unbox(inHist.get(idx));
+ }
+ idx = outHist.size() - renderPoints + i;
+ if (idx >= 0) {
+ outArr[i] = unbox(outHist.get(idx));
+ }
+ }
+ long curIn = inArr[renderPoints - 1];
+ long curOut = outArr[renderPoints - 1];
+
+ List<Span> titleSpans = new ArrayList<>();
+ if (chartLabel != null) {
+ String label = chartLabel;
+ if (CharWidth.of(label) > 30) {
+ label = CharWidth.truncateWithEllipsis(label, 30,
CharWidth.TruncatePosition.END);
+ }
+ titleSpans.add(Span.raw(" ["));
+ titleSpans.add(Span.styled(label, Theme.label().bold()));
+ titleSpans.add(Span.raw("] "));
+ }
+ titleSpans.add(Span.styled("▬", Theme.success()));
+ titleSpans.add(Span.raw(String.format(" in:%-4s ",
MetricsCollector.formatThroughput(curIn))));
+ titleSpans.add(Span.styled("▬", Style.EMPTY.fg(Theme.accent())));
+ titleSpans.add(Span.raw(String.format(" out:%-4s msg/s ",
MetricsCollector.formatThroughput(curOut))));
+
+ frame.renderWidget(DualSparkline.builder()
+ .topData(inArr)
+ .bottomData(outArr)
+ .topStyle(Theme.success())
+ .bottomStyle(Style.EMPTY.fg(Theme.accent()))
+ .showYAxis(true)
+ .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 /
4) + "s",
+ "-" + (renderPoints / 2) + "s", "-" + (renderPoints /
4) + "s", "now")
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+
.title(Title.from(Line.from(titleSpans.toArray(Span[]::new)))).build())
+ .build(), area);
+ }
+
+ static void renderPayloadSizeChart(
+ Frame frame, Rect area, LinkedList<Long> inHist, LinkedList<Long>
outHist) {
+ int renderPoints = Math.max(20, (Math.min(MAX_CHART_POINTS,
area.width() - 6) / 20) * 20);
+ long[] inArr = new long[renderPoints];
+ long[] outArr = new long[renderPoints];
+ for (int i = 0; i < renderPoints; i++) {
+ int idx = inHist.size() - renderPoints + i;
+ if (idx >= 0) {
+ inArr[i] = unbox(inHist.get(idx));
+ }
+ idx = outHist.size() - renderPoints + i;
+ if (idx >= 0) {
+ outArr[i] = unbox(outHist.get(idx));
+ }
+ }
+ long curIn = inArr[renderPoints - 1];
+ long curOut = outArr[renderPoints - 1];
+
+ Line chartTitle = Line.from(
+ Span.styled("▬", Theme.label()),
+ Span.raw(String.format(" in:%-8s ", sizeToString(curIn))),
+ Span.styled("▬", Theme.notice()),
+ Span.raw(String.format(" out:%-8s avg body ",
sizeToString(curOut))));
+
+ frame.renderWidget(DualSparkline.builder()
+ .topData(inArr)
+ .bottomData(outArr)
+ .topStyle(Theme.label())
+ .bottomStyle(Theme.notice())
+ .showYAxis(true)
+ .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 /
4) + "s",
+ "-" + (renderPoints / 2) + "s", "-" + (renderPoints /
4) + "s", "now")
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(Title.from(chartTitle)).build())
+ .build(), area);
+ }
+
+ static String sizeToString(long size) {
+ if (size < 0) {
+ return "-";
+ }
+ if (size == 0) {
+ return "0 B";
+ }
+ if (size < 1024) {
+ return size + " B";
+ } else if (size < 1024 * 1024) {
+ return String.format(Locale.US, "%.1f KB", size / 1024.0);
+ } else {
+ return String.format(Locale.US, "%.1f MB", size / (1024.0 *
1024.0));
+ }
+ }
+
+ private static long unbox(Long value) {
+ return value != null ? value : 0L;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
index 88a2b654a942..6f3b6945be2e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
@@ -89,6 +89,7 @@ class IntegrationInfo {
final List<RouteControllerInfo> routeControllerRoutes = new ArrayList<>();
final List<HealthCheckInfo> healthChecks = new ArrayList<>();
final List<EndpointInfo> endpoints = new ArrayList<>();
+ final List<ServiceInfo> services = new ArrayList<>();
final List<CircuitBreakerInfo> circuitBreakers = new ArrayList<>();
final List<KafkaConsumerInfo> kafkaConsumers = new ArrayList<>();
int errorCount;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
index bd7c71d8496c..63767d61489d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
@@ -72,6 +72,22 @@ class MetricsCollector {
private final Map<String, LinkedList<long[]>> perEndpointSamples = new
ConcurrentHashMap<>();
private final Map<String, Long> previousPerEndpointTime = new
ConcurrentHashMap<>();
+ // Per-endpoint payload size history — keyed by pid + "|" + uri
+ private final Map<String, LinkedList<Long>> perEndpointInSizeHistory = new
ConcurrentHashMap<>();
+ private final Map<String, LinkedList<Long>> perEndpointOutSizeHistory =
new ConcurrentHashMap<>();
+ private final Map<String, Long> previousPerEndpointSizeTime = new
ConcurrentHashMap<>();
+
+ // Service in/out sliding window history per PID (network services)
+ private final Map<String, LinkedList<Long>> serviceInHistory = new
ConcurrentHashMap<>();
+ private final Map<String, LinkedList<Long>> serviceOutHistory = new
ConcurrentHashMap<>();
+ private final Map<String, LinkedList<long[]>> serviceSamples = new
ConcurrentHashMap<>();
+ private final Map<String, Long> previousServiceTime = new
ConcurrentHashMap<>();
+
+ // Service payload size (mean body size) history per PID
+ private final Map<String, LinkedList<Long>> serviceInSizeHistory = new
ConcurrentHashMap<>();
+ private final Map<String, LinkedList<Long>> serviceOutSizeHistory = new
ConcurrentHashMap<>();
+ private final Map<String, Long> previousServiceSizeTime = new
ConcurrentHashMap<>();
+
// Circuit breaker throughput history per PID/cbId
private final Map<String, LinkedList<Long>> cbSuccessHistory = new
ConcurrentHashMap<>();
private final Map<String, LinkedList<Long>> cbFailHistory = new
ConcurrentHashMap<>();
@@ -140,6 +156,30 @@ class MetricsCollector {
return perEndpointOutHistory;
}
+ Map<String, LinkedList<Long>> getPerEndpointInSizeHistory() {
+ return perEndpointInSizeHistory;
+ }
+
+ Map<String, LinkedList<Long>> getPerEndpointOutSizeHistory() {
+ return perEndpointOutSizeHistory;
+ }
+
+ Map<String, LinkedList<Long>> getServiceInHistory() {
+ return serviceInHistory;
+ }
+
+ Map<String, LinkedList<Long>> getServiceOutHistory() {
+ return serviceOutHistory;
+ }
+
+ Map<String, LinkedList<Long>> getServiceInSizeHistory() {
+ return serviceInSizeHistory;
+ }
+
+ Map<String, LinkedList<Long>> getServiceOutSizeHistory() {
+ return serviceOutSizeHistory;
+ }
+
Map<String, LinkedList<Long>> getCbSuccessHistory() {
return cbSuccessHistory;
}
@@ -260,6 +300,63 @@ class MetricsCollector {
perEndpointSamples, previousPerEndpointTime,
perEndpointInHistory, perEndpointOutHistory);
}
+
+ // Per-endpoint payload size history (keyed by pid|uri)
+ for (EndpointInfo ep : info.endpoints) {
+ if (ep.uri == null || ep.meanBodySize < 0) {
+ continue;
+ }
+ String key = pid + "|" + ep.uri;
+ Long lastTime = previousPerEndpointSizeTime.get(key);
+ if (lastTime == null || now - lastTime >= 1000) {
+ previousPerEndpointSizeTime.put(key, now);
+ if ("in".equals(ep.direction)) {
+ addToHistory(perEndpointInSizeHistory, key,
ep.meanBodySize, MAX_ENDPOINT_CHART_POINTS);
+ } else if ("out".equals(ep.direction)) {
+ addToHistory(perEndpointOutSizeHistory, key,
ep.meanBodySize, MAX_ENDPOINT_CHART_POINTS);
+ }
+ }
+ }
+ }
+
+ void updateServiceHistory(IntegrationInfo info) {
+ long inTotal = info.services.stream()
+ .filter(s -> "in".equals(s.direction))
+ .mapToLong(s -> s.hits).sum();
+ long outTotal = info.services.stream()
+ .filter(s -> "out".equals(s.direction))
+ .mapToLong(s -> s.hits).sum();
+
+ long now = System.currentTimeMillis();
+ recordEndpointSample(info.pid, now, inTotal, outTotal,
+ serviceSamples, previousServiceTime, serviceInHistory,
serviceOutHistory);
+
+ // Correlate services with endpoint statistics to get payload size
data.
+ // Services reference endpointUri which matches the endpoint's uri
field.
+ Map<String, EndpointInfo> epByUri = new LinkedHashMap<>();
+ for (EndpointInfo ep : info.endpoints) {
+ if (ep.uri != null) {
+ epByUri.put(ep.uri, ep);
+ }
+ }
+ long inMeanSize = 0;
+ long outMeanSize = 0;
+ for (ServiceInfo si : info.services) {
+ EndpointInfo ep = si.endpointUri != null ?
epByUri.get(si.endpointUri) : null;
+ if (ep != null && ep.meanBodySize >= 0) {
+ if ("in".equals(si.direction)) {
+ inMeanSize = Math.max(inMeanSize, ep.meanBodySize);
+ } else if ("out".equals(si.direction)) {
+ outMeanSize = Math.max(outMeanSize, ep.meanBodySize);
+ }
+ }
+ }
+ Long lastSizeTime = previousServiceSizeTime.get(info.pid);
+ if (lastSizeTime == null || now - lastSizeTime >= 1000) {
+ previousServiceSizeTime.put(info.pid, now);
+ addToHistory(serviceInSizeHistory, info.pid, inMeanSize,
MAX_ENDPOINT_CHART_POINTS);
+ addToHistory(serviceOutSizeHistory, info.pid, outMeanSize,
MAX_ENDPOINT_CHART_POINTS);
+ }
}
private void recordEndpointSample(
@@ -368,6 +465,16 @@ class MetricsCollector {
removeByPrefix(pid + "|", perEndpointInHistory, perEndpointOutHistory,
perEndpointSamples, previousPerEndpointTime);
+ removeByPrefix(pid + "|", perEndpointInSizeHistory,
perEndpointOutSizeHistory,
+ previousPerEndpointSizeTime);
+
+ serviceInHistory.remove(pid);
+ serviceOutHistory.remove(pid);
+ serviceSamples.remove(pid);
+ previousServiceTime.remove(pid);
+ serviceInSizeHistory.remove(pid);
+ serviceOutSizeHistory.remove(pid);
+ previousServiceSizeTime.remove(pid);
heapMemHistory.remove(pid);
previousHeapTime.remove(pid);
@@ -396,6 +503,14 @@ class MetricsCollector {
previousEndpointSizeTime.remove(pid);
previousEndpointRemoteStubTime.remove(pid);
+ serviceInHistory.remove(pid);
+ serviceOutHistory.remove(pid);
+ serviceSamples.remove(pid);
+ previousServiceTime.remove(pid);
+ serviceInSizeHistory.remove(pid);
+ serviceOutSizeHistory.remove(pid);
+ previousServiceSizeTime.remove(pid);
+
heapMemHistory.remove(pid);
previousHeapTime.remove(pid);
cpuLoadAvg.remove(pid);
@@ -405,6 +520,8 @@ class MetricsCollector {
cbThroughputSamples, previousCbTime);
removeByPrefix(pid + "|", perEndpointInHistory, perEndpointOutHistory,
perEndpointSamples, previousPerEndpointTime);
+ removeByPrefix(pid + "|", perEndpointInSizeHistory,
perEndpointOutSizeHistory,
+ previousPerEndpointSizeTime);
}
/**
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/NetworkTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/NetworkTab.java
new file mode 100644
index 000000000000..6b925a9b2e2a
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/NetworkTab.java
@@ -0,0 +1,516 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.MouseEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
+
+class NetworkTab extends AbstractTableTab {
+
+ private static final int CHART_ALL = 0;
+ private static final int CHART_SINGLE = 1;
+ private static final int CHART_OFF = 2;
+
+ private final Map<String, LinkedList<Long>> serviceInHistory;
+ private final Map<String, LinkedList<Long>> serviceOutHistory;
+ private final Map<String, LinkedList<Long>> serviceInSizeHistory;
+ private final Map<String, LinkedList<Long>> serviceOutSizeHistory;
+ private final Map<String, LinkedList<Long>> perEndpointInHistory;
+ private final Map<String, LinkedList<Long>> perEndpointOutHistory;
+ private final Map<String, LinkedList<Long>> perEndpointInSizeHistory;
+ private final Map<String, LinkedList<Long>> perEndpointOutSizeHistory;
+
+ private int chartMode = CHART_ALL;
+ private int chartPanelHeight = 16;
+ private final DragSplit vSplit = new DragSplit();
+ private int flowPanelWidth = 38;
+ private final DragSplit hSplit = new DragSplit();
+
+ NetworkTab(MonitorContext ctx, MetricsCollector metrics) {
+ super(ctx, "component", "route", "dir", "protocol", "hits", "body",
"hdr", "uri");
+ sortIndex = 1;
+ sort = "route";
+ this.serviceInHistory = metrics.getServiceInHistory();
+ this.serviceOutHistory = metrics.getServiceOutHistory();
+ this.serviceInSizeHistory = metrics.getServiceInSizeHistory();
+ this.serviceOutSizeHistory = metrics.getServiceOutSizeHistory();
+ this.perEndpointInHistory = metrics.getPerEndpointInHistory();
+ this.perEndpointOutHistory = metrics.getPerEndpointOutHistory();
+ this.perEndpointInSizeHistory = metrics.getPerEndpointInSizeHistory();
+ this.perEndpointOutSizeHistory =
metrics.getPerEndpointOutSizeHistory();
+ }
+
+ @Override
+ protected int getRowCount() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ return info != null ? info.services.size() : 0;
+ }
+
+ @Override
+ protected boolean handleTabKeyEvent(KeyEvent ke) {
+ if (ke.isCharIgnoreCase('a')) {
+ chartMode = (chartMode + 1) % 3;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean handleMouseEvent(MouseEvent me, Rect area) {
+ if (vSplit.handleMouse(me, me.y())) {
+ if (vSplit.isDragging()) {
+ chartPanelHeight = Math.max(5, Math.min(area.y() +
area.height() - me.y(), area.height() - 5));
+ }
+ return true;
+ }
+ if (chartMode != CHART_OFF && hSplit.handleMouse(me, me.x())) {
+ if (hSplit.isDragging()) {
+ flowPanelWidth = Math.max(20, Math.min(me.x() - area.x(),
area.width() - 20));
+ }
+ return true;
+ }
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info != null) {
+ if (handleTableClick(me, lastTableArea, tableState,
info.services.size())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void navigateUp() {
+ tableState.selectPrevious();
+ }
+
+ @Override
+ public void navigateDown() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info != null) {
+ tableState.selectNext(info.services.size());
+ }
+ }
+
+ @Override
+ public boolean handleKeyEvent(KeyEvent ke) {
+ if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)
+ || ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)
+ || ke.isHome() || ke.isEnd()) {
+ return false;
+ }
+ return super.handleKeyEvent(ke);
+ }
+
+ @Override
+ protected void renderContent(Frame frame, Rect area, IntegrationInfo info)
{
+ List<ServiceInfo> sorted = new ArrayList<>(info.services);
+ sorted.sort(this::sortService);
+
+ // Build endpoint lookup by URI to get payload size data
+ Map<String, EndpointInfo> epByUri = new java.util.LinkedHashMap<>();
+ for (EndpointInfo ep : info.endpoints) {
+ if (ep.uri != null) {
+ epByUri.put(ep.uri, ep);
+ }
+ }
+
+ boolean hasSize = info.services.stream().anyMatch(si -> {
+ EndpointInfo ep = si.endpointUri != null ?
epByUri.get(si.endpointUri) : null;
+ return ep != null && (ep.meanBodySize >= 0 || ep.meanHeadersSize
>= 0);
+ });
+
+ List<Row> rows = new ArrayList<>();
+ for (ServiceInfo si : sorted) {
+ String dir = si.direction != null ? si.direction : "";
+ Style dirStyle = switch (dir) {
+ case "in" -> Theme.success();
+ case "out" -> Style.EMPTY.fg(Theme.accent());
+ default -> Theme.label();
+ };
+ String arrow = switch (dir) {
+ case "in" -> TuiIcons.KEY_RIGHT + " ";
+ case "out" -> TuiIcons.KEY_LEFT + " ";
+ default -> TuiIcons.ARROW_BOTH + " ";
+ };
+
+ List<Cell> cells = new ArrayList<>();
+ cells.add(Cell.from(Span.styled(si.component != null ?
si.component : "", Style.EMPTY.fg(Theme.accent()))));
+ cells.add(Cell.from(si.routeId != null ? si.routeId : ""));
+ cells.add(Cell.from(Span.styled(arrow + dir, dirStyle)));
+ cells.add(Cell.from(si.protocol != null ? si.protocol : ""));
+ cells.add(centerCell(si.hosted ? "x" : "", 8));
+ cells.add(rightCell(si.hits > 0 ? String.valueOf(si.hits) : "",
8));
+ if (hasSize) {
+ EndpointInfo ep = si.endpointUri != null ?
epByUri.get(si.endpointUri) : null;
+ long bodySize = ep != null ? ep.meanBodySize : -1;
+ long hdrSize = ep != null ? ep.meanHeadersSize : -1;
+ cells.add(rightCell(FlowHelper.sizeToString(bodySize), 10));
+ cells.add(rightCell(FlowHelper.sizeToString(hdrSize), 10));
+ }
+ cells.add(Cell.from(si.serviceUrl != null ? si.serviceUrl : ""));
+ rows.add(Row.from(cells));
+ }
+
+ int emptyCols = hasSize ? 9 : 7;
+ if (rows.isEmpty()) {
+ rows.add(emptyRow("No network services", emptyCols));
+ }
+
+ List<Cell> headerCells = new ArrayList<>();
+ headerCells.add(Cell.from(Span.styled(sortLabel("COMPONENT",
"component"), sortStyle("component"))));
+ headerCells.add(Cell.from(Span.styled(sortLabel("ROUTE", "route"),
sortStyle("route"))));
+ headerCells.add(Cell.from(Span.styled(sortLabel("DIR", "dir"),
sortStyle("dir"))));
+ headerCells.add(Cell.from(Span.styled(sortLabel("PROTOCOL",
"protocol"), sortStyle("protocol"))));
+ headerCells.add(centerCell("HOSTED", 8, Style.EMPTY.bold()));
+ headerCells.add(rightCell(sortLabel("HITS", "hits"), 8,
sortStyle("hits")));
+ if (hasSize) {
+ headerCells.add(rightCell(sortLabel("BODY", "body"), 10,
sortStyle("body")));
+ headerCells.add(rightCell(sortLabel("HDR", "hdr"), 10,
sortStyle("hdr")));
+ }
+ headerCells.add(Cell.from(Span.styled(sortLabel("SERVICE URL", "uri"),
sortStyle("uri"))));
+
+ List<Constraint> widths = new ArrayList<>();
+ widths.add(Constraint.length(18));
+ widths.add(Constraint.length(20));
+ widths.add(Constraint.length(8));
+ widths.add(Constraint.length(10));
+ widths.add(Constraint.length(8));
+ widths.add(Constraint.length(8));
+ if (hasSize) {
+ widths.add(Constraint.length(10));
+ widths.add(Constraint.length(10));
+ }
+ widths.add(Constraint.fill());
+
+ Table table = Table.builder()
+ .rows(rows)
+ .header(Row.from(headerCells))
+ .widths(widths.toArray(Constraint[]::new))
+ .highlightStyle(Theme.selectionBg())
+ .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" Network Services ").build())
+ .build();
+
+ boolean showChart = chartMode != CHART_OFF && ctx.shellPercent < 50;
+ List<Rect> chunks;
+ if (showChart) {
+ chartPanelHeight = Math.max(5, Math.min(chartPanelHeight,
area.height() - 5));
+ chunks = Layout.vertical().constraints(Constraint.fill(),
Constraint.length(chartPanelHeight)).split(area);
+ vSplit.setBorderPos(chunks.get(1).y());
+ } else {
+ chunks = List.of(area);
+ vSplit.clearBorderPos();
+ hSplit.clearBorderPos();
+ }
+
+ lastTableArea = chunks.get(0);
+ frame.renderStatefulWidget(table, chunks.get(0), tableState);
+ renderScrollbar(frame, sorted.size());
+
+ if (showChart) {
+ String selectedUri = null;
+ if (chartMode == CHART_SINGLE) {
+ Integer sel = tableState.selected();
+ if (sel != null && sel >= 0 && sel < sorted.size()) {
+ selectedUri = sorted.get(sel).endpointUri;
+ }
+ }
+
+ if (chartMode == CHART_SINGLE && selectedUri != null) {
+ renderSingleServiceChart(frame, chunks.get(1), selectedUri,
info);
+ } else {
+ long inTotal = info.services.stream()
+ .filter(s -> "in".equals(s.direction))
+ .mapToLong(s -> s.hits).sum();
+ long outTotal = info.services.stream()
+ .filter(s -> "out".equals(s.direction))
+ .mapToLong(s -> s.hits).sum();
+
+ boolean hasSizeHistory;
+ try {
+ hasSizeHistory = serviceInSizeHistory.values().stream()
+ .anyMatch(h -> new
ArrayList<>(h).stream().anyMatch(v -> v > 0))
+ || serviceOutSizeHistory.values().stream()
+ .anyMatch(h -> new
ArrayList<>(h).stream().anyMatch(v -> v > 0));
+ } catch (java.util.ConcurrentModificationException e) {
+ hasSizeHistory = false;
+ }
+
+ flowPanelWidth = Math.max(20, Math.min(flowPanelWidth,
chunks.get(1).width() - 20));
+ List<Rect> hParts = Layout.horizontal()
+ .constraints(Constraint.length(flowPanelWidth),
Constraint.fill())
+ .split(chunks.get(1));
+ hSplit.setBorderPos(hParts.get(1).x());
+
+ FlowHelper.renderFlowPanel(frame, hParts.get(0), inTotal,
outTotal, info.name);
+
+ LinkedList<Long> inHist =
serviceInHistory.getOrDefault(info.pid, new LinkedList<>());
+ LinkedList<Long> outHist =
serviceOutHistory.getOrDefault(info.pid, new LinkedList<>());
+
+ if (hasSizeHistory) {
+ List<Rect> chartSplit = Layout.horizontal()
+ .constraints(Constraint.percentage(50),
Constraint.percentage(50))
+ .split(hParts.get(1));
+ FlowHelper.renderThroughputChart(frame, chartSplit.get(0),
inHist, outHist);
+
+ LinkedList<Long> inSizeHist =
serviceInSizeHistory.getOrDefault(info.pid, new LinkedList<>());
+ LinkedList<Long> outSizeHist =
serviceOutSizeHistory.getOrDefault(info.pid, new LinkedList<>());
+ FlowHelper.renderPayloadSizeChart(frame,
chartSplit.get(1), inSizeHist, outSizeHist);
+ } else {
+ FlowHelper.renderThroughputChart(frame, hParts.get(1),
inHist, outHist);
+ }
+ }
+ }
+ }
+
+ private void renderSingleServiceChart(Frame frame, Rect area, String
endpointUri, IntegrationInfo info) {
+ long inTotal = info.services.stream()
+ .filter(s -> "in".equals(s.direction) &&
endpointUri.equals(s.endpointUri))
+ .mapToLong(s -> s.hits).sum();
+ long outTotal = info.services.stream()
+ .filter(s -> "out".equals(s.direction) &&
endpointUri.equals(s.endpointUri))
+ .mapToLong(s -> s.hits).sum();
+
+ flowPanelWidth = Math.max(20, Math.min(flowPanelWidth, area.width() -
20));
+ List<Rect> hParts = Layout.horizontal()
+ .constraints(Constraint.length(flowPanelWidth),
Constraint.fill())
+ .split(area);
+ hSplit.setBorderPos(hParts.get(1).x());
+
+ FlowHelper.renderFlowPanel(frame, hParts.get(0), inTotal, outTotal,
endpointUri);
+
+ String key = info.pid + "|" + endpointUri;
+ LinkedList<Long> inHist = perEndpointInHistory.getOrDefault(key, new
LinkedList<>());
+ LinkedList<Long> outHist = perEndpointOutHistory.getOrDefault(key, new
LinkedList<>());
+
+ LinkedList<Long> inSizeHist =
perEndpointInSizeHistory.getOrDefault(key, new LinkedList<>());
+ LinkedList<Long> outSizeHist =
perEndpointOutSizeHistory.getOrDefault(key, new LinkedList<>());
+ boolean hasSizeData;
+ try {
+ hasSizeData = new ArrayList<>(inSizeHist).stream().anyMatch(v -> v
> 0)
+ || new ArrayList<>(outSizeHist).stream().anyMatch(v -> v >
0);
+ } catch (java.util.ConcurrentModificationException e) {
+ hasSizeData = false;
+ }
+
+ if (hasSizeData) {
+ List<Rect> chartSplit = Layout.horizontal()
+ .constraints(Constraint.percentage(50),
Constraint.percentage(50))
+ .split(hParts.get(1));
+ FlowHelper.renderThroughputChart(frame, chartSplit.get(0), inHist,
outHist, endpointUri);
+ FlowHelper.renderPayloadSizeChart(frame, chartSplit.get(1),
inSizeHist, outSizeHist);
+ } else {
+ FlowHelper.renderThroughputChart(frame, hParts.get(1), inHist,
outHist, endpointUri);
+ }
+ }
+
+ private long lookupBodySize(ServiceInfo si) {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null || si.endpointUri == null) {
+ return -1;
+ }
+ for (EndpointInfo ep : info.endpoints) {
+ if (si.endpointUri.equals(ep.uri)) {
+ return ep.meanBodySize;
+ }
+ }
+ return -1;
+ }
+
+ private long lookupHeadersSize(ServiceInfo si) {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null || si.endpointUri == null) {
+ return -1;
+ }
+ for (EndpointInfo ep : info.endpoints) {
+ if (si.endpointUri.equals(ep.uri)) {
+ return ep.meanHeadersSize;
+ }
+ }
+ return -1;
+ }
+
+ private int sortService(ServiceInfo a, ServiceInfo b) {
+ int result = switch (sort) {
+ case "component" -> {
+ String ca = a.component != null ? a.component : "";
+ String cb = b.component != null ? b.component : "";
+ yield ca.compareToIgnoreCase(cb);
+ }
+ case "dir" -> {
+ String da = a.direction != null ? a.direction : "";
+ String db = b.direction != null ? b.direction : "";
+ yield da.compareToIgnoreCase(db);
+ }
+ case "protocol" -> {
+ String pa = a.protocol != null ? a.protocol : "";
+ String pb = b.protocol != null ? b.protocol : "";
+ yield pa.compareToIgnoreCase(pb);
+ }
+ case "hits" -> Long.compare(b.hits, a.hits);
+ case "body" -> Long.compare(lookupBodySize(b), lookupBodySize(a));
+ case "hdr" -> Long.compare(lookupHeadersSize(b),
lookupHeadersSize(a));
+ case "uri" -> {
+ String ua = a.serviceUrl != null ? a.serviceUrl : "";
+ String ub = b.serviceUrl != null ? b.serviceUrl : "";
+ yield ua.compareToIgnoreCase(ub);
+ }
+ default -> { // "route"
+ String ra = a.routeId != null ? a.routeId : "";
+ String rb = b.routeId != null ? b.routeId : "";
+ yield ra.compareToIgnoreCase(rb);
+ }
+ };
+ return sortReversed ? -result : result;
+ }
+
+ @Override
+ public void renderFooter(List<Span> spans) {
+ hint(spans, "Esc", "back");
+ hint(spans, TuiIcons.HINT_SCROLL, "navigate");
+ hint(spans, "s", "sort");
+ String chartLabel = switch (chartMode) {
+ case CHART_ALL -> "[all]";
+ case CHART_SINGLE -> "[single]";
+ default -> "[off]";
+ };
+ hint(spans, "a", "chart " + chartLabel);
+ }
+
+ @Override
+ public SelectionContext getSelectionContext() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null || info.services.isEmpty()) {
+ return null;
+ }
+ List<ServiceInfo> sorted = new ArrayList<>(info.services);
+ sorted.sort(this::sortService);
+ List<String> items = sorted.stream()
+ .map(s -> s.serviceUrl != null ? s.serviceUrl : "")
+ .toList();
+ Integer sel = tableState.selected();
+ return new SelectionContext("table", items, sel != null ? sel : -1,
items.size(), "Network Services");
+ }
+
+ @Override
+ public String description() {
+ return "Network-facing services (HTTP listeners, Kafka connections,
database links)";
+ }
+
+ @Override
+ public String getHelpText() {
+ return """
+ # Network Services
+
+ Network Services shows all **network-facing endpoints** — HTTP
listeners,
+ Kafka connections, database links, messaging brokers — with
direction, protocol,
+ and hit counts. Unlike the Endpoints tab which includes
internal plumbing
+ (`direct:`, `seda:`, `log:`), this tab focuses on real network
traffic.
+
+ ## Table Columns
+
+ - **COMPONENT** — The Camel component (e.g., `platform-http`,
`kafka`, `sql`)
+ - **ROUTE** — The route this service belongs to
+ - **DIR** — Direction: `in` (consuming/listening) or `out`
(producing/calling)
+ - **PROTOCOL** — Network protocol: `http`, `https`, `tcp`,
`amqp`, etc.
+ - **HOSTED** — Whether this is a locally hosted service (e.g.,
HTTP server) vs a remote client connection
+ - **HITS** — Total number of messages processed through this
service endpoint
+ - **BODY** — Average message body size (shown when payload
sizing is active)
+ - **HDR** — Average message headers size (shown when payload
sizing is active)
+ - **SERVICE URL** — The network address or connection URL
+
+ ## Flow Diagram
+
+ The bottom panel shows the same in/out flow diagram and
throughput sparkline
+ as the Endpoints tab, but scoped to network services only.
This gives a
+ clearer picture of actual external traffic without internal
routing noise.
+
+ ## Keys
+
+ - `Up/Down` — select service
+ - `s` — cycle sort column
+ - `S` — reverse sort order
+ - `a` — toggle chart on/off
+ """;
+ }
+
+ @Override
+ public JsonObject getTableDataAsJson() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null) {
+ return null;
+ }
+ JsonObject result = new JsonObject();
+ result.put("tab", "Network Services");
+ Map<String, EndpointInfo> epByUri = new java.util.LinkedHashMap<>();
+ for (EndpointInfo ep : info.endpoints) {
+ if (ep.uri != null) {
+ epByUri.put(ep.uri, ep);
+ }
+ }
+ JsonArray rowsArr = new JsonArray();
+ List<ServiceInfo> sorted = new ArrayList<>(info.services);
+ sorted.sort(this::sortService);
+ for (ServiceInfo si : sorted) {
+ JsonObject row = new JsonObject();
+ row.put("component", si.component);
+ row.put("routeId", si.routeId);
+ row.put("direction", si.direction);
+ row.put("protocol", si.protocol);
+ row.put("hosted", si.hosted);
+ row.put("hits", si.hits);
+ EndpointInfo ep = si.endpointUri != null ?
epByUri.get(si.endpointUri) : null;
+ if (ep != null && ep.meanBodySize >= 0) {
+ row.put("meanBodySize", ep.meanBodySize);
+ }
+ if (ep != null && ep.meanHeadersSize >= 0) {
+ row.put("meanHeadersSize", ep.meanHeadersSize);
+ }
+ row.put("serviceUrl", si.serviceUrl);
+ row.put("endpointUri", si.endpointUri);
+ rowsArr.add(row);
+ }
+ result.put("rows", rowsArr);
+ result.put("totalRows", info.services.size());
+ Integer sel = tableState.selected();
+ result.put("selectedIndex", sel != null ? sel : -1);
+ return result;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ServiceInfo.java
similarity index 74%
copy from
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java
copy to
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ServiceInfo.java
index e9e1ad6f959b..06034e7d67a6 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ServiceInfo.java
@@ -16,18 +16,13 @@
*/
package org.apache.camel.dsl.jbang.core.commands.tui;
-class ConsumerInfo {
- String id;
- String uri;
- String state;
- String className;
- boolean scheduled;
- int inflight;
- Boolean polling;
- Long totalCounter;
- Long delay;
- Long period;
- String sinceLastStarted;
- String sinceLastCompleted;
- String sinceLastFailed;
+class ServiceInfo {
+ String component;
+ String direction;
+ boolean hosted;
+ String protocol;
+ String serviceUrl;
+ String endpointUri;
+ String routeId;
+ long hits;
}
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 db04860676c5..0f6ab05993cc 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
@@ -326,6 +326,34 @@ final class StatusParser {
}
}
+ // Enrich consumers with quartz trigger schedule data
+ JsonObject quartzObj = (JsonObject) root.get("quartz");
+ if (quartzObj != null) {
+ JsonArray triggers = (JsonArray) quartzObj.get("triggers");
+ if (triggers != null) {
+ for (Object t : triggers) {
+ JsonObject tj = (JsonObject) t;
+ String routeId = tj.getString("routeId");
+ if (routeId != null) {
+ for (ConsumerInfo ci : info.consumers) {
+ if (routeId.equals(ci.id)) {
+ String cron = tj.getString("cron");
+ if (cron != null) {
+ ci.schedule = cron;
+ } else {
+ String interval =
tj.getString("repeatInterval");
+ if (interval != null) {
+ ci.schedule = interval;
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
// Parse producers
JsonObject producersObj = (JsonObject) root.get("producers");
if (producersObj != null) {
@@ -409,6 +437,27 @@ final class StatusParser {
}
}
+ // Parse services (network endpoints from service dev console)
+ JsonObject serviceObj = (JsonObject) root.get("services");
+ if (serviceObj != null) {
+ JsonArray serviceList = (JsonArray) serviceObj.get("services");
+ if (serviceList != null) {
+ for (Object s : serviceList) {
+ JsonObject sj = (JsonObject) s;
+ ServiceInfo si = new ServiceInfo();
+ si.component = sj.getString("component");
+ si.direction = sj.getString("direction");
+ si.hosted = Boolean.TRUE.equals(sj.get("hosted"));
+ si.protocol = sj.getString("protocol");
+ si.serviceUrl = sj.getString("serviceUrl");
+ si.endpointUri = sj.getString("endpointUri");
+ si.routeId = sj.getString("routeId");
+ si.hits = TuiHelper.objToLong(sj.get("hits"));
+ info.services.add(si);
+ }
+ }
+ }
+
// Parse circuit breakers: resilience4j, fault-tolerance, core
parseCbSection(root, "resilience4j", info);
parseCbSection(root, "fault-tolerance", info);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index 5f52269fb948..b88ec9ba9179 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -74,6 +74,7 @@ class TabRegistry {
private EventTab eventTab;
private RouteControllerTab routeControllerTab;
private EndpointsTab endpointsTab;
+ private NetworkTab networkTab;
private HttpTab httpTab;
private SourceTab sourceTab;
private HealthTab healthTab;
@@ -129,6 +130,7 @@ class TabRegistry {
sqlQueryTab = new SqlQueryTab(ctx);
sqlTraceTab = new SqlTraceTab(ctx);
endpointsTab = new EndpointsTab(ctx, dataService.metrics());
+ networkTab = new NetworkTab(ctx, dataService.metrics());
httpTab = new HttpTab(ctx);
sourceTab = new SourceTab(ctx);
healthTab = new HealthTab(ctx);
@@ -176,6 +178,7 @@ class TabRegistry {
// Observability
new MoreTab(TuiIcons.TAB_HEALTH, "Health", "H&ealth",
healthTab, "Observability"),
new MoreTab(TuiIcons.TAB_METRICS, "Metrics", "Metr&ics",
metricsTab, "Observability"),
+ new MoreTab(TuiIcons.TAB_NETWORK, "Network Services",
"&Network Services", networkTab, "Observability"),
new MoreTab(TuiIcons.TAB_EVENTS, "Events", "E&xchange Events",
eventTab, "Observability"),
new MoreTab(TuiIcons.TAB_SPANS, "Spans", "&OTel Spans",
spansTab, "Observability"),
// Data
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
index 3e96996b5b6c..32d95332f8e4 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
@@ -140,6 +140,7 @@ final class TuiIcons {
static final String TAB_MAVEN_DEPENDENCIES = "📜";
static final String TAB_MEMORY = MEMORY;
static final String TAB_MEMORY_LEAK = "💧";
+ static final String TAB_NETWORK = "🌐";
static final String TAB_METRICS = "📈";
static final String TAB_SQL_QUERY = KEY;
static final String TAB_SQL_TRACE = "🔎";
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java
index 38940bac9973..c5bfc716773a 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java
@@ -206,6 +206,27 @@ class ConsumersTabRenderTest {
assertTrue(footer.contains("sort"), "Footer should contain sort hint");
}
+ @Test
+ void serverProvidedSchedulePreferred() {
+ ConsumerInfo ci = addConsumer("quartz-route",
"quartz://myGroup/myTimer?cron=0+0/5+*+*+*+?", "Started",
+ "org.apache.camel.component.quartz.QuartzConsumer");
+ ci.schedule = "0 0/1 * * * ?";
+
+ String schedule = ConsumersTab.consumerSchedule(ci);
+ assertTrue(schedule.equals("0 0/1 * * * ?"),
+ "Server-provided schedule should take precedence over URI
parsing");
+ }
+
+ @Test
+ void quartzCronExtractedFromUri() {
+ ConsumerInfo ci = addConsumer("quartz-route",
"quartz://myGroup/myTimer?cron=0+0/5+*+*+*+?", "Started",
+ "org.apache.camel.component.quartz.QuartzConsumer");
+
+ String schedule = ConsumersTab.consumerSchedule(ci);
+ assertTrue(schedule.equals("0 0/5 * * * ?"),
+ "Cron should be extracted and decoded from quartz URI");
+ }
+
// ---- Helper methods ----
private ConsumerInfo addConsumer(String id, String uri, String state,
String className) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
index 6b384a904836..8d6c332b8f4e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
@@ -82,7 +82,7 @@ class TabRegistryTest {
@Test
void moreTabsHasTwentySevenEntries() {
- assertEquals(27, registry.moreTabs().size());
+ assertEquals(28, registry.moreTabs().size());
}
@Test
@@ -108,7 +108,7 @@ class TabRegistryTest {
// MORE_SHORTCUTS array carried before the MoreTab refactor. A label
edit that repoints a key must fail here.
List<Character> shortcuts =
registry.moreTabs().stream().map(TabRegistry.MoreTab::shortcut).toList();
assertEquals(
- List.of('W', 'C', 'N', 'H', 'F', 'U', 'Z', 'E', 'I', 'X', 'O',
'J', 'K', 'Q', 'R', 'A', 'H', 'M', 'Y',
+ List.of('W', 'C', 'N', 'H', 'F', 'U', 'Z', 'E', 'I', 'N', 'X',
'O', 'J', 'K', 'Q', 'R', 'A', 'H', 'M', 'Y',
'P', 'S', 'T', 'B', 'L', 'G', 'V', 'D'),
shortcuts, "More tab shortcut letters must match the
historical sequence");
}