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 1d1d2ed138c9 CAMEL-26698: camel-console - the redeliveries and the 
failures handled of routes and processors, shown on the Routes tab (#29357)
1d1d2ed138c9 is described below

commit 1d1d2ed138c9a316068c4fe8ae46726ce339313a
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Nov 6 21:58:55 2026 +0100

    CAMEL-26698: camel-console - the redeliveries and the failures handled of 
routes and processors, shown on the Routes tab (#29357)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../apache/camel/catalog/dev-consoles-openapi.json |  45 ++++++++
 .../camel/catalog/dev-consoles/processor.json      |  15 +++
 .../apache/camel/catalog/dev-consoles/route.json   |  30 ++++++
 .../org/apache/camel/dev-console/processor.json    |  15 +++
 .../org/apache/camel/dev-console/route.json        |  30 ++++++
 .../camel/impl/console/ProcessorDevConsole.java    |   9 +-
 .../apache/camel/impl/console/RouteDevConsole.java |   7 ++
 .../console/RouteDevConsoleRedeliveryTest.java     | 118 ++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/ProcessorInfo.java |   3 +
 .../dsl/jbang/core/commands/tui/RouteInfo.java     |   3 +
 .../dsl/jbang/core/commands/tui/RoutesTab.java     |  77 +++++++++----
 .../dsl/jbang/core/commands/tui/StatusParser.java  |   4 +
 .../src/main/resources/tui/help/routes.md          |   2 +
 .../tui/RoutesErrorHandlerColumnsTest.java         | 119 +++++++++++++++++++++
 14 files changed, 455 insertions(+), 22 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
index cbb6845ec89b..217ff636f9c1 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
@@ -5089,6 +5089,18 @@
                                                                                
                                                "type": "integer",
                                                                                
                                                "description": "Number of 
inflight exchanges"
                                                                                
                                        },
+                                                                               
                                        "failuresHandled": {
+                                                                               
                                                "type": "integer",
+                                                                               
                                                "description": "Number of 
exchanges whose failure was handled by the error handler, such as moved to a 
dead letter channel"
+                                                                               
                                        },
+                                                                               
                                        "redeliveries": {
+                                                                               
                                                "type": "integer",
+                                                                               
                                                "description": "Number of 
redeliveries by the error handler of Camel (internal only)"
+                                                                               
                                        },
+                                                                               
                                        "externalRedeliveries": {
+                                                                               
                                                "type": "integer",
+                                                                               
                                                "description": "Number of 
redeliveries initiated from outside, such as by a JMS broker"
+                                                                               
                                        },
                                                                                
                                        "exchangesThroughput": {
                                                                                
                                                "type": "string",
                                                                                
                                                "description": "Messages per 
second throughput (only present when available)"
@@ -5147,6 +5159,9 @@
                                                                                
                                        "exchangesTotal",
                                                                                
                                        "exchangesFailed",
                                                                                
                                        "exchangesInflight",
+                                                                               
                                        "failuresHandled",
+                                                                               
                                        "redeliveries",
+                                                                               
                                        "externalRedeliveries",
                                                                                
                                        "meanProcessingTime",
                                                                                
                                        "maxProcessingTime",
                                                                                
                                        "minProcessingTime"
@@ -6262,6 +6277,18 @@
                                                                                
                                                "type": "integer",
                                                                                
                                                "description": "Number of 
inflight exchanges"
                                                                                
                                        },
+                                                                               
                                        "failuresHandled": {
+                                                                               
                                                "type": "integer",
+                                                                               
                                                "description": "Number of 
exchanges whose failure was handled by the error handler, such as moved to a 
dead letter channel"
+                                                                               
                                        },
+                                                                               
                                        "redeliveries": {
+                                                                               
                                                "type": "integer",
+                                                                               
                                                "description": "Number of 
redeliveries by the error handler of Camel (internal only)"
+                                                                               
                                        },
+                                                                               
                                        "externalRedeliveries": {
+                                                                               
                                                "type": "integer",
+                                                                               
                                                "description": "Number of 
redeliveries initiated from outside, such as by a JMS broker"
+                                                                               
                                        },
                                                                                
                                        "remoteExchangesTotal": {
                                                                                
                                                "type": "integer",
                                                                                
                                                "description": "Total number of 
exchanges that came from a remote endpoint"
@@ -6328,6 +6355,9 @@
                                                                                
                                        "exchangesTotal",
                                                                                
                                        "exchangesFailed",
                                                                                
                                        "exchangesInflight",
+                                                                               
                                        "failuresHandled",
+                                                                               
                                        "redeliveries",
+                                                                               
                                        "externalRedeliveries",
                                                                                
                                        "remoteExchangesTotal",
                                                                                
                                        "remoteExchangesFailed",
                                                                                
                                        "remoteExchangesInflight",
@@ -6444,6 +6474,18 @@
                                                                                
                                                                        "type": 
"integer",
                                                                                
                                                                        
"description": "Number of inflight exchanges"
                                                                                
                                                                },
+                                                                               
                                                                
"failuresHandled": {
+                                                                               
                                                                        "type": 
"integer",
+                                                                               
                                                                        
"description": "Number of exchanges whose failure was handled by the error 
handler, such as moved to a dead letter channel"
+                                                                               
                                                                },
+                                                                               
                                                                "redeliveries": 
{
+                                                                               
                                                                        "type": 
"integer",
+                                                                               
                                                                        
"description": "Number of redeliveries by the error handler of Camel (internal 
only)"
+                                                                               
                                                                },
+                                                                               
                                                                
"externalRedeliveries": {
+                                                                               
                                                                        "type": 
"integer",
+                                                                               
                                                                        
"description": "Number of redeliveries initiated from outside, such as by a JMS 
broker"
+                                                                               
                                                                },
                                                                                
                                                                
"exchangesThroughput": {
                                                                                
                                                                        "type": 
"string",
                                                                                
                                                                        
"description": "Messages per second throughput (only present when available)"
@@ -6502,6 +6544,9 @@
                                                                                
                                                                
"exchangesTotal",
                                                                                
                                                                
"exchangesFailed",
                                                                                
                                                                
"exchangesInflight",
+                                                                               
                                                                
"failuresHandled",
+                                                                               
                                                                "redeliveries",
+                                                                               
                                                                
"externalRedeliveries",
                                                                                
                                                                
"meanProcessingTime",
                                                                                
                                                                
"maxProcessingTime",
                                                                                
                                                                
"minProcessingTime"
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/processor.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/processor.json
index 1f51c80a6478..1e11ee29ea4a 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/processor.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/processor.json
@@ -172,6 +172,18 @@
                   "type": "integer",
                   "description": "Number of inflight exchanges"
                 },
+                "failuresHandled": {
+                  "type": "integer",
+                  "description": "Number of exchanges whose failure was 
handled by the error handler, such as moved to a dead letter channel"
+                },
+                "redeliveries": {
+                  "type": "integer",
+                  "description": "Number of redeliveries by the error handler 
of Camel (internal only)"
+                },
+                "externalRedeliveries": {
+                  "type": "integer",
+                  "description": "Number of redeliveries initiated from 
outside, such as by a JMS broker"
+                },
                 "exchangesThroughput": {
                   "type": "string",
                   "description": "Messages per second throughput (only present 
when available)"
@@ -230,6 +242,9 @@
                 "exchangesTotal",
                 "exchangesFailed",
                 "exchangesInflight",
+                "failuresHandled",
+                "redeliveries",
+                "externalRedeliveries",
                 "meanProcessingTime",
                 "maxProcessingTime",
                 "minProcessingTime"
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route.json
index b92777039028..0802d135d9bb 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route.json
@@ -228,6 +228,18 @@
                   "type": "integer",
                   "description": "Number of inflight exchanges"
                 },
+                "failuresHandled": {
+                  "type": "integer",
+                  "description": "Number of exchanges whose failure was 
handled by the error handler, such as moved to a dead letter channel"
+                },
+                "redeliveries": {
+                  "type": "integer",
+                  "description": "Number of redeliveries by the error handler 
of Camel (internal only)"
+                },
+                "externalRedeliveries": {
+                  "type": "integer",
+                  "description": "Number of redeliveries initiated from 
outside, such as by a JMS broker"
+                },
                 "remoteExchangesTotal": {
                   "type": "integer",
                   "description": "Total number of exchanges that came from a 
remote endpoint"
@@ -294,6 +306,9 @@
                 "exchangesTotal",
                 "exchangesFailed",
                 "exchangesInflight",
+                "failuresHandled",
+                "redeliveries",
+                "externalRedeliveries",
                 "remoteExchangesTotal",
                 "remoteExchangesFailed",
                 "remoteExchangesInflight",
@@ -410,6 +425,18 @@
                         "type": "integer",
                         "description": "Number of inflight exchanges"
                       },
+                      "failuresHandled": {
+                        "type": "integer",
+                        "description": "Number of exchanges whose failure was 
handled by the error handler, such as moved to a dead letter channel"
+                      },
+                      "redeliveries": {
+                        "type": "integer",
+                        "description": "Number of redeliveries by the error 
handler of Camel (internal only)"
+                      },
+                      "externalRedeliveries": {
+                        "type": "integer",
+                        "description": "Number of redeliveries initiated from 
outside, such as by a JMS broker"
+                      },
                       "exchangesThroughput": {
                         "type": "string",
                         "description": "Messages per second throughput (only 
present when available)"
@@ -468,6 +495,9 @@
                       "exchangesTotal",
                       "exchangesFailed",
                       "exchangesInflight",
+                      "failuresHandled",
+                      "redeliveries",
+                      "externalRedeliveries",
                       "meanProcessingTime",
                       "maxProcessingTime",
                       "minProcessingTime"
diff --git 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/processor.json
 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/processor.json
index 1f51c80a6478..1e11ee29ea4a 100644
--- 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/processor.json
+++ 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/processor.json
@@ -172,6 +172,18 @@
                   "type": "integer",
                   "description": "Number of inflight exchanges"
                 },
+                "failuresHandled": {
+                  "type": "integer",
+                  "description": "Number of exchanges whose failure was 
handled by the error handler, such as moved to a dead letter channel"
+                },
+                "redeliveries": {
+                  "type": "integer",
+                  "description": "Number of redeliveries by the error handler 
of Camel (internal only)"
+                },
+                "externalRedeliveries": {
+                  "type": "integer",
+                  "description": "Number of redeliveries initiated from 
outside, such as by a JMS broker"
+                },
                 "exchangesThroughput": {
                   "type": "string",
                   "description": "Messages per second throughput (only present 
when available)"
@@ -230,6 +242,9 @@
                 "exchangesTotal",
                 "exchangesFailed",
                 "exchangesInflight",
+                "failuresHandled",
+                "redeliveries",
+                "externalRedeliveries",
                 "meanProcessingTime",
                 "maxProcessingTime",
                 "minProcessingTime"
diff --git 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route.json
 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route.json
index b92777039028..0802d135d9bb 100644
--- 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route.json
+++ 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route.json
@@ -228,6 +228,18 @@
                   "type": "integer",
                   "description": "Number of inflight exchanges"
                 },
+                "failuresHandled": {
+                  "type": "integer",
+                  "description": "Number of exchanges whose failure was 
handled by the error handler, such as moved to a dead letter channel"
+                },
+                "redeliveries": {
+                  "type": "integer",
+                  "description": "Number of redeliveries by the error handler 
of Camel (internal only)"
+                },
+                "externalRedeliveries": {
+                  "type": "integer",
+                  "description": "Number of redeliveries initiated from 
outside, such as by a JMS broker"
+                },
                 "remoteExchangesTotal": {
                   "type": "integer",
                   "description": "Total number of exchanges that came from a 
remote endpoint"
@@ -294,6 +306,9 @@
                 "exchangesTotal",
                 "exchangesFailed",
                 "exchangesInflight",
+                "failuresHandled",
+                "redeliveries",
+                "externalRedeliveries",
                 "remoteExchangesTotal",
                 "remoteExchangesFailed",
                 "remoteExchangesInflight",
@@ -410,6 +425,18 @@
                         "type": "integer",
                         "description": "Number of inflight exchanges"
                       },
+                      "failuresHandled": {
+                        "type": "integer",
+                        "description": "Number of exchanges whose failure was 
handled by the error handler, such as moved to a dead letter channel"
+                      },
+                      "redeliveries": {
+                        "type": "integer",
+                        "description": "Number of redeliveries by the error 
handler of Camel (internal only)"
+                      },
+                      "externalRedeliveries": {
+                        "type": "integer",
+                        "description": "Number of redeliveries initiated from 
outside, such as by a JMS broker"
+                      },
                       "exchangesThroughput": {
                         "type": "string",
                         "description": "Messages per second throughput (only 
present when available)"
@@ -468,6 +495,9 @@
                       "exchangesTotal",
                       "exchangesFailed",
                       "exchangesInflight",
+                      "failuresHandled",
+                      "redeliveries",
+                      "externalRedeliveries",
                       "meanProcessingTime",
                       "maxProcessingTime",
                       "minProcessingTime"
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/ProcessorDevConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/ProcessorDevConsole.java
index 9869297d53da..ee6382fc901f 100644
--- 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/ProcessorDevConsole.java
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/ProcessorDevConsole.java
@@ -59,6 +59,9 @@ public class ProcessorDevConsole extends AbstractDevConsole {
             @Metadata(description = "Total number of exchanges") long 
exchangesTotal,
             @Metadata(description = "Number of failed exchanges") long 
exchangesFailed,
             @Metadata(description = "Number of inflight exchanges") long 
exchangesInflight,
+            @Metadata(description = "Number of exchanges whose failure was 
handled by the error handler, such as moved to a dead letter channel") long 
failuresHandled,
+            @Metadata(description = "Number of redeliveries by the error 
handler of Camel (internal only)") long redeliveries,
+            @Metadata(description = "Number of redeliveries initiated from 
outside, such as by a JMS broker") long externalRedeliveries,
             @Metadata(description = "Messages per second throughput (only 
present when available)") String exchangesThroughput,
             @Metadata(description = "Mean processing time in milliseconds") 
long meanProcessingTime,
             @Metadata(description = "Max processing time in milliseconds") 
long maxProcessingTime,
@@ -208,6 +211,9 @@ public class ProcessorDevConsole extends AbstractDevConsole 
{
             sb.append(String.format("%n        Total: %s", 
mp.getExchangesTotal()));
             sb.append(String.format("%n        Failed: %s", 
mp.getExchangesFailed()));
             sb.append(String.format("%n        Inflight: %s", 
mp.getExchangesInflight()));
+            sb.append(String.format("%n        Failures Handled: %s", 
mp.getFailuresHandled()));
+            sb.append(String.format("%n        Redeliveries: %s", 
mp.getRedeliveries()));
+            sb.append(String.format("%n        External Redeliveries: %s", 
mp.getExternalRedeliveries()));
             long idle = mp.getIdleSince();
             if (idle > 0) {
                 sb.append(String.format("%n        Idle Since: %s", 
TimeUtils.printDuration(idle)));
@@ -403,7 +409,8 @@ public class ProcessorDevConsole extends AbstractDevConsole 
{
         Long lastFailed = timestampOf(mp.getLastExchangeFailureTimestamp());
 
         return new Statistics(
-                mp.getIdleSince(), mp.getExchangesTotal(), 
mp.getExchangesFailed(), mp.getExchangesInflight(), thp,
+                mp.getIdleSince(), mp.getExchangesTotal(), 
mp.getExchangesFailed(), mp.getExchangesInflight(),
+                mp.getFailuresHandled(), mp.getRedeliveries(), 
mp.getExternalRedeliveries(), thp,
                 mp.getMeanProcessingTime(), mp.getMaxProcessingTime(), 
mp.getMinProcessingTime(),
                 p50, p95, p99, lastProcessingTime, deltaProcessingTime,
                 lastCreated, lastCompleted, lastFailureHandled, lastFailed);
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDevConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDevConsole.java
index eb956478637c..a43301fcb7f5 100644
--- 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDevConsole.java
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDevConsole.java
@@ -67,6 +67,9 @@ public class RouteDevConsole extends AbstractDevConsole {
             @Metadata(description = "Total number of exchanges") long 
exchangesTotal,
             @Metadata(description = "Number of failed exchanges") long 
exchangesFailed,
             @Metadata(description = "Number of inflight exchanges") long 
exchangesInflight,
+            @Metadata(description = "Number of exchanges whose failure was 
handled by the error handler, such as moved to a dead letter channel") long 
failuresHandled,
+            @Metadata(description = "Number of redeliveries by the error 
handler of Camel (internal only)") long redeliveries,
+            @Metadata(description = "Number of redeliveries initiated from 
outside, such as by a JMS broker") long externalRedeliveries,
             @Metadata(description = "Total number of exchanges that came from 
a remote endpoint") long remoteExchangesTotal,
             @Metadata(description = "Number of failed exchanges that came from 
a remote endpoint") long remoteExchangesFailed,
             @Metadata(description = "Number of inflight exchanges that came 
from a remote endpoint") long remoteExchangesInflight,
@@ -216,6 +219,9 @@ public class RouteDevConsole extends AbstractDevConsole {
             sb.append(String.format("%n    Total: %s", 
mrb.getExchangesTotal()));
             sb.append(String.format("%n    Failed: %s", 
mrb.getExchangesFailed()));
             sb.append(String.format("%n    Inflight: %s", 
mrb.getExchangesInflight()));
+            sb.append(String.format("%n    Failures Handled: %s", 
mrb.getFailuresHandled()));
+            sb.append(String.format("%n    Redeliveries: %s", 
mrb.getRedeliveries()));
+            sb.append(String.format("%n    External Redeliveries: %s", 
mrb.getExternalRedeliveries()));
             long idle = mrb.getIdleSince();
             if (idle > 0) {
                 sb.append(String.format("%n    Idle Since: %s", 
TimeUtils.printDuration(idle)));
@@ -560,6 +566,7 @@ public class RouteDevConsole extends AbstractDevConsole {
                 coverage, hasLoad ? load1 : null, hasLoad ? load5 : null, 
hasLoad ? load15 : null,
                 thp.isEmpty() ? null : thp,
                 mrb.getIdleSince(), mrb.getExchangesTotal(), 
mrb.getExchangesFailed(), mrb.getExchangesInflight(),
+                mrb.getFailuresHandled(), mrb.getRedeliveries(), 
mrb.getExternalRedeliveries(),
                 mrb.getRemoteExchangesTotal(), mrb.getRemoteExchangesFailed(), 
mrb.getRemoteExchangesInflight(),
                 mrb.getMeanProcessingTime(), mrb.getMaxProcessingTime(), 
mrb.getMinProcessingTime(),
                 p50, p95, p99, lastProcessingTime, deltaProcessingTime,
diff --git 
a/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteDevConsoleRedeliveryTest.java
 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteDevConsoleRedeliveryTest.java
new file mode 100644
index 000000000000..16c70dcb9554
--- /dev/null
+++ 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteDevConsoleRedeliveryTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.console;
+
+import java.util.Map;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.console.DevConsole;
+import org.apache.camel.support.PluginHelper;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * CAMEL-26698: the route console has the redeliveries and the failures 
handled of the routes and their processors, so
+ * an exchange that failed and then succeeded on redelivery leaves a trace.
+ */
+class RouteDevConsoleRedeliveryTest extends ContextTestSupport {
+
+    @Override
+    protected boolean useJmx() {
+        // the route console reads the statistics from JMX
+        return true;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                
errorHandler(deadLetterChannel("mock:dead").maximumRedeliveries(2).redeliveryDelay(0));
+
+                // fails the first attempt of each exchange, so the redelivery 
succeeds
+                from("direct:flaky").routeId("flakyRoute")
+                        .process(e -> {
+                            if 
(e.getMessage().getHeader(Exchange.REDELIVERY_COUNTER, 0, Integer.class) == 0) {
+                                throw new IllegalStateException("first 
attempt");
+                            }
+                        }).id("flaky")
+                        .to("mock:result").id("result");
+
+                // fails every attempt, so the dead letter channel handles it
+                from("direct:broken").routeId("brokenRoute")
+                        .throwException(new 
IllegalStateException("always")).id("broken");
+            }
+        };
+    }
+
+    @Test
+    void testRedeliveriesAndFailuresHandled() {
+        template.sendBody("direct:flaky", "a");
+        template.sendBody("direct:flaky", "b");
+        template.sendBody("direct:broken", "c");
+
+        // the route counts the exchanges that completed after a redelivery, 
the processor each redelivery
+        JsonObject flaky = route("flakyRoute");
+        JsonObject stats = flaky.getMap("statistics");
+        Assertions.assertEquals(2, stats.getLong("redeliveries"));
+        Assertions.assertEquals(0, stats.getLong("failuresHandled"));
+        Assertions.assertEquals(0, stats.getLong("exchangesFailed"));
+        Assertions.assertEquals(0, stats.getLong("externalRedeliveries"));
+        // the redeliveries are counted at the step that was retried, not at 
the next one
+        Assertions.assertEquals(2, processor(flaky, 
"flaky").getLong("redeliveries"));
+        Assertions.assertEquals(0, processor(flaky, 
"result").getLong("redeliveries"));
+
+        JsonObject broken = route("brokenRoute");
+        stats = broken.getMap("statistics");
+        Assertions.assertEquals(1, stats.getLong("failuresHandled"));
+        Assertions.assertEquals(0, stats.getLong("exchangesFailed"));
+        JsonObject step = processor(broken, "broken");
+        Assertions.assertEquals(3, step.getLong("exchangesFailed"));
+        Assertions.assertEquals(2, step.getLong("redeliveries"));
+
+        String text = (String) console().call(DevConsole.MediaType.TEXT,
+                Map.of(RouteDevConsole.FILTER, "brokenRoute", 
RouteDevConsole.PROCESSORS, "true"));
+        Assertions.assertTrue(text.contains("Failures Handled: 1"), text);
+        // the processor of the route, indented
+        Assertions.assertTrue(text.contains("        Redeliveries: 2"), text);
+        Assertions.assertTrue(text.contains("External Redeliveries: 0"), text);
+    }
+
+    private JsonObject route(String routeId) {
+        JsonObject out = (JsonObject) console().call(DevConsole.MediaType.JSON,
+                Map.of(RouteDevConsole.FILTER, routeId, 
RouteDevConsole.PROCESSORS, "true"));
+        return (JsonObject) ((JsonArray) out.getCollection("routes")).get(0);
+    }
+
+    private static JsonObject processor(JsonObject route, String id) {
+        for (Object o : route.getCollection("processors")) {
+            JsonObject p = (JsonObject) o;
+            if (id.equals(p.getString("id"))) {
+                return p.getMap("statistics");
+            }
+        }
+        throw new AssertionError("No processor " + id + " in " + route);
+    }
+
+    private DevConsole console() {
+        return 
PluginHelper.getDevConsoleResolver(context).resolveDevConsole("route");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessorInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessorInfo.java
index 926ac8c30f91..71f37f3d23c9 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessorInfo.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessorInfo.java
@@ -29,6 +29,9 @@ class ProcessorInfo {
     int level;
     long total;
     long failed;
+    // the failures the error handler handled, and its redeliveries 
(CAMEL-26698)
+    long failuresHandled;
+    long redeliveries;
     long meanTime;
     long minTime;
     long maxTime;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
index d9649e3a83bd..19060c05fcdc 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
@@ -34,6 +34,9 @@ class RouteInfo {
     String coverage;
     long total;
     long failed;
+    // the failures the error handler handled, and its redeliveries 
(CAMEL-26698)
+    long failuresHandled;
+    long redeliveries;
     long inflight;
     long meanTime;
     long minTime;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
index 9cfcc7dd5a90..b533dc6c4025 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
@@ -1027,9 +1027,13 @@ class RoutesTab extends AbstractDiagramTab {
         int lineWidth = lineWidth(route);
         // PROCESSOR and BODY TYPES share what the borders, the gaps and the 
other columns leave: what a step does comes
         // first, the body types get the rest up to their width (the info 
panel of the diagram has them all)
-        int columns = 8 + (lineWidth > 0 ? 1 : 0) + (bodyTypes ? 1 : 0);
+        // REDEL and HAND only once the error handler redelivered or handled 
an exchange of the route
+        boolean errorHandler = route.redeliveries > 0 || route.failuresHandled 
> 0
+                || route.processors.stream().anyMatch(p -> p.redeliveries > 0 
|| p.failuresHandled > 0);
+        int columns = 8 + (lineWidth > 0 ? 1 : 0) + (bodyTypes ? 1 : 0) + 
(errorHandler ? 2 : 0);
         int rest = Math.max(0,
-                area.width() - 2 - (columns - 1) - (typeWidth + lineWidth + 10 
+ 8 + 6 + 8 + 20 + 12));
+                area.width() - 2 - (columns - 1)
+                               - (typeWidth + lineWidth + 10 + 8 + 6 + 8 + 20 
+ 12 + (errorHandler ? 12 : 0)));
         if (bodyTypes) {
             bodyColWidth = Math.max(CharWidth.of(" BODY TYPES"), 
Math.min(bodyColWidth, rest - MIN_PROCESSOR_WIDTH));
         }
@@ -1061,8 +1065,11 @@ class RoutesTab extends AbstractDiagramTab {
                 rightCell(formatThroughput(route.throughput), 8),
                 rightCell(String.valueOf(route.total), 8),
                 rightCell(String.valueOf(route.failed), 6,
-                        route.failed > 0 ? Theme.error() : Style.EMPTY),
-                rightCell(String.valueOf(route.inflight), 8)));
+                        route.failed > 0 ? Theme.error() : Style.EMPTY)));
+        if (errorHandler) {
+            routeCells.addAll(errorHandlerCells(route.redeliveries, 
route.failuresHandled));
+        }
+        routeCells.add(rightCell(String.valueOf(route.inflight), 8));
         if (bodyTypes) {
             routeCells.add(Cell.from(""));
         }
@@ -1094,8 +1101,11 @@ class RoutesTab extends AbstractDiagramTab {
                     rightCell(formatThroughput(proc.throughput), 8),
                     rightCell(String.valueOf(proc.total), 8),
                     rightCell(String.valueOf(proc.failed), 6,
-                            proc.failed > 0 ? Theme.error() : Style.EMPTY),
-                    rightCell(String.valueOf(proc.inflight), 8)));
+                            proc.failed > 0 ? Theme.error() : Style.EMPTY)));
+            if (errorHandler) {
+                cells.addAll(errorHandlerCells(proc.redeliveries, 
proc.failuresHandled));
+            }
+            cells.add(rightCell(String.valueOf(proc.inflight), 8));
             if (bodyTypes) {
                 cells.add(Cell.from(Span.styled(
                         " " + 
TuiHelper.truncate(BodyTypes.shortLabel(proc.bodyTypes, proc.bodyKinds), 
bodyColWidth - 1),
@@ -1105,10 +1115,27 @@ class RoutesTab extends AbstractDiagramTab {
             cells.add(Cell.from(buildPercentileBarLine(proc.p50Time, 
proc.p95Time, proc.p99Time, 10)));
             rows.add(Row.from(cells));
         }
-        String procTimingHeader = hasProcPercentiles ? "P50/P95/P99" : 
"MIN/MAX/MEAN";
-
         List<Cell> header = new ArrayList<>();
         List<Constraint> widths = new ArrayList<>();
+        processorHeader(header, widths, typeWidth, lineWidth, errorHandler, 
bodyTypes ? bodyColWidth : 0,
+                hasProcPercentiles);
+
+        return Table.builder()
+                .rows(rows)
+                .header(Row.from(header))
+                .widths(widths.toArray(Constraint[]::new))
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(" Processors [" + route.routeId + "] ")
+                        .build())
+                .build();
+    }
+
+    /** The header of the processor table and the widths of its columns. */
+    private void processorHeader(
+            List<Cell> header, List<Constraint> widths, int typeWidth, int 
lineWidth, boolean errorHandler,
+            int bodyColWidth, boolean hasProcPercentiles) {
+        String procTimingHeader = hasProcPercentiles ? "P50/P95/P99" : 
"MIN/MAX/MEAN";
+
         header.add(Cell.from(Span.styled("   TYPE", Style.EMPTY.bold())));
         widths.add(Constraint.length(typeWidth));
         if (lineWidth > 0) {
@@ -1119,15 +1146,21 @@ class RoutesTab extends AbstractDiagramTab {
                 Cell.from(Span.styled("PROCESSOR", Style.EMPTY.bold())),
                 rightCell(ctx.ratePerMinute ? "MSG/M" : "MSG/S", 8, 
Style.EMPTY.bold()),
                 rightCell("TOTAL", 8, Style.EMPTY.bold()),
-                rightCell("FAIL", 6, Style.EMPTY.bold()),
-                rightCell("INFLIGHT", 8, Style.EMPTY.bold())));
+                rightCell("FAIL", 6, Style.EMPTY.bold())));
         widths.addAll(List.of(
                 Constraint.fill(),
                 Constraint.length(10),
                 Constraint.length(8),
-                Constraint.length(6),
-                Constraint.length(8)));
-        if (bodyTypes) {
+                Constraint.length(6)));
+        if (errorHandler) {
+            header.add(rightCell("REDEL", 6, Style.EMPTY.bold()));
+            header.add(rightCell("HAND", 6, Style.EMPTY.bold()));
+            widths.add(Constraint.length(6));
+            widths.add(Constraint.length(6));
+        }
+        header.add(rightCell("INFLIGHT", 8, Style.EMPTY.bold()));
+        widths.add(Constraint.length(8));
+        if (bodyColWidth > 0) {
             header.add(Cell.from(Span.styled(" BODY TYPES", 
Style.EMPTY.bold())));
             widths.add(Constraint.length(bodyColWidth));
         }
@@ -1135,15 +1168,15 @@ class RoutesTab extends AbstractDiagramTab {
         header.add(Cell.from(""));
         widths.add(Constraint.length(20));
         widths.add(Constraint.length(12));
+    }
 
-        return Table.builder()
-                .rows(rows)
-                .header(Row.from(header))
-                .widths(widths.toArray(Constraint[]::new))
-                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
-                        .title(" Processors [" + route.routeId + "] ")
-                        .build())
-                .build();
+    /** The redeliveries and the failures handled by the error handler: a step 
that failed and was retried. */
+    private static List<Cell> errorHandlerCells(long redeliveries, long 
failuresHandled) {
+        return List.of(
+                rightCell(redeliveries > 0 ? String.valueOf(redeliveries) : 
"", 6,
+                        redeliveries > 0 ? Theme.warning() : Style.EMPTY),
+                rightCell(failuresHandled > 0 ? 
String.valueOf(failuresHandled) : "", 6,
+                        failuresHandled > 0 ? Theme.warning() : Style.EMPTY));
     }
 
     /** The type of a step, indented by its level: the tree of the steps of 
the route. */
@@ -1422,6 +1455,8 @@ class RoutesTab extends AbstractDiagramTab {
             row.put("uptime", ri.uptime);
             row.put("total", ri.total);
             row.put("failed", ri.failed);
+            row.put("failuresHandled", ri.failuresHandled);
+            row.put("redeliveries", ri.redeliveries);
             row.put("inflight", ri.inflight);
             row.put("mean", ri.meanTime);
             row.put("max", ri.maxTime);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
index a5e2ab416314..35e6a38654e7 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
@@ -273,6 +273,8 @@ final class StatusParser {
                     ri.throughput = objToString(rs.get("exchangesThroughput"));
                     ri.total = objToLong(rs.get("exchangesTotal"));
                     ri.failed = objToLong(rs.get("exchangesFailed"));
+                    ri.failuresHandled = objToLong(rs.get("failuresHandled"));
+                    ri.redeliveries = objToLong(rs.get("redeliveries"));
                     ri.inflight = objToLong(rs.get("exchangesInflight"));
                     ri.meanTime = Math.max(0, 
objToLong(rs.get("meanProcessingTime")));
                     ri.minTime = Math.max(0, 
objToLong(rs.get("minProcessingTime")));
@@ -320,6 +322,8 @@ final class StatusParser {
                         if (ps != null) {
                             pi.total = objToLong(ps.get("exchangesTotal"));
                             pi.failed = objToLong(ps.get("exchangesFailed"));
+                            pi.failuresHandled = 
objToLong(ps.get("failuresHandled"));
+                            pi.redeliveries = 
objToLong(ps.get("redeliveries"));
                             pi.meanTime = Math.max(0, 
objToLong(ps.get("meanProcessingTime")));
                             pi.minTime = Math.max(0, 
objToLong(ps.get("minProcessingTime")));
                             pi.maxTime = Math.max(0, 
objToLong(ps.get("maxProcessingTime")));
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md
index c61fe46fade0..84bce008e709 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md
@@ -25,6 +25,8 @@ Below the routes are the steps of the selected route, with 
the same numbers:
 - **TYPE** — The kind of step (`to`, `setBody`, `choice`), indented under the 
step it is inside, such as the steps of a branch of a choice
 - **LINE** — The line of the route source the step is on (when the runtime 
records the source locations)
 - **PROCESSOR** — What the step does, as the route diagram shows it: where it 
sends to (`log:outbox`), its expression (`simple{${body}}`) or its data format 
(`jackson`). An id the route gives a step (`id: audit`) is shown first; the ids 
Camel generates (`to1`, `setBody2`) are left out, as they only repeat the type
+- **REDEL** — The redeliveries of the error handler at this step: an exchange 
that failed here and was tried again, whether the next attempt succeeded or 
not. The route row counts the exchanges that completed after a redelivery. 
Shown when the error handler redelivered an exchange of the route, or handled 
one
+- **HAND** — The failures the error handler handled, such as an exchange moved 
to a dead letter channel: the route counts them as done, not failed, so FAIL 
stays 0
 - **BODY TYPES** — The body types the step has been reached with, across the 
messages (with the message history on)
 
 ## Percentile Latency (P50/P95/P99)
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesErrorHandlerColumnsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesErrorHandlerColumnsTest.java
new file mode 100644
index 000000000000..b0c5c53207d3
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesErrorHandlerColumnsTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.tui;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-26698: the processors of the Routes tab show the redeliveries and the 
failures handled by the error handler, so
+ * an exchange that failed and then succeeded on redelivery leaves a trace at 
the step that was retried.
+ */
+class RoutesErrorHandlerColumnsTest {
+
+    private MonitorContext ctx;
+    private IntegrationInfo info;
+
+    @BeforeEach
+    void setUp() {
+        Theme.resetForTesting();
+        info = new IntegrationInfo();
+        info.pid = "5678";
+        info.name = "chaosdemo";
+        ctx = new MonitorContext(new AtomicReference<>(List.of(info)), new 
AtomicReference<>(List.of()));
+        ctx.selectedPid = "5678";
+    }
+
+    @Test
+    void theStatusHasTheCountersOfTheRouteAndItsProcessors() throws Exception {
+        JsonObject route = (JsonObject) Jsoner.deserialize("""
+                {"routeId": "inventory", "from": "direct://inventory", 
"state": "Started",
+                 "statistics": {"exchangesTotal": 1715, "exchangesFailed": 0, 
"failuresHandled": 4, "redeliveries": 58},
+                 "processors": [{"id": "reserve", "processor": "setHeader", 
"level": 1,
+                                 "statistics": {"exchangesTotal": 1761, 
"exchangesFailed": 46, "failuresHandled": 0,
+                                                "redeliveries": 132}}]}
+                """);
+        JsonObject root = new JsonObject();
+        root.put("context", new JsonObject(Map.of("name", "chaosdemo")));
+        root.put("routes", new JsonArray(List.of(route)));
+        IntegrationInfo parsed = 
StatusParser.parseIntegration(ProcessHandle.current(), root);
+
+        RouteInfo ri = parsed.routes.get(0);
+        assertThat(ri.redeliveries).isEqualTo(58);
+        assertThat(ri.failuresHandled).isEqualTo(4);
+        assertThat(ri.processors.get(0).redeliveries).isEqualTo(132);
+        assertThat(ri.processors.get(0).failed).isEqualTo(46);
+    }
+
+    @Test
+    void theProcessorsShowTheRedeliveriesOfTheStepThatWasRetried() {
+        RouteInfo route = route();
+        route.redeliveries = 58;
+        route.failuresHandled = 4;
+        route.processors.get(0).failed = 46;
+        route.processors.get(0).redeliveries = 132;
+
+        String rendered = TuiTestHelper.renderToString(new RoutesTab(ctx), 
160, 30);
+
+        assertThat(rendered).contains("REDEL").contains("HAND");
+        assertThat(lineOf(rendered, "reserve")).contains("46").contains("132");
+        assertThat(lineOf(rendered, 
"direct://inventory")).contains("58").contains("4");
+        // the table data for an AI has the counters of the routes too
+        JsonObject row = (JsonObject) ((List<?>) new 
RoutesTab(ctx).getTableDataAsJson().get("rows")).get(0);
+        assertThat(row.getLong("redeliveries")).isEqualTo(58);
+        assertThat(row.getLong("failuresHandled")).isEqualTo(4);
+    }
+
+    @Test
+    void theColumnsAreLeftOutWhenNothingWasRedeliveredOrHandled() {
+        route();
+
+        String rendered = TuiTestHelper.renderToString(new RoutesTab(ctx), 
160, 30);
+
+        
assertThat(rendered).contains("INFLIGHT").doesNotContain("REDEL").doesNotContain("HAND");
+    }
+
+    private RouteInfo route() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "inventory";
+        route.from = "direct://inventory";
+        route.state = "Started";
+        route.uptime = "10s";
+        route.total = 1715;
+        ProcessorInfo reserve = new ProcessorInfo();
+        reserve.id = "reserve";
+        reserve.processor = "setHeader";
+        reserve.level = 1;
+        reserve.total = 1761;
+        route.processors.add(reserve);
+        info.routes.add(route);
+        return route;
+    }
+
+    private static String lineOf(String rendered, String text) {
+        return rendered.lines().filter(l -> l.contains(text) && 
l.contains("│")).reduce((a, b) -> b).orElse("");
+    }
+}

Reply via email to