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 e42a68043b10 CAMEL-23624: Add camel get error CLI command
e42a68043b10 is described below
commit e42a68043b1032e2d0ec134a860e994ef282f353
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed May 27 13:39:55 2026 +0200
CAMEL-23624: Add camel get error CLI command
Add a new `camel get error` CLI command to browse captured routing errors
from the ErrorRegistry. The errors dev console gains server-side filtering
by exception type (substring), time window (ago), and handled status. The
console is wired into LocalCliConnector so errors appear in the status JSON.
The CLI command supports --route, --exception, --ago, --handled, --limit,
--sort, --show, --watch and --json options with a default summary table.
Closes #23556
---
.../camel/impl/console/ErrorRegistryConsole.java | 81 ++++-
.../jbang-commands/camel-jbang-get-error.adoc | 34 ++
.../ROOT/pages/jbang-commands/camel-jbang-get.adoc | 1 +
.../camel/cli/connector/LocalCliConnector.java | 8 +
.../META-INF/camel-jbang-commands-metadata.json | 2 +-
.../dsl/jbang/core/commands/CamelJBangMain.java | 1 +
.../dsl/jbang/core/commands/process/ListError.java | 344 +++++++++++++++++++++
7 files changed, 454 insertions(+), 17 deletions(-)
diff --git
a/core/camel-console/src/main/java/org/apache/camel/impl/console/ErrorRegistryConsole.java
b/core/camel-console/src/main/java/org/apache/camel/impl/console/ErrorRegistryConsole.java
index 6b5477fb033f..9faba207c66b 100644
---
a/core/camel-console/src/main/java/org/apache/camel/impl/console/ErrorRegistryConsole.java
+++
b/core/camel-console/src/main/java/org/apache/camel/impl/console/ErrorRegistryConsole.java
@@ -16,13 +16,16 @@
*/
package org.apache.camel.impl.console;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.List;
import java.util.Map;
import org.apache.camel.spi.BacklogErrorEventMessage;
import org.apache.camel.spi.ErrorRegistry;
import org.apache.camel.spi.annotations.DevConsole;
import org.apache.camel.support.console.AbstractDevConsole;
+import org.apache.camel.util.TimeUtils;
import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
@@ -44,14 +47,27 @@ public class ErrorRegistryConsole extends
AbstractDevConsole {
*/
public static final String STACK_TRACE = "stackTrace";
+ /**
+ * Filter by exception type (case-insensitive substring match)
+ */
+ public static final String EXCEPTION = "exception";
+
+ /**
+ * Filter by time window as duration string (e.g. "60s", "5m", "1h"). Only
entries within this window are included.
+ */
+ public static final String AGO = "ago";
+
+ /**
+ * Filter by handled status ("true" or "false")
+ */
+ public static final String HANDLED = "handled";
+
public ErrorRegistryConsole() {
super("camel", "errors", "Error Registry", "Display captured routing
errors");
}
@Override
protected String doCallText(Map<String, Object> options) {
- String routeId = (String) options.get(ROUTE_ID);
- int max = parseLimit(options);
boolean includeStackTrace = "true".equals(options.get(STACK_TRACE));
StringBuilder sb = new StringBuilder();
@@ -60,12 +76,7 @@ public class ErrorRegistryConsole extends AbstractDevConsole
{
sb.append(String.format("%n Enabled: %s", registry.isEnabled()));
sb.append(String.format("%n Size: %s", registry.size()));
- Collection<BacklogErrorEventMessage> entries;
- if (routeId != null) {
- entries = registry.forRoute(routeId).browse(max);
- } else {
- entries = registry.browse(max);
- }
+ List<BacklogErrorEventMessage> entries = fetchAndFilter(registry,
options);
for (BacklogErrorEventMessage entry : entries) {
sb.append(String.format("%n %s (route: %s, node: %s, endpoint:
%s, handled: %s)",
@@ -94,8 +105,6 @@ public class ErrorRegistryConsole extends AbstractDevConsole
{
@Override
protected JsonObject doCallJson(Map<String, Object> options) {
- String routeId = (String) options.get(ROUTE_ID);
- int max = parseLimit(options);
boolean includeStackTrace = "true".equals(options.get(STACK_TRACE));
JsonObject root = new JsonObject();
@@ -106,12 +115,7 @@ public class ErrorRegistryConsole extends
AbstractDevConsole {
root.put("maximumEntries", registry.getMaximumEntries());
root.put("timeToLive", registry.getTimeToLive().toString());
- Collection<BacklogErrorEventMessage> entries;
- if (routeId != null) {
- entries = registry.forRoute(routeId).browse(max);
- } else {
- entries = registry.browse(max);
- }
+ List<BacklogErrorEventMessage> entries = fetchAndFilter(registry,
options);
final JsonArray list = new JsonArray();
for (BacklogErrorEventMessage entry : entries) {
@@ -130,6 +134,51 @@ public class ErrorRegistryConsole extends
AbstractDevConsole {
return root;
}
+ private static List<BacklogErrorEventMessage> fetchAndFilter(ErrorRegistry
registry, Map<String, Object> options) {
+ String routeId = (String) options.get(ROUTE_ID);
+ String exceptionFilter = (String) options.get(EXCEPTION);
+ String agoFilter = (String) options.get(AGO);
+ String handledFilter = (String) options.get(HANDLED);
+ int max = parseLimit(options);
+
+ // fetch all entries (route-scoped if requested), apply filters, then
limit
+ Collection<BacklogErrorEventMessage> all;
+ if (routeId != null) {
+ all = registry.forRoute(routeId).browse();
+ } else {
+ all = registry.browse();
+ }
+
+ long agoCutoff = -1;
+ if (agoFilter != null) {
+ try {
+ long millis = TimeUtils.toMilliSeconds(agoFilter);
+ agoCutoff = System.currentTimeMillis() - millis;
+ } catch (Exception e) {
+ // ignore invalid ago value
+ }
+ }
+
+ List<BacklogErrorEventMessage> result = new ArrayList<>();
+ for (BacklogErrorEventMessage entry : all) {
+ if (agoCutoff > 0 && entry.getTimestamp() < agoCutoff) {
+ continue;
+ }
+ if (exceptionFilter != null
+ &&
!entry.getExceptionType().toLowerCase().contains(exceptionFilter.toLowerCase()))
{
+ continue;
+ }
+ if (handledFilter != null &&
!String.valueOf(entry.isHandled()).equals(handledFilter)) {
+ continue;
+ }
+ result.add(entry);
+ if (max > 0 && max < Integer.MAX_VALUE && result.size() >= max) {
+ break;
+ }
+ }
+ return result;
+ }
+
private static int parseLimit(Map<String, Object> options) {
String limit = (String) options.get(LIMIT);
if (limit == null) {
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-error.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-error.adoc
new file mode 100644
index 000000000000..7e2cdbf17c7d
--- /dev/null
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-error.adoc
@@ -0,0 +1,34 @@
+
+// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
+= camel get error
+
+Get captured routing errors of Camel integrations
+
+
+== Usage
+
+[source,bash]
+----
+camel get error [options]
+----
+
+
+
+== Options
+
+[cols="2,5,1,2",options="header"]
+|===
+| Option | Description | Default | Type
+| `--ago` | Filter by time window, e.g. 60s, 5m, 1h | | String
+| `--exception` | Filter by exception type (substring match) | | String
+| `--handled` | Filter by handled status (true or false) | | String
+| `--json` | Output in JSON Format | | boolean
+| `--limit` | Maximum number of entries to display | | int
+| `--route` | Filter by route ID | | String
+| `--show` | Comma-separated detail sections to show: body, headers,
properties, variables, history, stackTrace | | String
+| `--sort` | Sort by pid, name or age | pid | String
+| `--watch` | Execute periodically and showing output fullscreen | | boolean
+| `-h,--help` | Display the help and sub-commands | | boolean
+|===
+
+
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get.adoc
index 5b889526db43..91dbbbb51184 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get.adoc
@@ -25,6 +25,7 @@ camel get [options]
| xref:jbang-commands/camel-jbang-get-context.adoc[context] | Get status of
Camel integrations
| xref:jbang-commands/camel-jbang-get-count.adoc[count] | Get total and failed
exchanges
| xref:jbang-commands/camel-jbang-get-endpoint.adoc[endpoint] | Get usage of
Camel endpoints
+| xref:jbang-commands/camel-jbang-get-error.adoc[error] | Get captured routing
errors of Camel integrations
| xref:jbang-commands/camel-jbang-get-event.adoc[event] | Get latest events of
Camel integrations
| xref:jbang-commands/camel-jbang-get-groovy.adoc[groovy] | Groovy Sources
used of Camel integrations
| xref:jbang-commands/camel-jbang-get-group.adoc[group] | Get status of Camel
route groups
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 f32121763448..a4ca0c6ccaf9 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
@@ -1384,6 +1384,14 @@ public class LocalCliConnector extends ServiceSupport
implements CliConnector, C
root.put("groovy", json);
}
}
+ DevConsole dc26 = dcr.resolveById("errors");
+ if (dc26 != null) {
+ JsonObject json = (JsonObject)
dc26.call(DevConsole.MediaType.JSON,
+ Map.of("stackTrace", "true"));
+ if (json != null && !json.isEmpty()) {
+ root.put("errors", json);
+ }
+ }
}
// various details
JsonObject mem = collectMemory();
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index f668e15d7669..da216b545ad7 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -14,7 +14,7 @@
{ "name": "eval", "fullName": "eval", "description": "Evaluate Camel
expressions and scripts", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.EvalCommand", "options": [ { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "expression",
"fullName": "eval expression", "description": "Evaluates Camel expression",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.EvalEx [...]
{ "name": "explain", "fullName": "explain", "description": "Explain what a
Camel route does using AI\/LLM", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Explain", "options": [ { "names":
"--api-key", "description": "API key for authentication. Also reads
ANTHROPIC_API_KEY, OPENAI_API_KEY, or LLM_API_KEY env vars", "javaType":
"java.lang.String", "type": "string" }, { "names": "--api-type", "description":
"API type: 'ollama', 'openai' (OpenAI-compatible), or 'anthropic' (A [...]
{ "name": "export", "fullName": "export", "description": "Export to other
runtimes (Camel Main, Spring Boot, or Quarkus)", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Export", "options": [ { "names":
"--build-property", "description": "Maven build properties, ex.
--build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, {
"names": "--camel-spring-boot-version", "description": "Camel version to use
with Spring Boot", "javaType": "java.lang.String", "ty [...]
- { "name": "get", "fullName": "get", "description": "Get status of Camel
integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ {
"names": "--watch", "description": "Execute periodically and showing output
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "bean",
"fullName": "get [...]
+ { "name": "get", "fullName": "get", "description": "Get status of Camel
integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ {
"names": "--watch", "description": "Execute periodically and showing output
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "bean",
"fullName": "get [...]
{ "name": "harden", "fullName": "harden", "description": "Suggest security
hardening for Camel routes using AI\/LLM", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Harden", "options": [ { "names":
"--api-key", "description": "API key for authentication. Also reads
OPENAI_API_KEY or LLM_API_KEY env vars", "javaType": "java.lang.String",
"type": "string" }, { "names": "--api-type", "description": "API type: 'ollama'
or 'openai' (OpenAI-compatible)", "defaultValue": "ollama", [...]
{ "name": "hawtio", "fullName": "hawtio", "description": "Launch Hawtio
web console", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.Hawtio", "options": [ {
"names": "--host", "description": "Hostname to bind the Hawtio web console to",
"defaultValue": "127.0.0.1", "javaType": "java.lang.String", "type": "string"
}, { "names": "--openUrl", "description": "To automatic open Hawtio web console
in the web browser", "defaultValue": "true", "javaType": "boolean", "type":
[...]
{ "name": "infra", "fullName": "infra", "description": "List and Run
external services for testing and prototyping", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.infra.InfraCommand", "options": [ {
"names": "--json", "description": "Output in JSON Format", "javaType":
"boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display
the help and sub-commands", "javaType": "boolean", "type": "boolean" } ],
"subcommands": [ { "name": "get", "fullName": "infra [...]
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
index 52366e048c5d..9d0996730950 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
@@ -148,6 +148,7 @@ public class CamelJBangMain implements Callable<Integer> {
.addSubcommand("context", new CommandLine(new
CamelContextStatus(this)))
.addSubcommand("count", new CommandLine(new
CamelCount(this)))
.addSubcommand("endpoint", new CommandLine(new
ListEndpoint(this)))
+ .addSubcommand("error", new CommandLine(new
ListError(this)))
.addSubcommand("event", new CommandLine(new
ListEvent(this)))
.addSubcommand("groovy", new CommandLine(new
ListGroovy(this)))
.addSubcommand("group", new CommandLine(new
CamelRouteGroupStatus(this)))
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
new file mode 100644
index 000000000000..96237a6276f8
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListError.java
@@ -0,0 +1,344 @@
+/*
+ * 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.process;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.github.freva.asciitable.AsciiTable;
+import com.github.freva.asciitable.Column;
+import com.github.freva.asciitable.HorizontalAlign;
+import com.github.freva.asciitable.OverflowBehaviour;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.PidNameAgeCompletionCandidates;
+import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "error",
+ description = "Get captured routing errors of Camel integrations",
sortOptions = false, showDefaultValues = true)
+public class ListError extends ProcessWatchCommand {
+
+ @CommandLine.Parameters(description = "Name or pid of running Camel
integration", arity = "0..1")
+ String name = "*";
+
+ @CommandLine.Option(names = { "--sort" }, completionCandidates =
PidNameAgeCompletionCandidates.class,
+ description = "Sort by pid, name or age", defaultValue
= "pid")
+ String sort;
+
+ @CommandLine.Option(names = { "--route" },
+ description = "Filter by route ID")
+ String route;
+
+ @CommandLine.Option(names = { "--exception" },
+ description = "Filter by exception type (substring
match)")
+ String exception;
+
+ @CommandLine.Option(names = { "--ago" },
+ description = "Filter by time window, e.g. 60s, 5m,
1h")
+ String ago;
+
+ @CommandLine.Option(names = { "--handled" },
+ description = "Filter by handled status (true or
false)")
+ String handled;
+
+ @CommandLine.Option(names = { "--limit" },
+ description = "Maximum number of entries to display")
+ int limit;
+
+ @CommandLine.Option(names = { "--show" },
+ description = "Comma-separated detail sections to
show: body, headers, properties, variables, history, stackTrace")
+ String show;
+
+ public ListError(CamelJBangMain main) {
+ super(main);
+ }
+
+ @Override
+ public Integer doProcessWatchCall() throws Exception {
+ List<Row> rows = new ArrayList<>();
+
+ Set<String> showSet = show != null
+ ?
Arrays.stream(show.split(",")).map(String::trim).collect(Collectors.toSet())
+ : Set.of();
+
+ List<Long> pids = findPids(name);
+ ProcessHandle.allProcesses()
+ .filter(ph -> pids.contains(ph.pid()))
+ .forEach(ph -> {
+ JsonObject root = loadStatus(ph.pid());
+ if (root != null) {
+ JsonObject context = (JsonObject) root.get("context");
+ if (context == null) {
+ return;
+ }
+ String pName = context.getString("name");
+ if ("CamelJBang".equals(pName)) {
+ pName = ProcessHelper.extractName(root, ph);
+ }
+ String pid = Long.toString(ph.pid());
+
+ JsonObject errors = (JsonObject) root.get("errors");
+ if (errors != null) {
+ JsonArray arr = (JsonArray) errors.get("errors");
+ if (arr != null) {
+ for (Object o : arr) {
+ JsonObject jo = (JsonObject) o;
+ Row row = new Row();
+ row.pid = pid;
+ row.name = pName;
+ row.routeId = jo.getString("routeId");
+ row.nodeId = jo.getString("nodeId");
+ row.exchangeId =
jo.getString("exchangeId");
+ row.handled = jo.getBoolean("handled") !=
null && jo.getBoolean("handled");
+ Long ts = jo.getLong("timestamp");
+ if (ts != null) {
+ row.timestamp = ts;
+ }
+ row.location = jo.getString("location");
+
+ // extract exception info
+ JsonObject ex = (JsonObject)
jo.get("exception");
+ if (ex != null) {
+ row.exceptionType =
ex.getString("type");
+ row.exceptionMessage =
ex.getString("message");
+ row.stackTrace = extractStackTrace(ex);
+ }
+
+ // extract message data
+ JsonObject msg = (JsonObject)
jo.get("message");
+ if (msg != null) {
+ row.body = msg.get("body") != null ?
msg.get("body").toString() : null;
+ row.headers = msg.get("headers");
+ }
+
+ // exchange properties and variables
+ row.properties =
jo.get("exchangeProperties");
+ row.variables =
jo.get("exchangeVariables");
+
+ // message history
+ Object mhObj = jo.get("messageHistory");
+ if (mhObj instanceof JsonArray mhArr) {
+ row.messageHistory = new
String[mhArr.size()];
+ for (int i = 0; i < mhArr.size(); i++)
{
+ row.messageHistory[i] =
mhArr.get(i).toString();
+ }
+ }
+
+ // apply client-side filters
+ if (matchesFilters(row)) {
+ rows.add(row);
+ }
+ }
+ }
+ }
+ }
+ });
+
+ // sort rows
+ rows.sort(this::sortRow);
+
+ // apply limit
+ List<Row> display = rows;
+ if (limit > 0 && rows.size() > limit) {
+ display = rows.subList(0, limit);
+ }
+
+ if (!display.isEmpty()) {
+ if (jsonOutput) {
+ printer().println(Jsoner.serialize(display.stream().map(r -> {
+ JsonObject jo = new JsonObject();
+ jo.put("pid", r.pid);
+ jo.put("name", r.name);
+ jo.put("age", getAge(r));
+ jo.put("route", r.routeId);
+ jo.put("nodeId", r.nodeId);
+ jo.put("exchangeId", r.exchangeId);
+ jo.put("handled", r.handled);
+ jo.put("exception", r.exceptionType);
+ jo.put("message", r.exceptionMessage);
+ return jo;
+ }).collect(Collectors.toList())));
+ } else {
+ printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS,
display, Arrays.asList(
+ new
Column().header("PID").headerAlign(HorizontalAlign.CENTER).with(r -> r.pid),
+ new
Column().header("NAME").dataAlign(HorizontalAlign.LEFT)
+ .maxWidth(30, OverflowBehaviour.ELLIPSIS_RIGHT)
+ .with(r -> r.name),
+ new
Column().header("AGO").dataAlign(HorizontalAlign.RIGHT)
+ .with(this::getAge),
+ new
Column().header("ROUTE").dataAlign(HorizontalAlign.LEFT)
+ .maxWidth(25, OverflowBehaviour.ELLIPSIS_RIGHT)
+ .with(r -> r.routeId),
+ new
Column().header("NODE").dataAlign(HorizontalAlign.LEFT)
+ .maxWidth(25, OverflowBehaviour.ELLIPSIS_RIGHT)
+ .with(r -> r.nodeId),
+ new
Column().header("HANDLED").dataAlign(HorizontalAlign.CENTER)
+ .with(r -> r.handled ? "true" : "false"),
+ new
Column().header("EXCEPTION").dataAlign(HorizontalAlign.LEFT)
+ .maxWidth(40, OverflowBehaviour.ELLIPSIS_RIGHT)
+ .with(r ->
shortExceptionType(r.exceptionType)),
+ new
Column().header("MESSAGE").dataAlign(HorizontalAlign.LEFT)
+ .maxWidth(60, OverflowBehaviour.ELLIPSIS_RIGHT)
+ .with(r -> r.exceptionMessage))));
+
+ // show detail sections
+ if (!showSet.isEmpty()) {
+ for (Row r : display) {
+ printer().println();
+ printer().printf(" Exchange: %s (route: %s, node:
%s)%n",
+ r.exchangeId, r.routeId, r.nodeId);
+ if (showSet.contains("body") && r.body != null) {
+ printer().printf(" Body:%n %s%n", r.body);
+ }
+ if (showSet.contains("headers") && r.headers != null) {
+ printer().printf(" Headers: %s%n", r.headers);
+ }
+ if (showSet.contains("properties") && r.properties !=
null) {
+ printer().printf(" Properties: %s%n",
r.properties);
+ }
+ if (showSet.contains("variables") && r.variables !=
null) {
+ printer().printf(" Variables: %s%n",
r.variables);
+ }
+ if (showSet.contains("history") && r.messageHistory !=
null) {
+ printer().printf(" Message History:%n");
+ for (String step : r.messageHistory) {
+ printer().printf(" %s%n", step);
+ }
+ }
+ if (showSet.contains("stackTrace") && r.stackTrace !=
null) {
+ printer().printf(" Stack Trace:%n");
+ for (String line : r.stackTrace) {
+ printer().printf(" %s%n", line);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ private boolean matchesFilters(Row row) {
+ if (route != null && !route.equals(row.routeId)) {
+ return false;
+ }
+ if (exception != null && (row.exceptionType == null
+ ||
!row.exceptionType.toLowerCase().contains(exception.toLowerCase()))) {
+ return false;
+ }
+ if (handled != null && !handled.equals(String.valueOf(row.handled))) {
+ return false;
+ }
+ if (ago != null) {
+ try {
+ long millis = TimeUtils.toMilliSeconds(ago);
+ long cutoff = System.currentTimeMillis() - millis;
+ if (row.timestamp < cutoff) {
+ return false;
+ }
+ } catch (Exception e) {
+ // ignore invalid ago value
+ }
+ }
+ return true;
+ }
+
+ private String getAge(Row r) {
+ if (r.timestamp > 0) {
+ return TimeUtils.printSince(r.timestamp);
+ }
+ return "";
+ }
+
+ private static String shortExceptionType(String type) {
+ if (type == null) {
+ return "";
+ }
+ int dot = type.lastIndexOf('.');
+ if (dot > 0) {
+ return type.substring(dot + 1);
+ }
+ return type;
+ }
+
+ private static String[] extractStackTrace(JsonObject ex) {
+ Object st = ex.get("stackTrace");
+ if (st instanceof JsonArray arr) {
+ String[] result = new String[arr.size()];
+ for (int i = 0; i < arr.size(); i++) {
+ result[i] = arr.get(i).toString();
+ }
+ return result;
+ }
+ return null;
+ }
+
+ protected int sortRow(Row o1, Row o2) {
+ String s = sort;
+ int negate = 1;
+ if (s.startsWith("-")) {
+ s = s.substring(1);
+ negate = -1;
+ }
+ switch (s) {
+ case "pid":
+ return Long.compare(Long.parseLong(o1.pid),
Long.parseLong(o2.pid)) * negate;
+ case "name":
+ return o1.name.compareToIgnoreCase(o2.name) * negate;
+ case "age":
+ return Long.compare(o1.timestamp, o2.timestamp) * negate;
+ default:
+ return 0;
+ }
+ }
+
+ private static class Row implements Cloneable {
+ String pid;
+ String name;
+ long timestamp;
+ String routeId;
+ String nodeId;
+ String exchangeId;
+ boolean handled;
+ String location;
+ String exceptionType;
+ String exceptionMessage;
+ String[] stackTrace;
+ String body;
+ Object headers;
+ Object properties;
+ Object variables;
+ String[] messageHistory;
+
+ Row copy() {
+ try {
+ return (Row) clone();
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+}