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);
+            }
+        }
+    }
+}

Reply via email to