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 a1a95ed95316 CAMEL-23623: Add Errors tab to Camel TUI (#23573)
a1a95ed95316 is described below
commit a1a95ed9531656cfc8efba6bff043695c72d2b0a
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed May 27 20:30:52 2026 +0200
CAMEL-23623: Add Errors tab to Camel TUI (#23573)
* 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 <[email protected]>
* 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 <[email protected]>
* 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 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.../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 | 176 +++++++++-
.../dsl/jbang/core/commands/tui/ErrorInfo.java | 45 +++
.../dsl/jbang/core/commands/tui/ErrorsTab.java | 382 +++++++++++++++++++++
.../dsl/jbang/core/commands/tui/HistoryTab.java | 3 +
.../jbang/core/commands/tui/IntegrationInfo.java | 2 +
9 files changed, 638 insertions(+), 23 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 4995629b631a..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
@@ -104,18 +104,19 @@ 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;
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_CONSUMERS = 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();
@@ -442,23 +445,26 @@ 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_CONSUMERS);
+ }
}
// Tab cycling (check Shift+Tab before Tab since Tab binding also
matches Shift+Tab)
@@ -730,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;
}
@@ -961,12 +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 Consumer "),
};
Tabs tabs = Tabs.builder()
@@ -986,7 +1002,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 +1044,11 @@ public class CamelMonitor extends CamelCommand {
} else if (cbCount > 0) {
badgeTexts[TAB_CIRCUIT_BREAKER] = "(" + cbCount + ")";
}
+ int errorCount = hasSelection ? sel.errorCount : 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 +1089,7 @@ public class CamelMonitor extends CamelCommand {
case TAB_HEALTH -> healthTab;
case TAB_HISTORY -> historyTab;
case TAB_HTTP -> httpTab;
+ case TAB_ERRORS -> errorsTab;
default -> null;
};
}
@@ -1999,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);
@@ -2996,6 +3023,12 @@ public class CamelMonitor extends CamelCommand {
}
}
+ // Parse error count from error registry (full error data is loaded on
demand by ErrorsTab)
+ JsonObject errorsObj = (JsonObject) root.get("errors");
+ if (errorsObj != null) {
+ info.errorCount = errorsObj.getIntegerOrDefault("size", 0);
+ }
+
// Parse REST DSL services
JsonObject restsObj = (JsonObject) root.get("rests");
if (restsObj != null) {
@@ -3107,6 +3140,111 @@ 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);
+ }
+ }
+ }
+ }
+
+ 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() {
@@ -3207,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"
+ "Overview", "Log", "Routes", "Endpoints",
+ "HTTP", "Health", "Inspect", "Errors", "Circuit Breaker",
"Consumers"
};
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..9f5d9663aff6
--- /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,382 @@
+/*
+ * 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 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 = { "id", "age", "route",
"node", "exception" };
+
+ private final MonitorContext ctx;
+ private final TableState tableState = new TableState();
+ private final ScrollbarState detailScrollState = new ScrollbarState();
+ 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;
+ }
+
+ @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.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;
+ }
+ 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;
+ tableState.selectNext(filteredSize());
+ }
+
+ @Override
+ public void render(Frame frame, Rect area) {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null) {
+ renderNoSelection(frame, area);
+ return;
+ }
+
+ List<ErrorInfo> sorted = applyFilter(info.errors);
+
+ 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(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 : ""),
+ 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(""), 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(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(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),
+ 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(2), selectedError);
+ }
+ }
+
+ @Override
+ 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, "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");
+ }
+
+ 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) {
+ String msg = ei.exceptionMessage;
+ try {
+ msg = Jsoner.unescape(msg);
+ } catch (Exception e) {
+ // ignore
+ }
+ sb.append(": ").append(msg);
+ }
+ if (ei.stackTrace != null) {
+ String st = ei.stackTrace;
+ try {
+ st = Jsoner.unescape(st);
+ } catch (Exception e) {
+ // ignore
+ }
+ sb.append("\n").append(st);
+ }
+ 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(" " +
TuiHelper.fixControlChars(step))));
+ }
+ lines.add(Line.from(Span.raw("")));
+ }
+
+ // exchange properties, variables, headers, body
+ if (showProperties && !ei.properties.isEmpty()) {
+ HistoryTab.addKvLines(lines, " Exchange Properties:",
ei.properties, ei.propertyTypes);
+ }
+ if (showVariables && !ei.variables.isEmpty()) {
+ HistoryTab.addKvLines(lines, " Exchange Variables:", ei.variables,
ei.variableTypes);
+ }
+ if (showHeaders && !ei.headers.isEmpty()) {
+ HistoryTab.addKvLines(lines, " Headers:", ei.headers,
ei.headerTypes);
+ }
+ if (showBody) {
+ 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 "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
+ };
+ 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> 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 921ab26ad816..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,8 @@ 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;
String readmeFiles;