You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/05/27 17:58:34 UTC
(camel) branch CAMEL-23623 created (now 9dba5593a0c3)
This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a change to branch CAMEL-23623
in repository https://gitbox.apache.org/repos/asf/camel.git
at 9dba5593a0c3 CAMEL-23623: Reorder TUI tabs for better workflow
This branch includes the following new commits:
new 403a85dd02f8 CAMEL-23623: Add Errors tab to Camel TUI
new c9263e16e170 CAMEL-23623: Enhance Errors tab in Camel TUI
new 9dba5593a0c3 CAMEL-23623: Reorder TUI tabs for better workflow
The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(camel) 03/03: CAMEL-23623: Reorder TUI tabs for better workflow
Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch CAMEL-23623
in repository https://gitbox.apache.org/repos/asf/camel.git
commit 9dba5593a0c3258f0c7daa9d6ca08e7523c1f14e
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed May 27 19:58:04 2026 +0200
CAMEL-23623: Reorder TUI tabs for better workflow
Move Errors tab next to Inspect (History), and Consumers to last.
New order: Overview, Log, Routes, Endpoints, HTTP, Health,
Inspect, Errors, Circuit Breaker, Consumers.
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 40 +++++++++++-----------
1 file changed, 20 insertions(+), 20 deletions(-)
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 9850a845f6d2..b5388943a95b 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -110,13 +110,13 @@ public class CamelMonitor extends CamelCommand {
private static final int TAB_OVERVIEW = 0;
private static final int TAB_LOG = 1;
private static final int TAB_ROUTES = 2;
- private static final int TAB_CONSUMERS = 3;
- private static final int TAB_ENDPOINTS = 4;
- private static final int TAB_HTTP = 5;
- private static final int TAB_HEALTH = 6;
- private static final int TAB_HISTORY = 7;
+ private static final int TAB_ENDPOINTS = 3;
+ private static final int TAB_HTTP = 4;
+ private static final int TAB_HEALTH = 5;
+ private static final int TAB_HISTORY = 6;
+ private static final int TAB_ERRORS = 7;
private static final int TAB_CIRCUIT_BREAKER = 8;
- private static final int TAB_ERRORS = 9;
+ private static final int TAB_CONSUMERS = 9;
// Overview sort columns
private static final String[] OVERVIEW_SORT_COLUMNS = { "pid", "name", "version", "status", "total", "fail" };
@@ -445,25 +445,25 @@ public class CamelMonitor extends CamelCommand {
return handleTabKey(TAB_ROUTES);
}
if (ke.isChar('4')) {
- return handleTabKey(TAB_CONSUMERS);
+ return handleTabKey(TAB_ENDPOINTS);
}
if (ke.isChar('5')) {
- return handleTabKey(TAB_ENDPOINTS);
+ return handleTabKey(TAB_HTTP);
}
if (ke.isChar('6')) {
- return handleTabKey(TAB_HTTP);
+ return handleTabKey(TAB_HEALTH);
}
if (ke.isChar('7')) {
- return handleTabKey(TAB_HEALTH);
+ return handleTabKey(TAB_HISTORY);
}
if (ke.isChar('8')) {
- return handleTabKey(TAB_HISTORY);
+ return handleTabKey(TAB_ERRORS);
}
if (ke.isChar('9')) {
return handleTabKey(TAB_CIRCUIT_BREAKER);
}
if (ke.isChar('0')) {
- return handleTabKey(TAB_ERRORS);
+ return handleTabKey(TAB_CONSUMERS);
}
}
@@ -976,13 +976,13 @@ public class CamelMonitor extends CamelCommand {
Line.from(" 1 Overview "),
Line.from(" 2 Log "),
Line.from(routesTab.isTopMode() ? " 3 Top " : " 3 Route "),
- Line.from(" 4 Consumer "),
- Line.from(" 5 Endpoint "),
- Line.from(" 6 HTTP "),
- Line.from(" 7 Health "),
- Line.from(" 8 Inspect "),
+ Line.from(" 4 Endpoint "),
+ Line.from(" 5 HTTP "),
+ Line.from(" 6 Health "),
+ Line.from(" 7 Inspect "),
+ Line.from(" 8 Errors "),
Line.from(" 9 Circuit Breaker "),
- Line.from(" 0 Errors "),
+ Line.from(" 0 Consumer "),
};
Tabs tabs = Tabs.builder()
@@ -3345,8 +3345,8 @@ public class CamelMonitor extends CamelCommand {
// ---- MCP accessor methods ----
private static final String[] TAB_NAMES = {
- "Overview", "Log", "Routes", "Consumers", "Endpoints",
- "HTTP", "Health", "Inspect", "Circuit Breaker", "Errors"
+ "Overview", "Log", "Routes", "Endpoints",
+ "HTTP", "Health", "Inspect", "Errors", "Circuit Breaker", "Consumers"
};
Buffer getLastBuffer() {
(camel) 02/03: CAMEL-23623: Enhance Errors tab in Camel TUI
Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch CAMEL-23623
in repository https://gitbox.apache.org/repos/asf/camel.git
commit c9263e16e170ca40630ff6f0c3f898942df5e233
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed May 27 19:55:54 2026 +0200
CAMEL-23623: Enhance Errors tab in Camel TUI
- Separate error data into dedicated {pid}-error.json file to keep status file lightweight
- Status file only contains error metadata (count); full data loaded on demand when Errors tab is active
- Add master/detail layout with sortable table (ID, AGO, ROUTE, NODE, HANDLED, EXCEPTION, MESSAGE)
- Detail pane shows exception with unescaped stack trace, message history, properties, variables, headers, body
- Add toggles: f=handled filter, p=properties, v=variables, h=headers, b=body, w=word-wrap, s=sort
- Add Home/End keys for jumping to top/bottom of detail pane
- Fix word-wrap content height underestimation in renderDetailPanel that prevented scrolling to bottom
- Update camel get error CLI to read from separate error file
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../camel/cli/connector/LocalCliConnector.java | 30 +++-
.../dsl/jbang/core/commands/CamelCommand.java | 4 +
.../dsl/jbang/core/commands/process/ListError.java | 6 +-
.../core/commands/process/ProcessBaseCommand.java | 13 ++
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 155 ++++++++++++++-------
.../dsl/jbang/core/commands/tui/ErrorsTab.java | 136 ++++++++++++++----
.../dsl/jbang/core/commands/tui/HistoryTab.java | 3 +
.../jbang/core/commands/tui/IntegrationInfo.java | 1 +
8 files changed, 266 insertions(+), 82 deletions(-)
diff --git a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index a4ca0c6ccaf9..f3514777cebe 100644
--- a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++ b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -125,6 +125,7 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
private long traceFilePos; // keep track of trace offset
private File messageHistoryFile;
private File debugFile;
+ private File errorFile;
private File receiveFile;
private long receiveFilePos; // keep track of receive offset
private byte[] lastSource;
@@ -196,6 +197,7 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
outputFile = createLockFile(lockFile.getName() + "-output.json");
traceFile = createLockFile(lockFile.getName() + "-trace.json");
messageHistoryFile = createLockFile(lockFile.getName() + "-history.json");
+ errorFile = createLockFile(lockFile.getName() + "-error.json");
debugFile = createLockFile(lockFile.getName() + "-debug.json");
receiveFile = createLockFile(lockFile.getName() + "-receive.json");
scheduledFuture = executor.scheduleWithFixedDelay(this::task, 0, delay, TimeUnit.MILLISECONDS);
@@ -1389,7 +1391,13 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
JsonObject json = (JsonObject) dc26.call(DevConsole.MediaType.JSON,
Map.of("stackTrace", "true"));
if (json != null && !json.isEmpty()) {
- root.put("errors", json);
+ // only include metadata in status file (full error data is in the error file)
+ JsonObject summary = new JsonObject();
+ summary.put("enabled", json.get("enabled"));
+ summary.put("size", json.get("size"));
+ summary.put("maximumEntries", json.get("maximumEntries"));
+ summary.put("timeToLive", json.get("timeToLive"));
+ root.put("errors", summary);
}
}
}
@@ -1481,6 +1489,23 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
LOG.trace("Error updating message-history file: {} due to: {}. This exception is ignored.",
messageHistoryFile, e.getMessage(), e);
}
+ try {
+ DevConsole dc13c = camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
+ .resolveById("errors");
+ if (dc13c != null) {
+ JsonObject json = (JsonObject) dc13c.call(DevConsole.MediaType.JSON,
+ Map.of("stackTrace", "true"));
+ if (json != null && !json.isEmpty()) {
+ LOG.trace("Updating error file: {}", errorFile);
+ String data = json.toJson() + System.lineSeparator();
+ IOHelper.writeText(data, errorFile);
+ }
+ }
+ } catch (Exception e) {
+ // ignore
+ LOG.trace("Error updating error file: {} due to: {}. This exception is ignored.",
+ errorFile, e.getMessage(), e);
+ }
try {
DevConsole dc14 = camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
.resolveById("receive");
@@ -1643,6 +1668,9 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
if (messageHistoryFile != null) {
FileUtil.deleteFile(messageHistoryFile);
}
+ if (errorFile != null) {
+ FileUtil.deleteFile(errorFile);
+ }
if (debugFile != null) {
FileUtil.deleteFile(debugFile);
}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java
index fdf0c4b63574..4d675773de1d 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java
@@ -119,6 +119,10 @@ public abstract class CamelCommand implements Callable<Integer> {
return CommandLineHelper.getCamelDir().resolve(pid + "-trace.json");
}
+ public Path getErrorFile(String pid) {
+ return CommandLineHelper.getCamelDir().resolve(pid + "-error.json");
+ }
+
public Path getReceiveFile(String pid) {
return CommandLineHelper.getCamelDir().resolve(pid + "-receive.json");
}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListError.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListError.java
index 881eb3115ffa..b1ae97222215 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListError.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListError.java
@@ -131,9 +131,9 @@ public class ListError extends ProcessWatchCommand {
}
String pid = Long.toString(ph.pid());
- JsonObject errors = (JsonObject) root.get("errors");
- if (errors != null) {
- JsonArray arr = (JsonArray) errors.get("errors");
+ JsonObject errorRoot = loadErrorFile(ph.pid());
+ if (errorRoot != null) {
+ JsonArray arr = (JsonArray) errorRoot.get("errors");
if (arr != null) {
for (Object o : arr) {
JsonObject jo = (JsonObject) o;
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessBaseCommand.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessBaseCommand.java
index 92d8db2301e9..b2176c9bf49e 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessBaseCommand.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessBaseCommand.java
@@ -105,6 +105,19 @@ abstract class ProcessBaseCommand extends CamelCommand {
return null;
}
+ JsonObject loadErrorFile(long pid) {
+ try {
+ Path f = getErrorFile(Long.toString(pid));
+ if (f != null && Files.exists(f)) {
+ String text = Files.readString(f);
+ return (JsonObject) Jsoner.deserialize(text);
+ }
+ } catch (Exception e) {
+ // ignore
+ }
+ return null;
+ }
+
String sourceLocLine(String location) {
while (StringHelper.countChar(location, ':') > 1) {
location = location.substring(location.indexOf(':') + 1);
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 31201caef382..9850a845f6d2 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -736,6 +736,15 @@ public class CamelMonitor extends CamelCommand {
if (tab == TAB_CIRCUIT_BREAKER) {
circuitBreakerTab.onTabSelected();
}
+ if (tab == TAB_ERRORS && ctx.selectedPid != null) {
+ try {
+ long pid = Long.parseLong(ctx.selectedPid);
+ refreshErrorData(List.of(pid));
+ } catch (NumberFormatException e) {
+ // ignore
+ }
+ errorsTab.onTabSelected();
+ }
tabsState.select(tab);
return true;
}
@@ -1035,7 +1044,7 @@ public class CamelMonitor extends CamelCommand {
} else if (cbCount > 0) {
badgeTexts[TAB_CIRCUIT_BREAKER] = "(" + cbCount + ")";
}
- int errorCount = hasSelection ? sel.errors.size() : 0;
+ int errorCount = hasSelection ? sel.errorCount : 0;
if (errorCount > 0) {
badgeTexts[TAB_ERRORS] = "(" + errorCount + ")";
badgeStyles[TAB_ERRORS] = red;
@@ -2012,6 +2021,11 @@ public class CamelMonitor extends CamelCommand {
}
}
+ // Refresh error data only when the Errors tab is visible
+ if (tabsState.selected() == TAB_ERRORS) {
+ refreshErrorData(pids);
+ }
+
// Refresh trace data only when the Inspect tab is visible
if (tabsState.selected() == TAB_HISTORY) {
refreshTraceData(pids);
@@ -3009,58 +3023,10 @@ public class CamelMonitor extends CamelCommand {
}
}
- // Parse errors from error registry
+ // Parse error count from error registry (full error data is loaded on demand by ErrorsTab)
JsonObject errorsObj = (JsonObject) root.get("errors");
if (errorsObj != null) {
- JsonArray errorList = (JsonArray) errorsObj.get("errors");
- if (errorList != null) {
- for (Object e : errorList) {
- JsonObject ej = (JsonObject) e;
- ErrorInfo ei = new ErrorInfo();
- ei.routeId = ej.getString("routeId");
- ei.nodeId = ej.getString("nodeId");
- ei.exchangeId = ej.getString("exchangeId");
- ei.handled = ej.getBooleanOrDefault("handled", false);
- Long ts = ej.getLong("timestamp");
- if (ts != null) {
- ei.timestamp = ts;
- }
- ei.location = ej.getString("location");
- ei.threadName = ej.getString("threadName");
- Long elapsed = ej.getLong("elapsed");
- if (elapsed != null) {
- ei.elapsed = elapsed;
- }
- ei.endpointUri = ej.getString("endpointUri");
- ei.fromEndpointUri = ej.getString("fromEndpointUri");
- // exception
- JsonObject ex = (JsonObject) ej.get("exception");
- if (ex != null) {
- ei.exceptionType = ex.getString("type");
- ei.exceptionMessage = ex.getString("message");
- ei.stackTrace = ex.getString("stackTrace");
- }
- // message history
- Object mhObj = ej.get("messageHistory");
- if (mhObj instanceof JsonArray mhArr) {
- ei.messageHistory = new String[mhArr.size()];
- for (int i = 0; i < mhArr.size(); i++) {
- ei.messageHistory[i] = mhArr.get(i).toString();
- }
- }
- // message (body, headers)
- JsonObject msg = (JsonObject) ej.get("message");
- if (msg != null) {
- ei.body = msg.getString("body");
- ei.bodyType = msg.getString("bodyType");
- parseKvArray(msg.getCollection("headers"), ei.headers, ei.headerTypes);
- }
- // exchange properties and variables
- parseKvArray(ej.getCollection("exchangeProperties"), ei.properties, ei.propertyTypes);
- parseKvArray(ej.getCollection("exchangeVariables"), ei.variables, ei.variableTypes);
- info.errors.add(ei);
- }
- }
+ info.errorCount = errorsObj.getIntegerOrDefault("size", 0);
}
// Parse REST DSL services
@@ -3192,6 +3158,93 @@ public class CamelMonitor extends CamelCommand {
}
}
+ private JsonObject loadErrorFile(long pid) {
+ return TuiHelper.loadStatus(pid, this::getErrorFile);
+ }
+
+ private void refreshErrorData(List<Long> pids) {
+ IntegrationInfo sel = findSelectedIntegration();
+ if (sel == null) {
+ return;
+ }
+ try {
+ long pid = Long.parseLong(sel.pid);
+ JsonObject root = loadErrorFile(pid);
+ if (root == null) {
+ return;
+ }
+ JsonArray errorList = (JsonArray) root.get("errors");
+ if (errorList == null) {
+ return;
+ }
+ List<ErrorInfo> parsed = new ArrayList<>();
+ for (Object e : errorList) {
+ JsonObject ej = (JsonObject) e;
+ ErrorInfo ei = new ErrorInfo();
+ ei.routeId = ej.getString("routeId");
+ ei.nodeId = ej.getString("nodeId");
+ ei.exchangeId = ej.getString("exchangeId");
+ ei.handled = Boolean.TRUE.equals(ej.get("handled"));
+ Long ts = ej.getLong("timestamp");
+ if (ts != null) {
+ ei.timestamp = ts;
+ }
+ ei.location = ej.getString("location");
+ ei.threadName = ej.getString("threadName");
+ Long elapsed = ej.getLong("elapsed");
+ if (elapsed != null) {
+ ei.elapsed = elapsed;
+ }
+ ei.endpointUri = ej.getString("endpointUri");
+ ei.fromEndpointUri = ej.getString("fromEndpointUri");
+ // exception
+ JsonObject ex = (JsonObject) ej.get("exception");
+ if (ex != null) {
+ ei.exceptionType = ex.getString("type");
+ ei.exceptionMessage = ex.getString("message");
+ ei.stackTrace = ex.getString("stackTrace");
+ }
+ // message history
+ Object mhObj = ej.get("messageHistory");
+ if (mhObj instanceof JsonArray mhArr) {
+ ei.messageHistory = new String[mhArr.size()];
+ for (int i = 0; i < mhArr.size(); i++) {
+ ei.messageHistory[i] = mhArr.get(i).toString();
+ }
+ }
+ // message (body, headers)
+ JsonObject msg = (JsonObject) ej.get("message");
+ if (msg != null) {
+ Object bodyObj = msg.get("body");
+ if (bodyObj instanceof JsonObject bodyJson) {
+ ei.body = bodyJson.getString("value");
+ ei.bodyType = bodyJson.getString("type");
+ } else if (bodyObj != null) {
+ ei.body = bodyObj.toString();
+ }
+ JsonArray hdrs = msg.getCollection("headers");
+ if (hdrs != null) {
+ parseKvArray(hdrs, ei.headers, ei.headerTypes);
+ }
+ }
+ // exchange properties and variables
+ JsonArray props = ej.getCollection("exchangeProperties");
+ if (props != null) {
+ parseKvArray(props, ei.properties, ei.propertyTypes);
+ }
+ JsonArray vars = ej.getCollection("exchangeVariables");
+ if (vars != null) {
+ parseKvArray(vars, ei.variables, ei.variableTypes);
+ }
+ parsed.add(ei);
+ }
+ sel.errors.clear();
+ sel.errors.addAll(parsed);
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+
// ---- Helpers ----
private IntegrationInfo findSelectedIntegration() {
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
index c74e3c7ceb39..9f5d9663aff6 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
@@ -35,22 +35,30 @@ import dev.tamboui.widgets.table.Cell;
import dev.tamboui.widgets.table.Row;
import dev.tamboui.widgets.table.Table;
import dev.tamboui.widgets.table.TableState;
+import org.apache.camel.util.json.Jsoner;
import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
class ErrorsTab implements MonitorTab {
- private static final String[] SORT_COLUMNS = { "age", "route", "exception" };
+ private static final String[] SORT_COLUMNS = { "id", "age", "route", "node", "exception" };
private final MonitorContext ctx;
private final TableState tableState = new TableState();
private final ScrollbarState detailScrollState = new ScrollbarState();
- private String sort = "age";
+ private String sort = "id";
private int sortIndex;
private boolean sortReversed;
+ private static final String[] HANDLED_FILTER = { "all", "true", "false" };
+ private int handledIndex;
+ private String handledFilter = "all";
private int detailScroll;
private int detailHScroll;
private boolean wordWrap = true;
+ private boolean showProperties;
+ private boolean showVariables;
+ private boolean showHeaders = true;
+ private boolean showBody = true;
ErrorsTab(MonitorContext ctx) {
this.ctx = ctx;
@@ -80,6 +88,35 @@ class ErrorsTab implements MonitorTab {
wordWrap = !wordWrap;
return true;
}
+ if (ke.isCharIgnoreCase('f')) {
+ handledIndex = (handledIndex + 1) % HANDLED_FILTER.length;
+ handledFilter = HANDLED_FILTER[handledIndex];
+ return true;
+ }
+ if (ke.isCharIgnoreCase('p')) {
+ showProperties = !showProperties;
+ return true;
+ }
+ if (ke.isCharIgnoreCase('v')) {
+ showVariables = !showVariables;
+ return true;
+ }
+ if (ke.isCharIgnoreCase('h')) {
+ showHeaders = !showHeaders;
+ return true;
+ }
+ if (ke.isCharIgnoreCase('b')) {
+ showBody = !showBody;
+ return true;
+ }
+ if (ke.isHome()) {
+ detailScroll = 0;
+ return true;
+ }
+ if (ke.isEnd()) {
+ detailScroll = Integer.MAX_VALUE;
+ return true;
+ }
if (ke.isPageUp()) {
detailScroll = Math.max(0, detailScroll - 5);
return true;
@@ -113,8 +150,7 @@ class ErrorsTab implements MonitorTab {
@Override
public void navigateDown() {
detailScroll = 0;
- IntegrationInfo info = ctx.findSelectedIntegration();
- tableState.selectNext(info != null ? info.errors.size() : 0);
+ tableState.selectNext(filteredSize());
}
@Override
@@ -125,8 +161,7 @@ class ErrorsTab implements MonitorTab {
return;
}
- List<ErrorInfo> sorted = new ArrayList<>(info.errors);
- sorted.sort(this::sortError);
+ List<ErrorInfo> sorted = applyFilter(info.errors);
List<Row> rows = new ArrayList<>();
for (ErrorInfo ei : sorted) {
@@ -138,6 +173,7 @@ class ErrorsTab implements MonitorTab {
String shortException = shortExceptionType(ei.exceptionType);
rows.add(Row.from(
+ Cell.from(ei.exchangeId != null ? ei.exchangeId : ""),
Cell.from(ago),
Cell.from(Span.styled(ei.routeId != null ? ei.routeId : "", Style.EMPTY.fg(Color.CYAN))),
Cell.from(ei.nodeId != null ? ei.nodeId : ""),
@@ -150,7 +186,7 @@ class ErrorsTab implements MonitorTab {
rows.add(Row.from(
Cell.from(Span.styled("No errors captured", Style.EMPTY.dim())),
Cell.from(""), Cell.from(""), Cell.from(""),
- Cell.from(""), Cell.from("")));
+ Cell.from(""), Cell.from(""), Cell.from("")));
}
ErrorInfo selectedError = null;
@@ -161,20 +197,22 @@ class ErrorsTab implements MonitorTab {
boolean showDetail = selectedError != null;
List<Rect> chunks = showDetail
? Layout.vertical()
- .constraints(Constraint.length(Math.min(sorted.size() + 3, 12)), Constraint.fill())
+ .constraints(Constraint.length(13), Constraint.length(1), Constraint.fill())
.split(area)
: List.of(area);
Table table = Table.builder()
.rows(rows)
.header(Row.from(
+ Cell.from(Span.styled(sortLabel("ID", "id"), sortStyle("id"))),
Cell.from(Span.styled(sortLabel("AGO", "age"), sortStyle("age"))),
Cell.from(Span.styled(sortLabel("ROUTE", "route"), sortStyle("route"))),
- Cell.from(Span.styled("NODE", Style.EMPTY.bold())),
+ Cell.from(Span.styled(sortLabel("NODE", "node"), sortStyle("node"))),
Cell.from(Span.styled("HANDLED", Style.EMPTY.bold())),
Cell.from(Span.styled(sortLabel("EXCEPTION", "exception"), sortStyle("exception"))),
Cell.from(Span.styled("MESSAGE", Style.EMPTY.bold()))))
.widths(
+ Constraint.length(38),
Constraint.length(8),
Constraint.length(20),
Constraint.length(20),
@@ -189,7 +227,7 @@ class ErrorsTab implements MonitorTab {
frame.renderStatefulWidget(table, chunks.get(0), tableState);
if (showDetail) {
- renderDetail(frame, chunks.get(1), selectedError);
+ renderDetail(frame, chunks.get(2), selectedError);
}
}
@@ -197,9 +235,18 @@ class ErrorsTab implements MonitorTab {
public void renderFooter(List<Span> spans) {
hint(spans, "Esc", "back");
hint(spans, "↑↓", "navigate");
+ hint(spans, "PgUp/Dn", "scroll detail");
+ if (!wordWrap) {
+ hint(spans, "←→", "h-scroll");
+ }
+ hint(spans, "Home/End", "top/end");
hint(spans, "s", "sort");
- hint(spans, "w", "wrap");
- hint(spans, "PgUp/Dn", "scroll");
+ hint(spans, "f", "handled [" + handledFilter + "]");
+ hint(spans, "p", "properties [" + (showProperties ? "on" : "off") + "]");
+ hint(spans, "v", "variables [" + (showVariables ? "on" : "off") + "]");
+ hint(spans, "h", "headers [" + (showHeaders ? "on" : "off") + "]");
+ hint(spans, "b", "body [" + (showBody ? "on" : "off") + "]");
+ hint(spans, "w", "wrap [" + (wordWrap ? "on" : "off") + "]");
hint(spans, "1-0", "tabs");
}
@@ -216,10 +263,22 @@ class ErrorsTab implements MonitorTab {
StringBuilder sb = new StringBuilder();
sb.append(ei.exceptionType);
if (ei.exceptionMessage != null) {
- sb.append(": ").append(ei.exceptionMessage);
+ String msg = ei.exceptionMessage;
+ try {
+ msg = Jsoner.unescape(msg);
+ } catch (Exception e) {
+ // ignore
+ }
+ sb.append(": ").append(msg);
}
if (ei.stackTrace != null) {
- sb.append("\n").append(ei.stackTrace);
+ String st = ei.stackTrace;
+ try {
+ st = Jsoner.unescape(st);
+ } catch (Exception e) {
+ // ignore
+ }
+ sb.append("\n").append(st);
}
exception = sb.toString();
}
@@ -229,24 +288,24 @@ class ErrorsTab implements MonitorTab {
if (ei.messageHistory != null && ei.messageHistory.length > 0) {
lines.add(Line.from(Span.styled(" Message History:", Style.EMPTY.fg(Color.MAGENTA).bold())));
for (String step : ei.messageHistory) {
- lines.add(Line.from(Span.raw(" " + step)));
+ lines.add(Line.from(Span.raw(" " + TuiHelper.fixControlChars(step))));
}
lines.add(Line.from(Span.raw("")));
}
- // variables, properties, headers
- if (!ei.variables.isEmpty()) {
- HistoryTab.addKvLines(lines, " Variables:", ei.variables, ei.variableTypes);
+ // exchange properties, variables, headers, body
+ if (showProperties && !ei.properties.isEmpty()) {
+ HistoryTab.addKvLines(lines, " Exchange Properties:", ei.properties, ei.propertyTypes);
}
- if (!ei.properties.isEmpty()) {
- HistoryTab.addKvLines(lines, " Properties:", ei.properties, ei.propertyTypes);
+ if (showVariables && !ei.variables.isEmpty()) {
+ HistoryTab.addKvLines(lines, " Exchange Variables:", ei.variables, ei.variableTypes);
}
- if (!ei.headers.isEmpty()) {
+ if (showHeaders && !ei.headers.isEmpty()) {
HistoryTab.addKvLines(lines, " Headers:", ei.headers, ei.headerTypes);
}
-
- // body
- HistoryTab.addBodyLines(lines, ei.body, ei.bodyType);
+ if (showBody) {
+ HistoryTab.addBodyLines(lines, ei.body, ei.bodyType);
+ }
int[] scroll = { detailScroll };
int[] hScroll = { detailHScroll };
@@ -265,7 +324,9 @@ class ErrorsTab implements MonitorTab {
private int sortError(ErrorInfo a, ErrorInfo b) {
int result = switch (sort) {
+ case "id" -> compareStr(a.exchangeId, b.exchangeId);
case "route" -> compareStr(a.routeId, b.routeId);
+ case "node" -> compareStr(a.nodeId, b.nodeId);
case "exception" -> compareStr(a.exceptionType, b.exceptionType);
default -> Long.compare(b.timestamp, a.timestamp); // newest first
};
@@ -289,12 +350,33 @@ class ErrorsTab implements MonitorTab {
if (info == null || info.errors.isEmpty()) {
return null;
}
- List<ErrorInfo> sorted = new ArrayList<>(info.errors);
- sorted.sort(this::sortError);
- List<String> items = sorted.stream()
+ List<ErrorInfo> filtered = applyFilter(info.errors);
+ List<String> items = filtered.stream()
.map(e -> e.exchangeId != null ? e.exchangeId : "")
.toList();
Integer sel = tableState.selected();
return new SelectionContext("table", items, sel != null ? sel : -1, items.size(), "Errors");
}
+
+ private List<ErrorInfo> applyFilter(List<ErrorInfo> errors) {
+ List<ErrorInfo> list = new ArrayList<>(errors);
+ list.sort(this::sortError);
+ if (!"all".equals(handledFilter)) {
+ boolean filterVal = "true".equals(handledFilter);
+ list.removeIf(e -> e.handled != filterVal);
+ }
+ return list;
+ }
+
+ private int filteredSize() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null) {
+ return 0;
+ }
+ if ("all".equals(handledFilter)) {
+ return info.errors.size();
+ }
+ boolean filterVal = "true".equals(handledFilter);
+ return (int) info.errors.stream().filter(e -> e.handled == filterVal).count();
+ }
}
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 668bfce4bbbf..19021e2f9dab 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
@@ -844,6 +844,9 @@ class HistoryTab implements MonitorTab {
int w = l.width();
contentHeight += Math.max(1, (w + visibleWidth - 1) / visibleWidth);
}
+ // word-wrap breaks at word boundaries which can produce more lines
+ // than char-based math; add padding so last section is always reachable
+ contentHeight += visibleHeight;
} else {
contentHeight = lines.size();
}
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 ffaa18c73fbb..099e7b2294ca 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
@@ -62,6 +62,7 @@ class IntegrationInfo {
final List<HealthCheckInfo> healthChecks = new ArrayList<>();
final List<EndpointInfo> endpoints = new ArrayList<>();
final List<CircuitBreakerInfo> circuitBreakers = new ArrayList<>();
+ int errorCount;
final List<ErrorInfo> errors = new ArrayList<>();
final List<HttpEndpointInfo> httpEndpoints = new ArrayList<>();
String httpServer;
(camel) 01/03: CAMEL-23623: Add Errors tab to Camel TUI
Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch CAMEL-23623
in repository https://gitbox.apache.org/repos/asf/camel.git
commit 403a85dd02f8afecfcac6672b597c48a88cb513a
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed May 27 18:08:53 2026 +0200
CAMEL-23623: Add Errors tab to Camel TUI
Add a new Errors tab (key 0) showing captured routing errors from the
ErrorRegistry in a master/detail layout. The master table displays AGO,
ROUTE, NODE, HANDLED, EXCEPTION and MESSAGE columns. The detail pane
shows exchange info, exception with stack trace, message history,
variables, properties, headers and body with scrolling support.
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 91 ++++++-
.../dsl/jbang/core/commands/tui/ErrorInfo.java | 45 ++++
.../dsl/jbang/core/commands/tui/ErrorsTab.java | 300 +++++++++++++++++++++
.../jbang/core/commands/tui/IntegrationInfo.java | 1 +
4 files changed, 434 insertions(+), 3 deletions(-)
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 4995629b631a..31201caef382 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -104,7 +104,7 @@ public class CamelMonitor extends CamelCommand {
private static final int MAX_ENDPOINT_CHART_POINTS = 60;
private static final int MAX_LOG_LINES = 3000;
private static final int MAX_TRACES = 200;
- private static final int NUM_TABS = 9;
+ private static final int NUM_TABS = 10;
// Tab indices
private static final int TAB_OVERVIEW = 0;
@@ -116,6 +116,7 @@ public class CamelMonitor extends CamelCommand {
private static final int TAB_HEALTH = 6;
private static final int TAB_HISTORY = 7;
private static final int TAB_CIRCUIT_BREAKER = 8;
+ private static final int TAB_ERRORS = 9;
// Overview sort columns
private static final String[] OVERVIEW_SORT_COLUMNS = { "pid", "name", "version", "status", "total", "fail" };
@@ -263,6 +264,7 @@ public class CamelMonitor extends CamelCommand {
private HealthTab healthTab;
private HistoryTab historyTab;
private CircuitBreakerTab circuitBreakerTab;
+ private ErrorsTab errorsTab;
private ClassLoader classLoader;
@@ -311,6 +313,7 @@ public class CamelMonitor extends CamelCommand {
healthTab = new HealthTab(ctx);
historyTab = new HistoryTab(ctx, traces, traceFilePositions);
circuitBreakerTab = new CircuitBreakerTab(ctx, cbSuccessHistory, cbFailHistory);
+ errorsTab = new ErrorsTab(ctx);
// Initial data load (synchronous before TUI starts)
refreshDataSync();
@@ -459,6 +462,9 @@ public class CamelMonitor extends CamelCommand {
if (ke.isChar('9')) {
return handleTabKey(TAB_CIRCUIT_BREAKER);
}
+ if (ke.isChar('0')) {
+ return handleTabKey(TAB_ERRORS);
+ }
}
// Tab cycling (check Shift+Tab before Tab since Tab binding also matches Shift+Tab)
@@ -967,6 +973,7 @@ public class CamelMonitor extends CamelCommand {
Line.from(" 7 Health "),
Line.from(" 8 Inspect "),
Line.from(" 9 Circuit Breaker "),
+ Line.from(" 0 Errors "),
};
Tabs tabs = Tabs.builder()
@@ -986,7 +993,7 @@ public class CamelMonitor extends CamelCommand {
int badgeY = area.y();
int dividerW = CharWidth.of(" | ");
- String[] badgeTexts = { "", "", "", "", "", "", "", "", "" };
+ String[] badgeTexts = { "", "", "", "", "", "", "", "", "", "" };
Style[] badgeStyles = new Style[labels.length];
Style yellow = Style.EMPTY.fg(Color.YELLOW).bold();
Style cyan = Style.EMPTY.fg(Color.CYAN).bold();
@@ -1028,6 +1035,11 @@ public class CamelMonitor extends CamelCommand {
} else if (cbCount > 0) {
badgeTexts[TAB_CIRCUIT_BREAKER] = "(" + cbCount + ")";
}
+ int errorCount = hasSelection ? sel.errors.size() : 0;
+ if (errorCount > 0) {
+ badgeTexts[TAB_ERRORS] = "(" + errorCount + ")";
+ badgeStyles[TAB_ERRORS] = red;
+ }
int tabX = 0;
for (int i = 0; i < labels.length; i++) {
@@ -1068,6 +1080,7 @@ public class CamelMonitor extends CamelCommand {
case TAB_HEALTH -> healthTab;
case TAB_HISTORY -> historyTab;
case TAB_HTTP -> httpTab;
+ case TAB_ERRORS -> errorsTab;
default -> null;
};
}
@@ -2996,6 +3009,60 @@ public class CamelMonitor extends CamelCommand {
}
}
+ // Parse errors from error registry
+ JsonObject errorsObj = (JsonObject) root.get("errors");
+ if (errorsObj != null) {
+ JsonArray errorList = (JsonArray) errorsObj.get("errors");
+ if (errorList != null) {
+ for (Object e : errorList) {
+ JsonObject ej = (JsonObject) e;
+ ErrorInfo ei = new ErrorInfo();
+ ei.routeId = ej.getString("routeId");
+ ei.nodeId = ej.getString("nodeId");
+ ei.exchangeId = ej.getString("exchangeId");
+ ei.handled = ej.getBooleanOrDefault("handled", false);
+ Long ts = ej.getLong("timestamp");
+ if (ts != null) {
+ ei.timestamp = ts;
+ }
+ ei.location = ej.getString("location");
+ ei.threadName = ej.getString("threadName");
+ Long elapsed = ej.getLong("elapsed");
+ if (elapsed != null) {
+ ei.elapsed = elapsed;
+ }
+ ei.endpointUri = ej.getString("endpointUri");
+ ei.fromEndpointUri = ej.getString("fromEndpointUri");
+ // exception
+ JsonObject ex = (JsonObject) ej.get("exception");
+ if (ex != null) {
+ ei.exceptionType = ex.getString("type");
+ ei.exceptionMessage = ex.getString("message");
+ ei.stackTrace = ex.getString("stackTrace");
+ }
+ // message history
+ Object mhObj = ej.get("messageHistory");
+ if (mhObj instanceof JsonArray mhArr) {
+ ei.messageHistory = new String[mhArr.size()];
+ for (int i = 0; i < mhArr.size(); i++) {
+ ei.messageHistory[i] = mhArr.get(i).toString();
+ }
+ }
+ // message (body, headers)
+ JsonObject msg = (JsonObject) ej.get("message");
+ if (msg != null) {
+ ei.body = msg.getString("body");
+ ei.bodyType = msg.getString("bodyType");
+ parseKvArray(msg.getCollection("headers"), ei.headers, ei.headerTypes);
+ }
+ // exchange properties and variables
+ parseKvArray(ej.getCollection("exchangeProperties"), ei.properties, ei.propertyTypes);
+ parseKvArray(ej.getCollection("exchangeVariables"), ei.variables, ei.variableTypes);
+ info.errors.add(ei);
+ }
+ }
+ }
+
// Parse REST DSL services
JsonObject restsObj = (JsonObject) root.get("rests");
if (restsObj != null) {
@@ -3107,6 +3174,24 @@ public class CamelMonitor extends CamelCommand {
}
}
+ @SuppressWarnings("unchecked")
+ private static void parseKvArray(JsonArray arr, Map<String, Object> values, Map<String, String> types) {
+ if (arr == null) {
+ return;
+ }
+ for (Object o : arr) {
+ JsonObject jo = (JsonObject) o;
+ String key = jo.getString("key");
+ if (key != null) {
+ values.put(key, jo.get("value"));
+ String type = jo.getString("type");
+ if (type != null) {
+ types.put(key, type);
+ }
+ }
+ }
+ }
+
// ---- Helpers ----
private IntegrationInfo findSelectedIntegration() {
@@ -3208,7 +3293,7 @@ public class CamelMonitor extends CamelCommand {
private static final String[] TAB_NAMES = {
"Overview", "Log", "Routes", "Consumers", "Endpoints",
- "HTTP", "Health", "Inspect", "Circuit Breaker"
+ "HTTP", "Health", "Inspect", "Circuit Breaker", "Errors"
};
Buffer getLastBuffer() {
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorInfo.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorInfo.java
new file mode 100644
index 000000000000..d01b5026412e
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorInfo.java
@@ -0,0 +1,45 @@
+/*
+ * 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.LinkedHashMap;
+import java.util.Map;
+
+class ErrorInfo {
+ String routeId;
+ String nodeId;
+ String exchangeId;
+ boolean handled;
+ long timestamp;
+ String location;
+ String threadName;
+ long elapsed;
+ String endpointUri;
+ String fromEndpointUri;
+ String exceptionType;
+ String exceptionMessage;
+ String stackTrace;
+ String[] messageHistory;
+ String body;
+ String bodyType;
+ final Map<String, Object> headers = new LinkedHashMap<>();
+ final Map<String, String> headerTypes = new LinkedHashMap<>();
+ final Map<String, Object> properties = new LinkedHashMap<>();
+ final Map<String, String> propertyTypes = new LinkedHashMap<>();
+ final Map<String, Object> variables = new LinkedHashMap<>();
+ final Map<String, String> variableTypes = new LinkedHashMap<>();
+}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
new file mode 100644
index 000000000000..c74e3c7ceb39
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
@@ -0,0 +1,300 @@
+/*
+ * 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.List;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.scrollbar.ScrollbarState;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import dev.tamboui.widgets.table.TableState;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
+
+class ErrorsTab implements MonitorTab {
+
+ private static final String[] SORT_COLUMNS = { "age", "route", "exception" };
+
+ private final MonitorContext ctx;
+ private final TableState tableState = new TableState();
+ private final ScrollbarState detailScrollState = new ScrollbarState();
+ private String sort = "age";
+ private int sortIndex;
+ private boolean sortReversed;
+ private int detailScroll;
+ private int detailHScroll;
+ private boolean wordWrap = true;
+
+ ErrorsTab(MonitorContext ctx) {
+ this.ctx = ctx;
+ }
+
+ @Override
+ public void onTabSelected() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info != null && !info.errors.isEmpty() && tableState.selected() == null) {
+ tableState.select(0);
+ }
+ }
+
+ @Override
+ public boolean handleKeyEvent(KeyEvent ke) {
+ if (ke.isChar('s')) {
+ sortIndex = (sortIndex + 1) % SORT_COLUMNS.length;
+ sort = SORT_COLUMNS[sortIndex];
+ sortReversed = false;
+ return true;
+ }
+ if (ke.isChar('S')) {
+ sortReversed = !sortReversed;
+ return true;
+ }
+ if (ke.isCharIgnoreCase('w')) {
+ wordWrap = !wordWrap;
+ return true;
+ }
+ if (ke.isPageUp()) {
+ detailScroll = Math.max(0, detailScroll - 5);
+ return true;
+ }
+ if (ke.isPageDown()) {
+ detailScroll += 5;
+ return true;
+ }
+ if (ke.isLeft() && !wordWrap) {
+ detailHScroll = Math.max(0, detailHScroll - 4);
+ return true;
+ }
+ if (ke.isRight() && !wordWrap) {
+ detailHScroll += 4;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean handleEscape() {
+ return false;
+ }
+
+ @Override
+ public void navigateUp() {
+ detailScroll = 0;
+ tableState.selectPrevious();
+ }
+
+ @Override
+ public void navigateDown() {
+ detailScroll = 0;
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ tableState.selectNext(info != null ? info.errors.size() : 0);
+ }
+
+ @Override
+ public void render(Frame frame, Rect area) {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null) {
+ renderNoSelection(frame, area);
+ return;
+ }
+
+ List<ErrorInfo> sorted = new ArrayList<>(info.errors);
+ sorted.sort(this::sortError);
+
+ List<Row> rows = new ArrayList<>();
+ for (ErrorInfo ei : sorted) {
+ String ago = ei.timestamp > 0
+ ? org.apache.camel.util.TimeUtils.printSince(ei.timestamp) : "";
+ String handledStr = ei.handled ? "true" : "false";
+ Style handledStyle = ei.handled
+ ? Style.EMPTY.fg(Color.GREEN) : Style.EMPTY.fg(Color.LIGHT_RED);
+ String shortException = shortExceptionType(ei.exceptionType);
+
+ rows.add(Row.from(
+ Cell.from(ago),
+ Cell.from(Span.styled(ei.routeId != null ? ei.routeId : "", Style.EMPTY.fg(Color.CYAN))),
+ Cell.from(ei.nodeId != null ? ei.nodeId : ""),
+ Cell.from(Span.styled(handledStr, handledStyle)),
+ Cell.from(shortException),
+ Cell.from(ei.exceptionMessage != null ? ei.exceptionMessage : "")));
+ }
+
+ if (rows.isEmpty()) {
+ rows.add(Row.from(
+ Cell.from(Span.styled("No errors captured", Style.EMPTY.dim())),
+ Cell.from(""), Cell.from(""), Cell.from(""),
+ Cell.from(""), Cell.from("")));
+ }
+
+ ErrorInfo selectedError = null;
+ Integer sel = tableState.selected();
+ if (sel != null && sel >= 0 && sel < sorted.size()) {
+ selectedError = sorted.get(sel);
+ }
+ boolean showDetail = selectedError != null;
+ List<Rect> chunks = showDetail
+ ? Layout.vertical()
+ .constraints(Constraint.length(Math.min(sorted.size() + 3, 12)), Constraint.fill())
+ .split(area)
+ : List.of(area);
+
+ Table table = Table.builder()
+ .rows(rows)
+ .header(Row.from(
+ Cell.from(Span.styled(sortLabel("AGO", "age"), sortStyle("age"))),
+ Cell.from(Span.styled(sortLabel("ROUTE", "route"), sortStyle("route"))),
+ Cell.from(Span.styled("NODE", Style.EMPTY.bold())),
+ Cell.from(Span.styled("HANDLED", Style.EMPTY.bold())),
+ Cell.from(Span.styled(sortLabel("EXCEPTION", "exception"), sortStyle("exception"))),
+ Cell.from(Span.styled("MESSAGE", Style.EMPTY.bold()))))
+ .widths(
+ Constraint.length(8),
+ Constraint.length(20),
+ Constraint.length(20),
+ Constraint.length(8),
+ Constraint.length(30),
+ Constraint.fill())
+ .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
+ .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+ .block(Block.builder().borderType(BorderType.ROUNDED).title(" Errors ").build())
+ .build();
+
+ frame.renderStatefulWidget(table, chunks.get(0), tableState);
+
+ if (showDetail) {
+ renderDetail(frame, chunks.get(1), selectedError);
+ }
+ }
+
+ @Override
+ public void renderFooter(List<Span> spans) {
+ hint(spans, "Esc", "back");
+ hint(spans, "↑↓", "navigate");
+ hint(spans, "s", "sort");
+ hint(spans, "w", "wrap");
+ hint(spans, "PgUp/Dn", "scroll");
+ hint(spans, "1-0", "tabs");
+ }
+
+ private void renderDetail(Frame frame, Rect area, ErrorInfo ei) {
+ List<Line> lines = new ArrayList<>();
+
+ HistoryTab.addExchangeInfoLines(lines,
+ ei.exchangeId, ei.routeId, ei.nodeId, null, ei.location,
+ ei.elapsed, ei.threadName, !ei.handled);
+
+ // exception with stack trace
+ String exception = null;
+ if (ei.exceptionType != null) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(ei.exceptionType);
+ if (ei.exceptionMessage != null) {
+ sb.append(": ").append(ei.exceptionMessage);
+ }
+ if (ei.stackTrace != null) {
+ sb.append("\n").append(ei.stackTrace);
+ }
+ exception = sb.toString();
+ }
+ HistoryTab.addExceptionLines(lines, exception);
+
+ // message history
+ if (ei.messageHistory != null && ei.messageHistory.length > 0) {
+ lines.add(Line.from(Span.styled(" Message History:", Style.EMPTY.fg(Color.MAGENTA).bold())));
+ for (String step : ei.messageHistory) {
+ lines.add(Line.from(Span.raw(" " + step)));
+ }
+ lines.add(Line.from(Span.raw("")));
+ }
+
+ // variables, properties, headers
+ if (!ei.variables.isEmpty()) {
+ HistoryTab.addKvLines(lines, " Variables:", ei.variables, ei.variableTypes);
+ }
+ if (!ei.properties.isEmpty()) {
+ HistoryTab.addKvLines(lines, " Properties:", ei.properties, ei.propertyTypes);
+ }
+ if (!ei.headers.isEmpty()) {
+ HistoryTab.addKvLines(lines, " Headers:", ei.headers, ei.headerTypes);
+ }
+
+ // body
+ HistoryTab.addBodyLines(lines, ei.body, ei.bodyType);
+
+ int[] scroll = { detailScroll };
+ int[] hScroll = { detailHScroll };
+ HistoryTab.renderDetailPanel(frame, area, lines, wordWrap, hScroll, scroll, detailScrollState);
+ detailScroll = scroll[0];
+ detailHScroll = hScroll[0];
+ }
+
+ private String sortLabel(String label, String column) {
+ return MonitorContext.sortLabel(label, column, sort, sortReversed);
+ }
+
+ private Style sortStyle(String column) {
+ return MonitorContext.sortStyle(column, sort);
+ }
+
+ private int sortError(ErrorInfo a, ErrorInfo b) {
+ int result = switch (sort) {
+ case "route" -> compareStr(a.routeId, b.routeId);
+ case "exception" -> compareStr(a.exceptionType, b.exceptionType);
+ default -> Long.compare(b.timestamp, a.timestamp); // newest first
+ };
+ return sortReversed ? -result : result;
+ }
+
+ private static String shortExceptionType(String type) {
+ if (type == null) {
+ return "";
+ }
+ int dot = type.lastIndexOf('.');
+ if (dot > 0) {
+ return type.substring(dot + 1);
+ }
+ return type;
+ }
+
+ @Override
+ public SelectionContext getSelectionContext() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null || info.errors.isEmpty()) {
+ return null;
+ }
+ List<ErrorInfo> sorted = new ArrayList<>(info.errors);
+ sorted.sort(this::sortError);
+ List<String> items = sorted.stream()
+ .map(e -> e.exchangeId != null ? e.exchangeId : "")
+ .toList();
+ Integer sel = tableState.selected();
+ return new SelectionContext("table", items, sel != null ? sel : -1, items.size(), "Errors");
+ }
+}
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 921ab26ad816..ffaa18c73fbb 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
@@ -62,6 +62,7 @@ class IntegrationInfo {
final List<HealthCheckInfo> healthChecks = new ArrayList<>();
final List<EndpointInfo> endpoints = new ArrayList<>();
final List<CircuitBreakerInfo> circuitBreakers = new ArrayList<>();
+ final List<ErrorInfo> errors = new ArrayList<>();
final List<HttpEndpointInfo> httpEndpoints = new ArrayList<>();
String httpServer;
String readmeFiles;