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