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