This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch CAMEL-23624
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 2b1c4a99f0df475f3dfce8c83dcc3a3f6e3833c1
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed May 27 12:18:15 2026 +0200

    CAMEL-23624: Add camel get error CLI command
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../camel/impl/console/ErrorRegistryConsole.java   |  81 ++++-
 .../camel/cli/connector/LocalCliConnector.java     |   8 +
 .../dsl/jbang/core/commands/CamelJBangMain.java    |   1 +
 .../dsl/jbang/core/commands/process/ListError.java | 344 +++++++++++++++++++++
 4 files changed, 418 insertions(+), 16 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/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/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