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


Reply via email to