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 aefcda906357 CAMEL-23533: ErrorRegistry - auto-enable in dev profile
(#23565)
aefcda906357 is described below
commit aefcda9063574e5491415d18a0f4540097bb30f9
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed May 27 18:06:43 2026 +0200
CAMEL-23533: ErrorRegistry - auto-enable in dev profile (#23565)
* CAMEL-23533: ErrorRegistry - auto-enable in dev profile
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23624: Add --last and --show=all to camel get error
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23533: ErrorRegistry - fix nodeId always null by extracting from
message history
The historyNodeId on the exchange extension is cleared after each node
finishes processing, so by the time the error event fires it is always
null. Instead, extract the node id and source location from the last
entry in the message history which correctly captures the failing node.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23533: ErrorRegistry - deduplicate errors by exchangeId
When an error is handled at multiple levels (e.g., circuit breaker
catches internally, then route error handler catches again), the same
exchangeId would appear multiple times. Now only the latest capture
is kept per exchangeId.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23533: ErrorRegistry - add exchange ID column to camel get error
Show the exchange ID in the default table output so users can correlate
errors with log entries and detect duplicates.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23533: ErrorRegistry - skip correlated copy exchanges
EIPs like circuit breaker, multicast, splitter, and recipient list
create correlated copies of the exchange for internal processing. These
copies have their own exchange ID and fire separate error events, causing
duplicate entries. For correlated copies, capture the error under the
original exchange ID and preserve the inner node info (e.g.,
throwException inside circuit breaker). For original exchanges, skip
capture if already recorded from a correlated copy since the copy has
more specific info about where the error actually occurred.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23533: ErrorRegistry - include exchange properties and variables in
JSON output
The asJSon() method was only outputting the message sub-object but
exchange properties and variables were nested inside it and not
promoted to the top-level JSON. Extract them from the message data
snapshot and include them at the top level so the CLI and dev console
can display them.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23533: ErrorRegistry - use MessageTableHelper for error details
display
Use MessageTableHelper (same as tracer/debugger) for --last/--show detail
output. Add --logging-color option. Simplify --json to dump raw error JSON.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23533: ErrorRegistry - fix null exchange pattern in error detail
display
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23533: ErrorRegistry - extract exchange pattern from message JSON
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23533: ErrorRegistry - add --id filter to camel get error
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23533: ErrorRegistry - add --detail option to camel get error
The --detail option shows full details (body, headers, properties,
variables, history, stackTrace) for each error entry. The --last
option implies --detail.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23533: ErrorRegistry - update bundled circuit-breaker example
Add variables, properties and headers to the route, and document
camel get error with --last and --id --detail options.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.../camel/impl/engine/DefaultErrorRegistry.java | 55 +++++++-
.../camel/impl/ErrorRegistryDeduplicateTest.java | 71 +++++++++++
.../org/apache/camel/impl/ErrorRegistryTest.java | 34 +++++
.../org/apache/camel/main/ProfileConfigurer.java | 2 +
.../jbang-commands/camel-jbang-get-error.adoc | 6 +-
.../META-INF/camel-jbang-commands-metadata.json | 2 +-
.../dsl/jbang/core/commands/process/ListError.java | 139 +++++++++++----------
.../resources/examples/circuit-breaker/README.md | 23 ++++
.../examples/circuit-breaker/route.camel.yaml | 25 +++-
9 files changed, 279 insertions(+), 78 deletions(-)
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java
index 19f88187c31a..50397280a8a3 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java
@@ -35,6 +35,7 @@ import org.apache.camel.spi.CamelEvent;
import org.apache.camel.spi.ErrorRegistry;
import org.apache.camel.spi.ErrorRegistryView;
import org.apache.camel.support.EventNotifierSupport;
+import org.apache.camel.support.LoggerHelper;
import org.apache.camel.support.MessageHelper;
import org.apache.camel.util.json.JsonObject;
import org.apache.camel.util.json.Jsonable;
@@ -94,6 +95,7 @@ public class DefaultErrorRegistry extends
EventNotifierSupport implements ErrorR
return !enabled;
}
+ @SuppressWarnings("unchecked")
private void capture(Exchange exchange, boolean handled) {
Throwable exception;
if (handled) {
@@ -105,9 +107,13 @@ public class DefaultErrorRegistry extends
EventNotifierSupport implements ErrorR
return;
}
+ // for correlated copy exchanges (e.g., created by circuit breaker,
multicast, splitter)
+ // use the original exchange ID so the error is tracked under the
parent exchange
+ String correlationId =
exchange.getProperty(ExchangePropertyKey.CORRELATION_ID, String.class);
+
long uid = uidCounter.incrementAndGet();
long timestamp = System.currentTimeMillis();
- String exchangeId = exchange.getExchangeId();
+ String exchangeId = correlationId != null ? correlationId :
exchange.getExchangeId();
String routeId =
exchange.getProperty(ExchangePropertyKey.FAILURE_ROUTE_ID, String.class);
if (routeId == null) {
routeId = exchange.getFromRouteId();
@@ -122,9 +128,20 @@ public class DefaultErrorRegistry extends
EventNotifierSupport implements ErrorR
}
String endpointUri =
exchange.getProperty(ExchangePropertyKey.FAILURE_ENDPOINT, String.class);
- // capture node location from exchange extension
- String toNode = exchange.getExchangeExtension().getHistoryNodeId();
- String location =
exchange.getExchangeExtension().getHistoryNodeSource();
+ // capture node id and location from the last message history entry
+ // (the historyNodeId on the exchange extension is cleared after the
node finishes processing,
+ // so by the time the error event fires it is always null)
+ String toNode = null;
+ String location = null;
+ List<MessageHistory> history
+ = exchange.getProperty(ExchangePropertyKey.MESSAGE_HISTORY,
List.class);
+ if (history != null && !history.isEmpty()) {
+ MessageHistory last = history.get(history.size() - 1);
+ if (last.getNode() != null) {
+ toNode = last.getNode().getId();
+ location =
LoggerHelper.getLineNumberLoggerName(last.getNode());
+ }
+ }
// capture step id (set by Step EIP)
String stepId = exchange.getProperty(ExchangePropertyKey.STEP_ID,
String.class);
@@ -164,6 +181,20 @@ public class DefaultErrorRegistry extends
EventNotifierSupport implements ErrorR
endpointUri, toNode, stepId, fromEndpointUri, routeUptime,
elapsed,
threadName, data, exception, handled, messageHistory);
+ // deduplicate by exchange ID:
+ // - correlated copy (inner): has more specific node info (e.g.,
throwException inside circuit breaker),
+ // so it replaces any existing entry for the same original exchange
+ // - original exchange (outer): if already captured from a correlated
copy, skip it
+ // since the copy has more specific info about where the error
actually occurred
+ if (correlationId != null) {
+ entries.removeIf(e -> exchangeId.equals(e.getExchangeId()));
+ } else {
+ for (BacklogErrorEventMessage e : entries) {
+ if (exchangeId.equals(e.getExchangeId())) {
+ return;
+ }
+ }
+ }
entries.addFirst(entry);
evict();
}
@@ -599,8 +630,20 @@ public class DefaultErrorRegistry extends
EventNotifierSupport implements ErrorR
jo.put("elapsed", elapsed);
jo.put("threadName", threadName);
jo.put("handled", handled);
- // message data
- jo.put("message", data.getMap("message"));
+ // message data (body, headers)
+ Map<String, Object> msg = data.getMap("message");
+ jo.put("message", msg);
+ // exchange properties and variables are inside the "message" data
snapshot
+ if (msg != null) {
+ Object props = msg.get("exchangeProperties");
+ if (props != null) {
+ jo.put("exchangeProperties", props);
+ }
+ Object vars = msg.get("exchangeVariables");
+ if (vars != null) {
+ jo.put("exchangeVariables", vars);
+ }
+ }
// exception
if (exception != null) {
try {
diff --git
a/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryDeduplicateTest.java
b/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryDeduplicateTest.java
new file mode 100644
index 000000000000..97fa3158b3b9
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryDeduplicateTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.impl;
+
+import java.util.Collection;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.BacklogErrorEventMessage;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ErrorRegistryDeduplicateTest extends ContextTestSupport {
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+ context.getErrorRegistry().setEnabled(true);
+ context.setMessageHistory(true);
+ return context;
+ }
+
+ @Test
+ public void testDeduplicatesSameExchangeId() throws Exception {
+ getMockEndpoint("mock:dead").expectedMessageCount(1);
+
+ template.sendBody("direct:start", "Hello");
+
+ assertMockEndpointsSatisfied();
+
+ Collection<BacklogErrorEventMessage> entries =
context.getErrorRegistry().browse();
+ assertEquals(1, entries.size(), "Same exchangeId should appear only
once in the error registry");
+ assertEquals(true, entries.iterator().next().isHandled());
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+
errorHandler(deadLetterChannel("mock:dead").maximumRedeliveries(0));
+
+ from("direct:start").routeId("dedup")
+ .to("direct:sub");
+
+ // sub-route throws exception which is handled by its own
onException
+ // firing ExchangeFailureHandledEvent, then the error
propagates to parent
+ // route's deadLetterChannel which fires another
ExchangeFailureHandledEvent
+ from("direct:sub").routeId("sub")
+
.errorHandler(deadLetterChannel("mock:dead").maximumRedeliveries(0))
+ .throwException(new IllegalArgumentException("Forced
error"));
+ }
+ };
+ }
+}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryTest.java
b/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryTest.java
index 864fe500b059..847ae8702a41 100644
--- a/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryTest.java
+++ b/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryTest.java
@@ -68,6 +68,7 @@ public class ErrorRegistryTest extends ContextTestSupport {
assertEquals("direct://start", entry.getFromEndpointUri());
assertTrue(entry.getRouteUptime() >= 0, "Route uptime should be
non-negative");
assertTrue(entry.getElapsed() >= 0, "Elapsed time should be
non-negative");
+ assertNotNull(entry.getToNode(), "Node id should be captured from
message history");
}
@Test
@@ -165,6 +166,7 @@ public class ErrorRegistryTest extends ContextTestSupport {
assertFalse(entry.isHandled());
assertEquals("java.lang.IllegalArgumentException",
entry.getExceptionType());
assertEquals("Unhandled error", entry.getExceptionMessage());
+ assertNotNull(entry.getToNode(), "Node id should be captured for
unhandled errors");
}
@Test
@@ -207,6 +209,30 @@ public class ErrorRegistryTest extends ContextTestSupport {
"Message JSON should contain body");
}
+ @Test
+ public void testErrorRegistryCapturesVariablesPropertiesHeaders() throws
Exception {
+ getMockEndpoint("mock:dead").expectedMessageCount(1);
+ template.sendBody("direct:withData", "Test Body");
+ assertMockEndpointsSatisfied();
+
+ BacklogErrorEventMessage entry =
context.getErrorRegistry().browse().iterator().next();
+
+ // verify in toJSon output
+ String json = entry.toJSon(2);
+ assertTrue(json.contains("myVar"), "JSON should contain variable
name");
+ assertTrue(json.contains("varValue"), "JSON should contain variable
value");
+ assertTrue(json.contains("myProp"), "JSON should contain property
name");
+ assertTrue(json.contains("propValue"), "JSON should contain property
value");
+ assertTrue(json.contains("myHeader"), "JSON should contain header
name");
+ assertTrue(json.contains("headerValue"), "JSON should contain header
value");
+
+ // verify in asJSon map
+ Map<String, Object> map = entry.asJSon();
+ assertNotNull(map.get("exchangeVariables"), "JSON map should contain
exchangeVariables");
+ assertNotNull(map.get("exchangeProperties"), "JSON map should contain
exchangeProperties");
+ assertNotNull(map.get("message"), "JSON map should contain message
with headers");
+ }
+
@Test
public void testErrorRegistryToJson() throws Exception {
getMockEndpoint("mock:dead").expectedMessageCount(1);
@@ -221,6 +247,7 @@ public class ErrorRegistryTest extends ContextTestSupport {
assertTrue(json.contains("\"handled\""));
assertTrue(json.contains("\"exception\""));
assertTrue(json.contains("\"message\""));
+ assertTrue(json.contains("\"nodeId\""), "JSON should contain nodeId");
}
@Test
@@ -237,6 +264,7 @@ public class ErrorRegistryTest extends ContextTestSupport {
assertNotNull(json.get("exchangeId"));
assertNotNull(json.get("exception"));
assertNotNull(json.get("message"));
+ assertNotNull(json.get("nodeId"), "JSON map should contain nodeId");
assertEquals("direct://start", json.get("fromEndpointUri"));
assertTrue((long) json.get("routeUptime") >= 0);
assertTrue((long) json.get("elapsed") >= 0);
@@ -264,6 +292,12 @@ public class ErrorRegistryTest extends ContextTestSupport {
from("direct:fail").routeId("failRoute")
.throwException(new IllegalArgumentException("Endpoint
error"));
+
+ from("direct:withData").routeId("dataRoute")
+ .setVariable("myVar", constant("varValue"))
+ .setProperty("myProp", constant("propValue"))
+ .setHeader("myHeader", constant("headerValue"))
+ .throwException(new IllegalArgumentException("Data
error"));
}
};
}
diff --git
a/core/camel-main/src/main/java/org/apache/camel/main/ProfileConfigurer.java
b/core/camel-main/src/main/java/org/apache/camel/main/ProfileConfigurer.java
index 27c753c6822b..e9d7873a2265 100644
--- a/core/camel-main/src/main/java/org/apache/camel/main/ProfileConfigurer.java
+++ b/core/camel-main/src/main/java/org/apache/camel/main/ProfileConfigurer.java
@@ -52,6 +52,8 @@ public class ProfileConfigurer {
if (!config.isTracing()) {
config.setTracingStandby(true);
}
+ // enable error registry to capture routing errors
+ config.errorRegistryConfig().withEnabled(true);
}
if ("dev".equals(profile)) {
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
index 7e2cdbf17c7d..ecd48c020df4 100644
---
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
@@ -20,12 +20,16 @@ camel get error [options]
|===
| Option | Description | Default | Type
| `--ago` | Filter by time window, e.g. 60s, 5m, 1h | | String
+| `--detail` | Show full details of each error entry | | boolean
| `--exception` | Filter by exception type (substring match) | | String
| `--handled` | Filter by handled status (true or false) | | String
+| `--id` | Filter by exchange ID | | String
| `--json` | Output in JSON Format | | boolean
+| `--last` | Show only the last (newest) error with full details | | boolean
| `--limit` | Maximum number of entries to display | | int
+| `--logging-color` | Use colored logging | true | boolean
| `--route` | Filter by route ID | | String
-| `--show` | Comma-separated detail sections to show: body, headers,
properties, variables, history, stackTrace | | String
+| `--show` | Comma-separated detail sections to show: body, headers,
properties, variables, history, stackTrace, or 'all' for all sections | |
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/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 da216b545ad7..d0d78976a693 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/process/ListError.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListError.java
index 96237a6276f8..881eb3115ffa 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListError.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListError.java
@@ -27,6 +27,7 @@ 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.commands.action.MessageTableHelper;
import org.apache.camel.dsl.jbang.core.common.PidNameAgeCompletionCandidates;
import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
import org.apache.camel.util.TimeUtils;
@@ -63,25 +64,56 @@ public class ListError extends ProcessWatchCommand {
description = "Filter by handled status (true or
false)")
String handled;
+ @CommandLine.Option(names = { "--id" },
+ description = "Filter by exchange ID")
+ String id;
+
@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")
+ description = "Comma-separated detail sections to
show: body, headers, properties, variables, history, stackTrace, or 'all' for
all sections")
String show;
+ @CommandLine.Option(names = { "--logging-color" }, defaultValue = "true",
description = "Use colored logging")
+ boolean loggingColor = true;
+
+ @CommandLine.Option(names = { "--detail" },
+ description = "Show full details of each error entry")
+ boolean detail;
+
+ @CommandLine.Option(names = { "--last" },
+ description = "Show only the last (newest) error with
full details")
+ boolean last;
+
public ListError(CamelJBangMain main) {
super(main);
}
+ private static final Set<String> ALL_SECTIONS
+ = Set.of("body", "headers", "properties", "variables", "history",
"stackTrace");
+
@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();
+ if (last) {
+ limit = 1;
+ detail = true;
+ }
+ if (detail) {
+ show = "all";
+ }
+
+ Set<String> showSet;
+ if ("all".equals(show)) {
+ showSet = ALL_SECTIONS;
+ } else if (show != null) {
+ showSet =
Arrays.stream(show.split(",")).map(String::trim).collect(Collectors.toSet());
+ } else {
+ showSet = Set.of();
+ }
List<Long> pids = findPids(name);
ProcessHandle.allProcesses()
@@ -117,26 +149,15 @@ public class ListError extends ProcessWatchCommand {
row.timestamp = ts;
}
row.location = jo.getString("location");
+ row.rawJson = jo;
// 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) {
@@ -167,19 +188,8 @@ public class ListError extends ProcessWatchCommand {
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())));
+ // dump the raw JSON from the error entries
+ printer().println(Jsoner.serialize(display.stream().map(r ->
r.rawJson).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),
@@ -188,6 +198,8 @@ public class ListError extends ProcessWatchCommand {
.with(r -> r.name),
new
Column().header("AGO").dataAlign(HorizontalAlign.RIGHT)
.with(this::getAge),
+ new
Column().header("ID").dataAlign(HorizontalAlign.LEFT)
+ .with(r -> r.exchangeId),
new
Column().header("ROUTE").dataAlign(HorizontalAlign.LEFT)
.maxWidth(25, OverflowBehaviour.ELLIPSIS_RIGHT)
.with(r -> r.routeId),
@@ -203,34 +215,44 @@ public class ListError extends ProcessWatchCommand {
.maxWidth(60, OverflowBehaviour.ELLIPSIS_RIGHT)
.with(r -> r.exceptionMessage))));
- // show detail sections
+ // show detail sections using MessageTableHelper
if (!showSet.isEmpty()) {
+ MessageTableHelper tableHelper = new MessageTableHelper();
+ tableHelper.setLoggingColor(loggingColor);
+ tableHelper.setPretty(true);
+
tableHelper.setShowExchangeProperties(showSet.contains("properties"));
+
tableHelper.setShowExchangeVariables(showSet.contains("variables"));
+ tableHelper.setShowHeaders(showSet.contains("headers") ||
showSet.contains("body"));
+
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);
+ // build the message root for MessageTableHelper
+ JsonObject msg = r.rawJson.getMap("message");
+ if (msg == null) {
+ msg = new JsonObject();
}
- if (showSet.contains("headers") && r.headers != null) {
- printer().printf(" Headers: %s%n", r.headers);
+ // promote exchange properties/variables into the
message root
+ Object ep = r.rawJson.get("exchangeProperties");
+ if (ep != null) {
+ msg.put("exchangeProperties", ep);
}
- if (showSet.contains("properties") && r.properties !=
null) {
- printer().printf(" Properties: %s%n",
r.properties);
+ Object ev = r.rawJson.get("exchangeVariables");
+ if (ev != null) {
+ msg.put("exchangeVariables", ev);
}
- if (showSet.contains("variables") && r.variables !=
null) {
- printer().printf(" Variables: %s%n",
r.variables);
+ String exchangePattern =
msg.getString("exchangePattern");
+ // exception
+ JsonObject cause = r.rawJson.getMap("exception");
+ String data = tableHelper.getDataAsTable(
+ r.exchangeId, exchangePattern, null, null,
null, msg, cause);
+ if (data != null && !data.isEmpty()) {
+ printer().print(data);
}
+ // message history
if (showSet.contains("history") && r.messageHistory !=
null) {
- printer().printf(" Message History:%n");
+ printer().println("History");
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);
+ printer().printf(" %s%n", step);
}
}
}
@@ -242,6 +264,9 @@ public class ListError extends ProcessWatchCommand {
}
private boolean matchesFilters(Row row) {
+ if (id != null && !id.equals(row.exchangeId)) {
+ return false;
+ }
if (route != null && !route.equals(row.routeId)) {
return false;
}
@@ -284,18 +309,6 @@ public class ListError extends ProcessWatchCommand {
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;
@@ -326,12 +339,8 @@ public class ListError extends ProcessWatchCommand {
String location;
String exceptionType;
String exceptionMessage;
- String[] stackTrace;
- String body;
- Object headers;
- Object properties;
- Object variables;
String[] messageHistory;
+ JsonObject rawJson;
Row copy() {
try {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/circuit-breaker/README.md
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/circuit-breaker/README.md
index 38a0209d9641..9bfb59c16757 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/circuit-breaker/README.md
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/circuit-breaker/README.md
@@ -22,3 +22,26 @@ how the state of the circuit breaker changes from closed to
open due to many fai
```sh
camel get circuit-breaker --watch
```
+
+## Inspecting errors
+
+Because the circuit breaker triggers exceptions, you can use `camel get error`
to inspect
+captured routing errors:
+
+```sh
+camel get error
+```
+
+This shows a summary table with PID, route, node, exchange ID, exception type
and message.
+
+To see full details of the last error (body, headers, variables, properties,
exception and message history):
+
+```sh
+camel get error --last
+```
+
+You can also pick a specific error by its exchange ID:
+
+```sh
+camel get error --id=<exchangeId> --detail
+```
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/circuit-breaker/route.camel.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/circuit-breaker/route.camel.yaml
index 7566bd348447..0ddd73d11b67 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/circuit-breaker/route.camel.yaml
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/circuit-breaker/route.camel.yaml
@@ -17,27 +17,42 @@
- route:
from:
- uri: timer:start
+ uri: timer
+ parameters:
+ timerName: start
+ period: 1000
steps:
- setBody:
expression:
constant:
expression: Hello Camel
+ - setVariable:
+ name: myCounter
+ expression:
+ simple:
+ expression: "${random(100)}"
+ - setProperty:
+ name: myRegion
+ expression:
+ constant:
+ expression: us-east-1
+ - setHeader:
+ name: myTraceId
+ expression:
+ simple:
+ expression: "trace-${exchangeId}"
- circuitBreaker:
resilience4jConfiguration:
minimumNumberOfCalls: 10
- failureRateThreshold: 50
waitDurationInOpenState: 20
steps:
- filter:
expression:
simple:
- expression: ${random(10)} > 2
+ expression: "${random(10)} > 2"
steps:
- throwException:
message: Forced error
exceptionType: java.lang.IllegalArgumentException
- log:
message: "${body} (CircuitBreaker is open:
${exchangeProperty.CamelCircuitBreakerResponseShortCircuited})"
- parameters:
- period: 1000