This is an automated email from the ASF dual-hosted git repository.
gnodet 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 8172c904ffc1 CAMEL-23079: Add ErrorRegistry SPI for capturing routing
errors
8172c904ffc1 is described below
commit 8172c904ffc1d5bbdcad1cf6f74a8c2cd54ec1e4
Author: Guillaume Nodet <[email protected]>
AuthorDate: Tue Mar 10 11:46:48 2026 +0100
CAMEL-23079: Add ErrorRegistry SPI for capturing routing errors
Add an opt-in ErrorRegistry SPI that captures snapshots of exceptions
during message routing without retaining references to exchanges or
exceptions. The registry stores error metadata (exception type, message,
route, endpoint, timestamp) plus optional stack traces and message
history traces.
Features:
- Bounded in-memory storage with configurable max entries and TTL
- Per-route scoped views for browsing and clearing errors
- Dev console (errors) with text and JSON output
- JMX MBean for management and monitoring
- Configurable via camel.main properties
Disabled by default. Enable with camel.main.errorRegistryEnabled=true.
---
.../apache/camel/catalog/dev-consoles.properties | 1 +
.../apache/camel/catalog/dev-consoles/errors.json | 15 +
.../main/camel-main-configuration-metadata.json | 4 +
.../main/java/org/apache/camel/CamelContext.java | 15 +
.../java/org/apache/camel/spi/ErrorRegistry.java | 96 ++++++
.../org/apache/camel/spi/ErrorRegistryEntry.java | 76 +++++
.../org/apache/camel/spi/ErrorRegistryView.java | 47 +++
.../camel/impl/engine/AbstractCamelContext.java | 19 ++
.../impl/engine/DefaultCamelContextExtension.java | 20 ++
.../camel/impl/engine/DefaultErrorRegistry.java | 332 +++++++++++++++++++++
.../impl/engine/DefaultManagementStrategy.java | 7 +-
.../camel/impl/engine/SimpleCamelContext.java | 6 +
.../org/apache/camel/dev-console/errors.json | 15 +
.../services/org/apache/camel/dev-console/errors | 2 +
.../org/apache/camel/dev-consoles.properties | 2 +-
.../camel/impl/console/ErrorRegistryConsole.java | 157 ++++++++++
.../apache/camel/impl/CamelContextConfigurer.java | 6 +
.../org/apache/camel/impl/ErrorRegistryTest.java | 243 +++++++++++++++
.../impl/event/SimpleEventNotifierEventsTest.java | 6 +-
.../MainConfigurationPropertiesConfigurer.java | 28 ++
.../camel-main-configuration-metadata.json | 4 +
core/camel-main/src/main/docs/main.adoc | 6 +-
.../camel/main/DefaultConfigurationConfigurer.java | 6 +
.../camel/main/DefaultConfigurationProperties.java | 97 ++++++
.../api/management/mbean/CamelOpenMBeanTypes.java | 19 ++
.../mbean/ManagedErrorRegistryMBean.java | 64 ++++
.../management/JmxManagementLifecycleStrategy.java | 4 +
.../management/mbean/ManagedErrorRegistry.java | 140 +++++++++
.../management/ManagedNonManagedServiceTest.java | 2 +-
...edProducerRouteAddRemoveRegisterAlwaysTest.java | 2 +-
.../management/ManagedRouteAddRemoveTest.java | 2 +-
docs/user-manual/modules/ROOT/nav.adoc | 1 +
.../ROOT/pages/camel-4x-upgrade-guide-4_19.adoc | 5 +
.../modules/ROOT/pages/error-registry.adoc | 60 ++++
34 files changed, 1499 insertions(+), 10 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
index 6954ed2b5d4e..db039fb9379c 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
@@ -10,6 +10,7 @@ consumer
context
debug
endpoint
+errors
eval-language
event
fault-tolerance
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/errors.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/errors.json
new file mode 100644
index 000000000000..62a53fcd97bc
--- /dev/null
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/errors.json
@@ -0,0 +1,15 @@
+{
+ "console": {
+ "kind": "console",
+ "group": "camel",
+ "name": "errors",
+ "title": "Error Registry",
+ "description": "Display captured routing errors",
+ "deprecated": false,
+ "javaType": "org.apache.camel.impl.console.ErrorRegistryConsole",
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-console",
+ "version": "4.19.0-SNAPSHOT"
+ }
+}
+
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
index 1b156fda5539..89b92a71e735 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
@@ -69,6 +69,10 @@
{ "name": "camel.main.endpointBridgeErrorHandler", "required": false,
"description": "Allows for bridging the consumer to the Camel routing Error
Handler, which mean any exceptions occurred while the consumer is trying to
pickup incoming messages, or the likes, will now be processed as a message and
handled by the routing Error Handler. By default the consumer will use the
org.apache.camel.spi.ExceptionHandler to deal with exceptions, that will be
logged at WARN\/ERROR level and igno [...]
{ "name": "camel.main.endpointLazyStartProducer", "required": false,
"description": "Whether the producer should be started lazy (on the first
message). By starting lazy you can use this to allow CamelContext and routes to
startup in situations where a producer may otherwise fail during starting and
cause the route to fail being started. By deferring this startup to be lazy
then the startup failure can be handled during routing messages via Camel's
routing error handlers. Beware that [...]
{ "name": "camel.main.endpointRuntimeStatisticsEnabled", "required":
false, "description": "Sets whether endpoint runtime statistics is enabled
(gathers runtime usage of each incoming and outgoing endpoints). The default
value is false.", "sourceType":
"org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean",
"javaType": "boolean", "defaultValue": false, "secret": false },
+ { "name": "camel.main.errorRegistryEnabled", "required": false,
"description": "Sets whether the error registry is enabled to capture errors
during message routing. This is by default disabled.", "sourceType":
"org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean",
"javaType": "boolean", "defaultValue": false, "secret": false },
+ { "name": "camel.main.errorRegistryMaximumEntries", "required": false,
"description": "Sets the maximum number of error entries to keep in the error
registry. When the limit is exceeded, the oldest entries are evicted. The
default value is 100.", "sourceType":
"org.apache.camel.main.DefaultConfigurationProperties", "type": "integer",
"javaType": "int", "defaultValue": 100, "secret": false },
+ { "name": "camel.main.errorRegistryStackTraceEnabled", "required": false,
"description": "Sets whether to capture stack traces in the error registry.
This is enabled by default.", "sourceType":
"org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean",
"javaType": "boolean", "defaultValue": true, "secret": false },
+ { "name": "camel.main.errorRegistryTimeToLiveSeconds", "required": false,
"description": "Sets the time-to-live in seconds for error entries in the error
registry. Entries older than this are evicted. The default value is 3600 (1
hour).", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties",
"type": "integer", "javaType": "int", "defaultValue": 3600, "secret": false },
{ "name": "camel.main.exchangeFactory", "required": false, "description":
"Controls whether to pool (reuse) exchanges or create new exchanges
(prototype). Using pooled will reduce JVM garbage collection overhead by
avoiding to re-create Exchange instances per message each consumer receives.
The default is prototype mode.", "sourceType":
"org.apache.camel.main.DefaultConfigurationProperties", "type": "enum",
"javaType": "java.lang.String", "defaultValue": "default", "secret": false, "
[...]
{ "name": "camel.main.exchangeFactoryCapacity", "required": false,
"description": "The capacity the pool (for each consumer) uses for storing
exchanges. The default capacity is 100.", "sourceType":
"org.apache.camel.main.DefaultConfigurationProperties", "type": "integer",
"javaType": "int", "defaultValue": 100, "secret": false },
{ "name": "camel.main.exchangeFactoryStatisticsEnabled", "required":
false, "description": "Configures whether statistics is enabled on exchange
factory.", "sourceType":
"org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean",
"javaType": "boolean", "defaultValue": false, "secret": false },
diff --git a/core/camel-api/src/main/java/org/apache/camel/CamelContext.java
b/core/camel-api/src/main/java/org/apache/camel/CamelContext.java
index dd4e4ba78b63..e5921b3d2141 100644
--- a/core/camel-api/src/main/java/org/apache/camel/CamelContext.java
+++ b/core/camel-api/src/main/java/org/apache/camel/CamelContext.java
@@ -30,6 +30,7 @@ import org.apache.camel.spi.DataFormat;
import org.apache.camel.spi.DataType;
import org.apache.camel.spi.Debugger;
import org.apache.camel.spi.EndpointRegistry;
+import org.apache.camel.spi.ErrorRegistry;
import org.apache.camel.spi.ExecutorServiceManager;
import org.apache.camel.spi.InflightRepository;
import org.apache.camel.spi.Injector;
@@ -1228,6 +1229,20 @@ public interface CamelContext extends
CamelContextLifecycle, RuntimeConfiguratio
*/
void setInflightRepository(InflightRepository repository);
+ /**
+ * Gets the error registry
+ *
+ * @return the error registry
+ */
+ ErrorRegistry getErrorRegistry();
+
+ /**
+ * Sets a custom error registry to use
+ *
+ * @param errorRegistry the error registry
+ */
+ void setErrorRegistry(ErrorRegistry errorRegistry);
+
/**
* Gets the application CamelContext class loader which may be helpful for
running camel in other containers
*
diff --git
a/core/camel-api/src/main/java/org/apache/camel/spi/ErrorRegistry.java
b/core/camel-api/src/main/java/org/apache/camel/spi/ErrorRegistry.java
new file mode 100644
index 000000000000..1f57b5d547d3
--- /dev/null
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/ErrorRegistry.java
@@ -0,0 +1,96 @@
+/*
+ * 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.spi;
+
+import java.time.Duration;
+
+import org.apache.camel.StaticService;
+
+/**
+ * A registry which captures exceptions that occurred during message routing
and stores them in memory.
+ * <p/>
+ * This is an opt-in feature that must be enabled. When enabled, the registry
captures error snapshots (exception type,
+ * message, stack trace) without retaining references to the original exchange
or exception objects.
+ * <p/>
+ * The registry has a configurable maximum capacity and time-to-live to
prevent unbounded memory growth and stale data.
+ * <p/>
+ * The registry itself implements {@link ErrorRegistryView} for global scope,
and scoped views for individual routes can
+ * be obtained via {@link #forRoute(String)}.
+ *
+ * @see ErrorRegistryEntry
+ * @see ErrorRegistryView
+ */
+public interface ErrorRegistry extends ErrorRegistryView, StaticService {
+
+ /**
+ * Gets a view scoped to a specific route.
+ * <p/>
+ * The returned view is a lightweight filter over the same underlying data.
+ *
+ * @param routeId the route id
+ * @return a view containing only errors from the given route
+ */
+ ErrorRegistryView forRoute(String routeId);
+
+ // -- Configuration --
+
+ /**
+ * Whether the error registry is enabled
+ */
+ boolean isEnabled();
+
+ /**
+ * Sets whether the error registry is enabled.
+ * <p/>
+ * This is by default disabled.
+ */
+ void setEnabled(boolean enabled);
+
+ /**
+ * The maximum number of error entries to keep in the registry
+ */
+ int getMaximumEntries();
+
+ /**
+ * Sets the maximum number of error entries to keep. When the limit is
exceeded, the oldest entries are evicted.
+ * <p/>
+ * The default value is 100.
+ */
+ void setMaximumEntries(int maximumEntries);
+
+ /**
+ * The time-to-live for error entries
+ */
+ Duration getTimeToLive();
+
+ /**
+ * Sets the time-to-live for error entries. Entries older than this
duration are evicted.
+ * <p/>
+ * The default value is 1 hour.
+ */
+ void setTimeToLive(Duration timeToLive);
+
+ /**
+ * Whether stack trace capture is enabled
+ */
+ boolean isStackTraceEnabled();
+
+ /**
+ * Sets whether to capture stack traces. This is enabled by default.
+ */
+ void setStackTraceEnabled(boolean stackTraceEnabled);
+}
diff --git
a/core/camel-api/src/main/java/org/apache/camel/spi/ErrorRegistryEntry.java
b/core/camel-api/src/main/java/org/apache/camel/spi/ErrorRegistryEntry.java
new file mode 100644
index 000000000000..b8bb1dea77cf
--- /dev/null
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/ErrorRegistryEntry.java
@@ -0,0 +1,76 @@
+/*
+ * 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.spi;
+
+import java.time.Instant;
+
+/**
+ * A snapshot of an error that occurred during message routing.
+ * <p/>
+ * This is an immutable value object that does not hold references to the
original exchange or exception.
+ */
+public interface ErrorRegistryEntry {
+
+ /**
+ * The exchange id
+ */
+ String exchangeId();
+
+ /**
+ * The route id where the error occurred
+ */
+ String routeId();
+
+ /**
+ * The endpoint URI where the error occurred (if available)
+ */
+ String endpointUri();
+
+ /**
+ * The timestamp when the error occurred
+ */
+ Instant timestamp();
+
+ /**
+ * Whether the error was handled by an error handler or onException
+ */
+ boolean handled();
+
+ /**
+ * The fully qualified class name of the exception
+ */
+ String exceptionType();
+
+ /**
+ * The exception message
+ */
+ String exceptionMessage();
+
+ /**
+ * The stack trace lines, or {@code null} if stack trace capture is
disabled.
+ * <p/>
+ * Each element represents one line of the stack trace.
+ */
+ String[] stackTrace();
+
+ /**
+ * The message history trace, or {@code null} if message history is not
enabled.
+ * <p/>
+ * Each element represents one step in the routing history (e.g.
"routeId[nodeId]").
+ */
+ String[] messageHistory();
+}
diff --git
a/core/camel-api/src/main/java/org/apache/camel/spi/ErrorRegistryView.java
b/core/camel-api/src/main/java/org/apache/camel/spi/ErrorRegistryView.java
new file mode 100644
index 000000000000..d42a152e8d1d
--- /dev/null
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/ErrorRegistryView.java
@@ -0,0 +1,47 @@
+/*
+ * 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.spi;
+
+import java.util.Collection;
+
+/**
+ * A scoped view over error entries in an {@link ErrorRegistry}, supporting
browsing and clearing.
+ */
+public interface ErrorRegistryView {
+
+ /**
+ * The number of error entries in this view
+ */
+ int size();
+
+ /**
+ * Browse all error entries, sorted by most recent first
+ */
+ Collection<ErrorRegistryEntry> browse();
+
+ /**
+ * Browse error entries with a limit, sorted by most recent first
+ *
+ * @param limit maximum number of entries to return
+ */
+ Collection<ErrorRegistryEntry> browse(int limit);
+
+ /**
+ * Clear all error entries in this view
+ */
+ void clear();
+}
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
index 0af2db032ab9..888f62cd5dcc 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
@@ -116,6 +116,7 @@ import org.apache.camel.spi.DumpRoutesStrategy;
import org.apache.camel.spi.EndpointRegistry;
import org.apache.camel.spi.EndpointServiceRegistry;
import org.apache.camel.spi.EndpointStrategy;
+import org.apache.camel.spi.ErrorRegistry;
import org.apache.camel.spi.EventNotifier;
import org.apache.camel.spi.ExchangeFactory;
import org.apache.camel.spi.ExchangeFactoryManager;
@@ -2612,6 +2613,12 @@ public abstract class AbstractCamelContext extends
BaseService
addService(runtimeEndpointRegistry, true, true);
}
+ // register error registry as event notifier so it captures exchange
failure events
+ ErrorRegistry errorRegistry = getErrorRegistry();
+ if (errorRegistry instanceof EventNotifier && getManagementStrategy()
!= null) {
+ getManagementStrategy().addEventNotifier((EventNotifier)
errorRegistry);
+ }
+
bindDataFormats();
// init components
@@ -3871,6 +3878,16 @@ public abstract class AbstractCamelContext extends
BaseService
camelContextExtension.setInflightRepository(repository);
}
+ @Override
+ public ErrorRegistry getErrorRegistry() {
+ return camelContextExtension.getErrorRegistry();
+ }
+
+ @Override
+ public void setErrorRegistry(ErrorRegistry errorRegistry) {
+ camelContextExtension.setErrorRegistry(errorRegistry);
+ }
+
@Override
public void setAutoStartup(Boolean autoStartup) {
this.autoStartup = autoStartup;
@@ -4430,6 +4447,8 @@ public abstract class AbstractCamelContext extends
BaseService
protected abstract InflightRepository createInflightRepository();
+ protected abstract ErrorRegistry createErrorRegistry();
+
protected abstract AsyncProcessorAwaitManager
createAsyncProcessorAwaitManager();
protected abstract RouteController createRouteController();
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelContextExtension.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelContextExtension.java
index 62be92e65c3d..9a78f6580a1c 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelContextExtension.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelContextExtension.java
@@ -46,6 +46,7 @@ import org.apache.camel.spi.ClassResolver;
import org.apache.camel.spi.EndpointServiceRegistry;
import org.apache.camel.spi.EndpointStrategy;
import org.apache.camel.spi.EndpointUriFactory;
+import org.apache.camel.spi.ErrorRegistry;
import org.apache.camel.spi.EventNotifier;
import org.apache.camel.spi.ExchangeFactory;
import org.apache.camel.spi.ExchangeFactoryManager;
@@ -127,6 +128,7 @@ class DefaultCamelContextExtension implements
ExtendedCamelContext {
private volatile MessageHistoryFactory messageHistoryFactory;
private volatile StreamCachingStrategy streamCachingStrategy;
private volatile InflightRepository inflightRepository;
+ private volatile ErrorRegistry errorRegistry;
private volatile UuidGenerator uuidGenerator;
private volatile Tracer tracer;
private volatile TransformerRegistry transformerRegistry;
@@ -878,6 +880,24 @@ class DefaultCamelContextExtension implements
ExtendedCamelContext {
this.inflightRepository =
camelContext.getInternalServiceManager().addService(camelContext, repository);
}
+ ErrorRegistry getErrorRegistry() {
+ if (errorRegistry == null) {
+ lock.lock();
+ try {
+ if (errorRegistry == null) {
+ setErrorRegistry(camelContext.createErrorRegistry());
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+ return errorRegistry;
+ }
+
+ void setErrorRegistry(ErrorRegistry errorRegistry) {
+ this.errorRegistry =
camelContext.getInternalServiceManager().addService(camelContext,
errorRegistry);
+ }
+
UuidGenerator getUuidGenerator() {
if (uuidGenerator == null) {
lock.lock();
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java
new file mode 100644
index 000000000000..93509f5c8227
--- /dev/null
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java
@@ -0,0 +1,332 @@
+/*
+ * 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.engine;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentLinkedDeque;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.ExchangePropertyKey;
+import org.apache.camel.MessageHistory;
+import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.spi.ErrorRegistry;
+import org.apache.camel.spi.ErrorRegistryEntry;
+import org.apache.camel.spi.ErrorRegistryView;
+import org.apache.camel.support.EventNotifierSupport;
+
+/**
+ * Default {@link ErrorRegistry} implementation that listens to exchange
failure events and captures error snapshots.
+ */
+public class DefaultErrorRegistry extends EventNotifierSupport implements
ErrorRegistry {
+
+ private final ConcurrentLinkedDeque<ErrorRegistryEntry> entries = new
ConcurrentLinkedDeque<>();
+ private volatile boolean enabled;
+ private volatile int maximumEntries = 100;
+ private volatile Duration timeToLive = Duration.ofHours(1);
+ private volatile boolean stackTraceEnabled = true;
+
+ public DefaultErrorRegistry() {
+ // only listen to exchange failure events
+ setIgnoreCamelContextEvents(true);
+ setIgnoreCamelContextInitEvents(true);
+ setIgnoreRouteEvents(true);
+ setIgnoreServiceEvents(true);
+ // ignore all exchange events by default (disabled); toggled when
enabled
+ setIgnoreExchangeEvents(true);
+ setIgnoreExchangeCreatedEvent(true);
+ setIgnoreExchangeCompletedEvent(true);
+ setIgnoreExchangeFailedEvents(true);
+ setIgnoreExchangeRedeliveryEvents(true);
+ setIgnoreExchangeSentEvents(true);
+ setIgnoreExchangeSendingEvents(true);
+ setIgnoreExchangeAsyncProcessingStartedEvents(true);
+ setIgnoreStepEvents(true);
+ }
+
+ @Override
+ public void notify(CamelEvent event) throws Exception {
+ if (!enabled) {
+ return;
+ }
+ if (event instanceof CamelEvent.ExchangeFailedEvent e) {
+ capture(e.getExchange(), false);
+ } else if (event instanceof CamelEvent.ExchangeFailureHandledEvent e) {
+ capture(e.getExchange(), true);
+ }
+ }
+
+ @Override
+ public boolean isEnabled(CamelEvent event) {
+ return enabled;
+ }
+
+ @Override
+ public boolean isDisabled() {
+ return !enabled;
+ }
+
+ private void capture(Exchange exchange, boolean handled) {
+ Throwable exception;
+ if (handled) {
+ // when handled, the exception has been moved to EXCEPTION_CAUGHT
property
+ exception =
exchange.getProperty(ExchangePropertyKey.EXCEPTION_CAUGHT, Throwable.class);
+ } else {
+ exception = exchange.getException();
+ }
+ if (exception == null) {
+ return;
+ }
+
+ String exchangeId = exchange.getExchangeId();
+ String routeId =
exchange.getProperty(ExchangePropertyKey.FAILURE_ROUTE_ID, String.class);
+ if (routeId == null) {
+ routeId = exchange.getFromRouteId();
+ }
+ String endpointUri =
exchange.getProperty(ExchangePropertyKey.FAILURE_ENDPOINT, String.class);
+ String exceptionType = exception.getClass().getName();
+ String exceptionMessage = exception.getMessage();
+ String[] stackTrace = stackTraceEnabled ? captureStackTrace(exception)
: null;
+ String[] messageHistory = captureMessageHistory(exchange);
+
+ DefaultErrorRegistryEntry entry = new DefaultErrorRegistryEntry(
+ exchangeId, routeId, endpointUri, Instant.now(),
+ handled, exceptionType, exceptionMessage, stackTrace,
messageHistory);
+
+ entries.addFirst(entry);
+ evict();
+ }
+
+ private static String[] captureStackTrace(Throwable exception) {
+ StringWriter writer = new StringWriter();
+ exception.printStackTrace(new PrintWriter(writer, true));
+ return writer.toString().split("\\r?\\n");
+ }
+
+ @SuppressWarnings("unchecked")
+ private static String[] captureMessageHistory(Exchange exchange) {
+ List<MessageHistory> history
+ = exchange.getProperty(ExchangePropertyKey.MESSAGE_HISTORY,
List.class);
+ if (history == null || history.isEmpty()) {
+ return null;
+ }
+ String[] result = new String[history.size()];
+ for (int i = 0; i < history.size(); i++) {
+ MessageHistory mh = history.get(i);
+ String nodeId = mh.getNode() != null ? mh.getNode().getId() : null;
+ long elapsed = mh.getElapsed();
+ if (elapsed > 0) {
+ result[i] = mh.getRouteId() + "[" + nodeId + "] (" + elapsed +
" ms)";
+ } else {
+ result[i] = mh.getRouteId() + "[" + nodeId + "]";
+ }
+ }
+ return result;
+ }
+
+ private void evict() {
+ // remove excess entries beyond maximum
+ while (entries.size() > maximumEntries) {
+ entries.pollLast();
+ }
+ // remove expired entries from the tail (oldest)
+ Instant cutoff = Instant.now().minus(timeToLive);
+ while (!entries.isEmpty()) {
+ ErrorRegistryEntry last = entries.peekLast();
+ if (last != null && last.timestamp().isBefore(cutoff)) {
+ entries.pollLast();
+ } else {
+ break;
+ }
+ }
+ }
+
+ // -- View methods (global scope) --
+
+ @Override
+ public int size() {
+ evict();
+ return entries.size();
+ }
+
+ @Override
+ public Collection<ErrorRegistryEntry> browse() {
+ return browse(-1);
+ }
+
+ @Override
+ public Collection<ErrorRegistryEntry> browse(int limit) {
+ evict();
+ if (limit <= 0) {
+ return Collections.unmodifiableList(new ArrayList<>(entries));
+ }
+ List<ErrorRegistryEntry> result = new ArrayList<>(Math.min(limit,
entries.size()));
+ int count = 0;
+ for (ErrorRegistryEntry entry : entries) {
+ if (count >= limit) {
+ break;
+ }
+ result.add(entry);
+ count++;
+ }
+ return Collections.unmodifiableList(result);
+ }
+
+ @Override
+ public void clear() {
+ entries.clear();
+ }
+
+ // -- Scoped view --
+
+ @Override
+ public ErrorRegistryView forRoute(String routeId) {
+ return new RouteView(routeId);
+ }
+
+ // -- Configuration --
+
+ @Override
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ // toggle exchange event listening based on enabled state
+ setIgnoreExchangeEvents(!enabled);
+ setIgnoreExchangeFailedEvents(!enabled);
+ if (enabled && getCamelContext() != null) {
+ // ensure exchange event notification is activated
+
getCamelContext().getCamelContextExtension().setEventNotificationApplicable(true);
+ }
+ }
+
+ @Override
+ public int getMaximumEntries() {
+ return maximumEntries;
+ }
+
+ @Override
+ public void setMaximumEntries(int maximumEntries) {
+ this.maximumEntries = maximumEntries;
+ }
+
+ @Override
+ public Duration getTimeToLive() {
+ return timeToLive;
+ }
+
+ @Override
+ public void setTimeToLive(Duration timeToLive) {
+ this.timeToLive = timeToLive;
+ }
+
+ @Override
+ public boolean isStackTraceEnabled() {
+ return stackTraceEnabled;
+ }
+
+ @Override
+ public void setStackTraceEnabled(boolean stackTraceEnabled) {
+ this.stackTraceEnabled = stackTraceEnabled;
+ }
+
+ /**
+ * A filtered view over entries for a specific route.
+ */
+ private class RouteView implements ErrorRegistryView {
+
+ private final String routeId;
+
+ RouteView(String routeId) {
+ this.routeId = Objects.requireNonNull(routeId);
+ }
+
+ @Override
+ public int size() {
+ evict();
+ int count = 0;
+ for (ErrorRegistryEntry entry : entries) {
+ if (routeId.equals(entry.routeId())) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ @Override
+ public Collection<ErrorRegistryEntry> browse() {
+ return browse(-1);
+ }
+
+ @Override
+ public Collection<ErrorRegistryEntry> browse(int limit) {
+ evict();
+ List<ErrorRegistryEntry> result = new ArrayList<>();
+ for (ErrorRegistryEntry entry : entries) {
+ if (routeId.equals(entry.routeId())) {
+ result.add(entry);
+ if (limit > 0 && result.size() >= limit) {
+ break;
+ }
+ }
+ }
+ return Collections.unmodifiableList(result);
+ }
+
+ @Override
+ public void clear() {
+ entries.removeIf(entry -> routeId.equals(entry.routeId()));
+ }
+ }
+
+ /**
+ * Immutable snapshot of an error.
+ */
+ private record DefaultErrorRegistryEntry(
+ String exchangeId,
+ String routeId,
+ String endpointUri,
+ Instant timestamp,
+ boolean handled,
+ String exceptionType,
+ String exceptionMessage,
+ String[] stackTrace,
+ String[] messageHistory)
+ implements
+ ErrorRegistryEntry {
+
+ @Override
+ public String[] stackTrace() {
+ return stackTrace != null ? stackTrace.clone() : null;
+ }
+
+ @Override
+ public String[] messageHistory() {
+ return messageHistory != null ? messageHistory.clone() : null;
+ }
+ }
+}
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultManagementStrategy.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultManagementStrategy.java
index 43f10d4a6177..d203f1d0130e 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultManagementStrategy.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultManagementStrategy.java
@@ -197,8 +197,11 @@ public class DefaultManagementStrategy extends
ServiceSupport implements Managem
@Override
protected void doInit() throws Exception {
ObjectHelper.notNull(getCamelContext(), "CamelContext", this);
- if (!getEventNotifiers().isEmpty()) {
-
getCamelContext().getCamelContextExtension().setEventNotificationApplicable(true);
+ for (EventNotifier notifier : getEventNotifiers()) {
+ if (!notifier.isIgnoreExchangeEvents()) {
+
getCamelContext().getCamelContextExtension().setEventNotificationApplicable(true);
+ break;
+ }
}
for (EventNotifier notifier : eventNotifiers) {
// inject CamelContext if the service is aware
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/SimpleCamelContext.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/SimpleCamelContext.java
index 03af8dd61b8d..23db758d00d6 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/SimpleCamelContext.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/SimpleCamelContext.java
@@ -52,6 +52,7 @@ import org.apache.camel.spi.DeferServiceFactory;
import org.apache.camel.spi.DumpRoutesStrategy;
import org.apache.camel.spi.EndpointRegistry;
import org.apache.camel.spi.EndpointServiceRegistry;
+import org.apache.camel.spi.ErrorRegistry;
import org.apache.camel.spi.ExchangeFactory;
import org.apache.camel.spi.ExchangeFactoryManager;
import org.apache.camel.spi.ExecutorServiceManager;
@@ -343,6 +344,11 @@ public class SimpleCamelContext extends
AbstractCamelContext {
return new DefaultInflightRepository();
}
+ @Override
+ protected ErrorRegistry createErrorRegistry() {
+ return new DefaultErrorRegistry();
+ }
+
@Override
protected AsyncProcessorAwaitManager createAsyncProcessorAwaitManager() {
return new DefaultAsyncProcessorAwaitManager();
diff --git
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/errors.json
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/errors.json
new file mode 100644
index 000000000000..62a53fcd97bc
--- /dev/null
+++
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/errors.json
@@ -0,0 +1,15 @@
+{
+ "console": {
+ "kind": "console",
+ "group": "camel",
+ "name": "errors",
+ "title": "Error Registry",
+ "description": "Display captured routing errors",
+ "deprecated": false,
+ "javaType": "org.apache.camel.impl.console.ErrorRegistryConsole",
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-console",
+ "version": "4.19.0-SNAPSHOT"
+ }
+}
+
diff --git
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/errors
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/errors
new file mode 100644
index 000000000000..16f177d2d647
--- /dev/null
+++
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/errors
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.impl.console.ErrorRegistryConsole
diff --git
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
index cfe26d07ff4b..a322d5a6b410 100644
---
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
+++
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
@@ -1,5 +1,5 @@
# Generated by camel build tools - do NOT edit this file!
-dev-consoles=bean blocked browse circuit-breaker consumer context debug
endpoint eval-language event gc health inflight internal-tasks java-security
jvm log memory message-history processor producer properties receive reload
rest route route-controller route-dump route-group route-structure send service
simple-language source startup-recorder system-properties thread top trace
transformers type-converters variables
+dev-consoles=bean blocked browse circuit-breaker consumer context debug
endpoint errors eval-language event gc health inflight internal-tasks
java-security jvm log memory message-history processor producer properties
receive reload rest route route-controller route-dump route-group
route-structure send service simple-language source startup-recorder
system-properties thread top trace transformers type-converters variables
groupId=org.apache.camel
artifactId=camel-console
version=4.19.0-SNAPSHOT
diff --git
a/core/camel-console/src/main/java/org/apache/camel/impl/console/ErrorRegistryConsole.java
b/core/camel-console/src/main/java/org/apache/camel/impl/console/ErrorRegistryConsole.java
new file mode 100644
index 000000000000..9dc8e3bbf32b
--- /dev/null
+++
b/core/camel-console/src/main/java/org/apache/camel/impl/console/ErrorRegistryConsole.java
@@ -0,0 +1,157 @@
+/*
+ * 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.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.spi.ErrorRegistry;
+import org.apache.camel.spi.ErrorRegistryEntry;
+import org.apache.camel.spi.annotations.DevConsole;
+import org.apache.camel.support.console.AbstractDevConsole;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+@DevConsole(name = "errors", displayName = "Error Registry", description =
"Display captured routing errors")
+public class ErrorRegistryConsole extends AbstractDevConsole {
+
+ /**
+ * Filter by route id
+ */
+ public static final String ROUTE_ID = "routeId";
+
+ /**
+ * Limits the number of entries displayed
+ */
+ public static final String LIMIT = "limit";
+
+ /**
+ * Whether to include stack traces
+ */
+ public static final String STACK_TRACE = "stackTrace";
+
+ public ErrorRegistryConsole() {
+ super("camel", "errors", "Error Registry", "Display captured routing
errors");
+ }
+
+ @Override
+ protected String doCallText(Map<String, Object> options) {
+ String routeId = (String) options.get(ROUTE_ID);
+ int max = parseLimit(options);
+ boolean includeStackTrace = "true".equals(options.get(STACK_TRACE));
+
+ StringBuilder sb = new StringBuilder();
+
+ ErrorRegistry registry = getCamelContext().getErrorRegistry();
+ sb.append(String.format("%n Enabled: %s", registry.isEnabled()));
+ sb.append(String.format("%n Size: %s", registry.size()));
+
+ Collection<ErrorRegistryEntry> entries;
+ if (routeId != null) {
+ entries = registry.forRoute(routeId).browse(max);
+ } else {
+ entries = registry.browse(max);
+ }
+
+ for (ErrorRegistryEntry entry : entries) {
+ sb.append(String.format("%n %s (route: %s, endpoint: %s,
handled: %s, type: %s, message: %s, timestamp: %s)",
+ entry.exchangeId(), entry.routeId(), entry.endpointUri(),
+ entry.handled(), entry.exceptionType(),
entry.exceptionMessage(),
+ entry.timestamp()));
+ if (entry.messageHistory() != null) {
+ sb.append(String.format("%n Message History:"));
+ for (String step : entry.messageHistory()) {
+ sb.append(String.format("%n %s", step));
+ }
+ }
+ if (includeStackTrace && entry.stackTrace() != null) {
+ for (String line : entry.stackTrace()) {
+ sb.append(String.format("%n %s", line));
+ }
+ }
+ }
+
+ return sb.toString();
+ }
+
+ @Override
+ protected JsonObject doCallJson(Map<String, Object> options) {
+ String routeId = (String) options.get(ROUTE_ID);
+ int max = parseLimit(options);
+ boolean includeStackTrace = "true".equals(options.get(STACK_TRACE));
+
+ JsonObject root = new JsonObject();
+
+ ErrorRegistry registry = getCamelContext().getErrorRegistry();
+ root.put("enabled", registry.isEnabled());
+ root.put("size", registry.size());
+ root.put("maximumEntries", registry.getMaximumEntries());
+ root.put("timeToLive", registry.getTimeToLive().toString());
+ root.put("stackTraceEnabled", registry.isStackTraceEnabled());
+
+ Collection<ErrorRegistryEntry> entries;
+ if (routeId != null) {
+ entries = registry.forRoute(routeId).browse(max);
+ } else {
+ entries = registry.browse(max);
+ }
+
+ final List<JsonObject> list = new ArrayList<>();
+ for (ErrorRegistryEntry entry : entries) {
+ JsonObject jo = new JsonObject();
+ jo.put("exchangeId", entry.exchangeId());
+ jo.put("routeId", entry.routeId());
+ jo.put("endpointUri", entry.endpointUri());
+ jo.put("timestamp", entry.timestamp().toString());
+ jo.put("handled", entry.handled());
+ jo.put("exceptionType", entry.exceptionType());
+ jo.put("exceptionMessage", entry.exceptionMessage());
+ if (entry.messageHistory() != null) {
+ JsonArray history = new JsonArray();
+ for (String step : entry.messageHistory()) {
+ history.add(step);
+ }
+ jo.put("messageHistory", history);
+ }
+ if (includeStackTrace && entry.stackTrace() != null) {
+ JsonArray stackTrace = new JsonArray();
+ for (String line : entry.stackTrace()) {
+ stackTrace.add(line);
+ }
+ jo.put("stackTrace", stackTrace);
+ }
+ list.add(jo);
+ }
+ root.put("errors", list);
+
+ return root;
+ }
+
+ private static int parseLimit(Map<String, Object> options) {
+ String limit = (String) options.get(LIMIT);
+ if (limit == null) {
+ return Integer.MAX_VALUE;
+ }
+ try {
+ return Integer.parseInt(limit);
+ } catch (NumberFormatException e) {
+ return Integer.MAX_VALUE;
+ }
+ }
+}
diff --git
a/core/camel-core-engine/src/generated/java/org/apache/camel/impl/CamelContextConfigurer.java
b/core/camel-core-engine/src/generated/java/org/apache/camel/impl/CamelContextConfigurer.java
index 50bb74825b3a..cceb0c83c9c2 100644
---
a/core/camel-core-engine/src/generated/java/org/apache/camel/impl/CamelContextConfigurer.java
+++
b/core/camel-core-engine/src/generated/java/org/apache/camel/impl/CamelContextConfigurer.java
@@ -54,6 +54,8 @@ public class CamelContextConfigurer extends
org.apache.camel.support.component.P
case "devConsole": target.setDevConsole(property(camelContext,
java.lang.Boolean.class, value)); return true;
case "dumproutes":
case "dumpRoutes": target.setDumpRoutes(property(camelContext,
java.lang.String.class, value)); return true;
+ case "errorregistry":
+ case "errorRegistry": target.setErrorRegistry(property(camelContext,
org.apache.camel.spi.ErrorRegistry.class, value)); return true;
case "executorservicemanager":
case "executorServiceManager":
target.setExecutorServiceManager(property(camelContext,
org.apache.camel.spi.ExecutorServiceManager.class, value)); return true;
case "globaloptions":
@@ -174,6 +176,8 @@ public class CamelContextConfigurer extends
org.apache.camel.support.component.P
case "devConsole": return java.lang.Boolean.class;
case "dumproutes":
case "dumpRoutes": return java.lang.String.class;
+ case "errorregistry":
+ case "errorRegistry": return org.apache.camel.spi.ErrorRegistry.class;
case "executorservicemanager":
case "executorServiceManager": return
org.apache.camel.spi.ExecutorServiceManager.class;
case "globaloptions":
@@ -295,6 +299,8 @@ public class CamelContextConfigurer extends
org.apache.camel.support.component.P
case "devConsole": return target.isDevConsole();
case "dumproutes":
case "dumpRoutes": return target.getDumpRoutes();
+ case "errorregistry":
+ case "errorRegistry": return target.getErrorRegistry();
case "executorservicemanager":
case "executorServiceManager": return
target.getExecutorServiceManager();
case "globaloptions":
diff --git
a/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryTest.java
b/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryTest.java
new file mode 100644
index 000000000000..68d2d5f3fdf4
--- /dev/null
+++ b/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryTest.java
@@ -0,0 +1,243 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.impl;
+
+import java.util.Collection;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.ErrorRegistry;
+import org.apache.camel.spi.ErrorRegistryEntry;
+import org.apache.camel.spi.ErrorRegistryView;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ErrorRegistryTest extends ContextTestSupport {
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+ context.getErrorRegistry().setEnabled(true);
+ context.setMessageHistory(true);
+ return context;
+ }
+
+ @Test
+ public void testErrorRegistryCapturesHandledError() throws Exception {
+ getMockEndpoint("mock:dead").expectedMessageCount(1);
+
+ template.sendBody("direct:start", "Hello World");
+
+ assertMockEndpointsSatisfied();
+
+ ErrorRegistry registry = context.getErrorRegistry();
+ Collection<ErrorRegistryEntry> entries = registry.browse();
+ assertEquals(1, entries.size());
+
+ ErrorRegistryEntry entry = entries.iterator().next();
+ assertNotNull(entry.exchangeId());
+ assertEquals("foo", entry.routeId());
+ assertNotNull(entry.timestamp());
+ assertTrue(entry.handled());
+ assertEquals("java.lang.IllegalArgumentException",
entry.exceptionType());
+ assertEquals("Forced error", entry.exceptionMessage());
+ // stack trace is enabled by default
+ assertNotNull(entry.stackTrace());
+ assertTrue(entry.stackTrace().length > 0);
+ }
+
+ @Test
+ public void testErrorRegistryDisabled() throws Exception {
+ context.getErrorRegistry().setEnabled(false);
+
+ getMockEndpoint("mock:dead").expectedMessageCount(1);
+ template.sendBody("direct:start", "Hello World");
+ assertMockEndpointsSatisfied();
+
+ assertEquals(0, context.getErrorRegistry().size());
+ }
+
+ @Test
+ public void testErrorRegistryForRoute() throws Exception {
+ getMockEndpoint("mock:dead").expectedMessageCount(2);
+
+ template.sendBody("direct:start", "Hello World");
+ template.sendBody("direct:start2", "Bye World");
+
+ assertMockEndpointsSatisfied();
+
+ ErrorRegistry registry = context.getErrorRegistry();
+ assertEquals(2, registry.size());
+
+ // test forRoute view
+ ErrorRegistryView fooView = registry.forRoute("foo");
+ assertEquals(1, fooView.size());
+ ErrorRegistryEntry fooEntry = fooView.browse().iterator().next();
+ assertEquals("foo", fooEntry.routeId());
+
+ ErrorRegistryView barView = registry.forRoute("bar");
+ assertEquals(1, barView.size());
+
+ // clear only foo route
+ fooView.clear();
+ assertEquals(1, registry.size());
+ assertEquals(0, fooView.size());
+ assertEquals(1, barView.size());
+ }
+
+ @Test
+ public void testErrorRegistryMaximumEntries() throws Exception {
+ context.getErrorRegistry().setMaximumEntries(2);
+
+ getMockEndpoint("mock:dead").expectedMessageCount(3);
+
+ template.sendBody("direct:start", "A");
+ template.sendBody("direct:start", "B");
+ template.sendBody("direct:start", "C");
+
+ assertMockEndpointsSatisfied();
+
+ // only 2 most recent entries should be kept
+ assertEquals(2, context.getErrorRegistry().size());
+ }
+
+ @Test
+ public void testErrorRegistryWithStackTrace() throws Exception {
+ getMockEndpoint("mock:dead").expectedMessageCount(1);
+ template.sendBody("direct:start", "Hello World");
+ assertMockEndpointsSatisfied();
+
+ ErrorRegistryEntry entry =
context.getErrorRegistry().browse().iterator().next();
+ assertNotNull(entry.stackTrace());
+ assertTrue(entry.stackTrace().length > 0);
+ assertTrue(entry.stackTrace()[0].contains("IllegalArgumentException"));
+ }
+
+ @Test
+ public void testErrorRegistryWithoutStackTrace() throws Exception {
+ context.getErrorRegistry().setStackTraceEnabled(false);
+
+ getMockEndpoint("mock:dead").expectedMessageCount(1);
+ template.sendBody("direct:start", "Hello World");
+ assertMockEndpointsSatisfied();
+
+ ErrorRegistryEntry entry =
context.getErrorRegistry().browse().iterator().next();
+ assertNull(entry.stackTrace());
+ }
+
+ @Test
+ public void testErrorRegistryBrowseLimit() throws Exception {
+ getMockEndpoint("mock:dead").expectedMessageCount(3);
+
+ template.sendBody("direct:start", "A");
+ template.sendBody("direct:start", "B");
+ template.sendBody("direct:start", "C");
+
+ assertMockEndpointsSatisfied();
+
+ assertEquals(3, context.getErrorRegistry().size());
+ assertEquals(2, context.getErrorRegistry().browse(2).size());
+ }
+
+ @Test
+ public void testErrorRegistryClear() throws Exception {
+ getMockEndpoint("mock:dead").expectedMessageCount(1);
+ template.sendBody("direct:start", "Hello World");
+ assertMockEndpointsSatisfied();
+
+ assertEquals(1, context.getErrorRegistry().size());
+ context.getErrorRegistry().clear();
+ assertEquals(0, context.getErrorRegistry().size());
+ }
+
+ @Test
+ public void testErrorRegistryCapturesUnhandledError() throws Exception {
+ try {
+ template.sendBody("direct:unhandled", "Hello World");
+ } catch (Exception e) {
+ // expected
+ }
+
+ ErrorRegistry registry = context.getErrorRegistry();
+ Collection<ErrorRegistryEntry> entries = registry.browse();
+ assertEquals(1, entries.size());
+
+ ErrorRegistryEntry entry = entries.iterator().next();
+ assertNotNull(entry.exchangeId());
+ assertEquals("unhandled", entry.routeId());
+ assertFalse(entry.handled());
+ assertEquals("java.lang.IllegalArgumentException",
entry.exceptionType());
+ assertEquals("Unhandled error", entry.exceptionMessage());
+ }
+
+ @Test
+ public void testErrorRegistryCapturesEndpointUri() throws Exception {
+ getMockEndpoint("mock:dead").expectedMessageCount(1);
+ template.sendBody("direct:withEndpoint", "Hello World");
+ assertMockEndpointsSatisfied();
+
+ ErrorRegistryEntry entry =
context.getErrorRegistry().browse().iterator().next();
+ assertNotNull(entry.endpointUri());
+ assertTrue(entry.endpointUri().contains("direct://fail"),
+ "Expected endpoint URI to contain direct://fail but was: " +
entry.endpointUri());
+ }
+
+ @Test
+ public void testErrorRegistryCapturesMessageHistory() throws Exception {
+ getMockEndpoint("mock:dead").expectedMessageCount(1);
+ template.sendBody("direct:start", "Hello World");
+ assertMockEndpointsSatisfied();
+
+ ErrorRegistryEntry entry =
context.getErrorRegistry().browse().iterator().next();
+ assertNotNull(entry.messageHistory(), "Message history should be
captured when enabled");
+ assertTrue(entry.messageHistory().length > 0, "Message history should
have at least one entry");
+ // verify format includes route and node info
+ assertTrue(entry.messageHistory()[0].contains("foo"), "Message history
should contain route id");
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ errorHandler(deadLetterChannel("mock:dead"));
+
+ from("direct:start").routeId("foo")
+ .throwException(new IllegalArgumentException("Forced
error"));
+
+ from("direct:start2").routeId("bar")
+ .throwException(new IllegalArgumentException("Forced
error 2"));
+
+ from("direct:unhandled").routeId("unhandled")
+ .errorHandler(noErrorHandler())
+ .throwException(new
IllegalArgumentException("Unhandled error"));
+
+ from("direct:withEndpoint").routeId("withEndpoint")
+ .to("direct:fail");
+
+ from("direct:fail").routeId("failRoute")
+ .throwException(new IllegalArgumentException("Endpoint
error"));
+ }
+ };
+ }
+}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/impl/event/SimpleEventNotifierEventsTest.java
b/core/camel-core/src/test/java/org/apache/camel/impl/event/SimpleEventNotifierEventsTest.java
index e7d9fe75aba7..527a131fbb14 100644
---
a/core/camel-core/src/test/java/org/apache/camel/impl/event/SimpleEventNotifierEventsTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/impl/event/SimpleEventNotifierEventsTest.java
@@ -69,7 +69,7 @@ public class SimpleEventNotifierEventsTest {
@Test
public void testExchangeDone() throws Exception {
- // optimized as this does not require exchange events
+ // no exchange event notifiers are active (ErrorRegistry is disabled
by default, SimpleEventNotifierSupport ignores exchange events)
assertFalse(context.getCamelContextExtension().isEventNotificationApplicable());
MockEndpoint mock = context.getEndpoint("mock:result",
MockEndpoint.class);
@@ -110,7 +110,7 @@ public class SimpleEventNotifierEventsTest {
@Test
public void testExchangeFailed() {
- // optimized as this does not require exchange events
+ // no exchange event notifiers are active (ErrorRegistry is disabled
by default, SimpleEventNotifierSupport ignores exchange events)
assertFalse(context.getCamelContextExtension().isEventNotificationApplicable());
Exception e = assertThrows(Exception.class,
@@ -150,7 +150,7 @@ public class SimpleEventNotifierEventsTest {
@Test
public void testSuspendResume() {
- // optimized as this does not require exchange events
+ // no exchange event notifiers are active (ErrorRegistry is disabled
by default, SimpleEventNotifierSupport ignores exchange events)
assertFalse(context.getCamelContextExtension().isEventNotificationApplicable());
assertEquals(12, events.size());
diff --git
a/core/camel-main/src/generated/java/org/apache/camel/main/MainConfigurationPropertiesConfigurer.java
b/core/camel-main/src/generated/java/org/apache/camel/main/MainConfigurationPropertiesConfigurer.java
index 5a0ac2359a10..6f0c3b4febe3 100644
---
a/core/camel-main/src/generated/java/org/apache/camel/main/MainConfigurationPropertiesConfigurer.java
+++
b/core/camel-main/src/generated/java/org/apache/camel/main/MainConfigurationPropertiesConfigurer.java
@@ -62,6 +62,10 @@ public class MainConfigurationPropertiesConfigurer extends
org.apache.camel.supp
map.put("EndpointBridgeErrorHandler", boolean.class);
map.put("EndpointLazyStartProducer", boolean.class);
map.put("EndpointRuntimeStatisticsEnabled", boolean.class);
+ map.put("ErrorRegistryEnabled", boolean.class);
+ map.put("ErrorRegistryMaximumEntries", int.class);
+ map.put("ErrorRegistryStackTraceEnabled", boolean.class);
+ map.put("ErrorRegistryTimeToLiveSeconds", int.class);
map.put("ExchangeFactory", java.lang.String.class);
map.put("ExchangeFactoryCapacity", int.class);
map.put("ExchangeFactoryStatisticsEnabled", boolean.class);
@@ -234,6 +238,14 @@ public class MainConfigurationPropertiesConfigurer extends
org.apache.camel.supp
case "endpointLazyStartProducer":
target.setEndpointLazyStartProducer(property(camelContext, boolean.class,
value)); return true;
case "endpointruntimestatisticsenabled":
case "endpointRuntimeStatisticsEnabled":
target.setEndpointRuntimeStatisticsEnabled(property(camelContext,
boolean.class, value)); return true;
+ case "errorregistryenabled":
+ case "errorRegistryEnabled":
target.setErrorRegistryEnabled(property(camelContext, boolean.class, value));
return true;
+ case "errorregistrymaximumentries":
+ case "errorRegistryMaximumEntries":
target.setErrorRegistryMaximumEntries(property(camelContext, int.class,
value)); return true;
+ case "errorregistrystacktraceenabled":
+ case "errorRegistryStackTraceEnabled":
target.setErrorRegistryStackTraceEnabled(property(camelContext, boolean.class,
value)); return true;
+ case "errorregistrytimetoliveseconds":
+ case "errorRegistryTimeToLiveSeconds":
target.setErrorRegistryTimeToLiveSeconds(property(camelContext, int.class,
value)); return true;
case "exchangefactory":
case "exchangeFactory":
target.setExchangeFactory(property(camelContext, java.lang.String.class,
value)); return true;
case "exchangefactorycapacity":
@@ -494,6 +506,14 @@ public class MainConfigurationPropertiesConfigurer extends
org.apache.camel.supp
case "endpointLazyStartProducer": return boolean.class;
case "endpointruntimestatisticsenabled":
case "endpointRuntimeStatisticsEnabled": return boolean.class;
+ case "errorregistryenabled":
+ case "errorRegistryEnabled": return boolean.class;
+ case "errorregistrymaximumentries":
+ case "errorRegistryMaximumEntries": return int.class;
+ case "errorregistrystacktraceenabled":
+ case "errorRegistryStackTraceEnabled": return boolean.class;
+ case "errorregistrytimetoliveseconds":
+ case "errorRegistryTimeToLiveSeconds": return int.class;
case "exchangefactory":
case "exchangeFactory": return java.lang.String.class;
case "exchangefactorycapacity":
@@ -750,6 +770,14 @@ public class MainConfigurationPropertiesConfigurer extends
org.apache.camel.supp
case "endpointLazyStartProducer": return
target.isEndpointLazyStartProducer();
case "endpointruntimestatisticsenabled":
case "endpointRuntimeStatisticsEnabled": return
target.isEndpointRuntimeStatisticsEnabled();
+ case "errorregistryenabled":
+ case "errorRegistryEnabled": return target.isErrorRegistryEnabled();
+ case "errorregistrymaximumentries":
+ case "errorRegistryMaximumEntries": return
target.getErrorRegistryMaximumEntries();
+ case "errorregistrystacktraceenabled":
+ case "errorRegistryStackTraceEnabled": return
target.isErrorRegistryStackTraceEnabled();
+ case "errorregistrytimetoliveseconds":
+ case "errorRegistryTimeToLiveSeconds": return
target.getErrorRegistryTimeToLiveSeconds();
case "exchangefactory":
case "exchangeFactory": return target.getExchangeFactory();
case "exchangefactorycapacity":
diff --git
a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
index 1b156fda5539..89b92a71e735 100644
---
a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
+++
b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
@@ -69,6 +69,10 @@
{ "name": "camel.main.endpointBridgeErrorHandler", "required": false,
"description": "Allows for bridging the consumer to the Camel routing Error
Handler, which mean any exceptions occurred while the consumer is trying to
pickup incoming messages, or the likes, will now be processed as a message and
handled by the routing Error Handler. By default the consumer will use the
org.apache.camel.spi.ExceptionHandler to deal with exceptions, that will be
logged at WARN\/ERROR level and igno [...]
{ "name": "camel.main.endpointLazyStartProducer", "required": false,
"description": "Whether the producer should be started lazy (on the first
message). By starting lazy you can use this to allow CamelContext and routes to
startup in situations where a producer may otherwise fail during starting and
cause the route to fail being started. By deferring this startup to be lazy
then the startup failure can be handled during routing messages via Camel's
routing error handlers. Beware that [...]
{ "name": "camel.main.endpointRuntimeStatisticsEnabled", "required":
false, "description": "Sets whether endpoint runtime statistics is enabled
(gathers runtime usage of each incoming and outgoing endpoints). The default
value is false.", "sourceType":
"org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean",
"javaType": "boolean", "defaultValue": false, "secret": false },
+ { "name": "camel.main.errorRegistryEnabled", "required": false,
"description": "Sets whether the error registry is enabled to capture errors
during message routing. This is by default disabled.", "sourceType":
"org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean",
"javaType": "boolean", "defaultValue": false, "secret": false },
+ { "name": "camel.main.errorRegistryMaximumEntries", "required": false,
"description": "Sets the maximum number of error entries to keep in the error
registry. When the limit is exceeded, the oldest entries are evicted. The
default value is 100.", "sourceType":
"org.apache.camel.main.DefaultConfigurationProperties", "type": "integer",
"javaType": "int", "defaultValue": 100, "secret": false },
+ { "name": "camel.main.errorRegistryStackTraceEnabled", "required": false,
"description": "Sets whether to capture stack traces in the error registry.
This is enabled by default.", "sourceType":
"org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean",
"javaType": "boolean", "defaultValue": true, "secret": false },
+ { "name": "camel.main.errorRegistryTimeToLiveSeconds", "required": false,
"description": "Sets the time-to-live in seconds for error entries in the error
registry. Entries older than this are evicted. The default value is 3600 (1
hour).", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties",
"type": "integer", "javaType": "int", "defaultValue": 3600, "secret": false },
{ "name": "camel.main.exchangeFactory", "required": false, "description":
"Controls whether to pool (reuse) exchanges or create new exchanges
(prototype). Using pooled will reduce JVM garbage collection overhead by
avoiding to re-create Exchange instances per message each consumer receives.
The default is prototype mode.", "sourceType":
"org.apache.camel.main.DefaultConfigurationProperties", "type": "enum",
"javaType": "java.lang.String", "defaultValue": "default", "secret": false, "
[...]
{ "name": "camel.main.exchangeFactoryCapacity", "required": false,
"description": "The capacity the pool (for each consumer) uses for storing
exchanges. The default capacity is 100.", "sourceType":
"org.apache.camel.main.DefaultConfigurationProperties", "type": "integer",
"javaType": "int", "defaultValue": 100, "secret": false },
{ "name": "camel.main.exchangeFactoryStatisticsEnabled", "required":
false, "description": "Configures whether statistics is enabled on exchange
factory.", "sourceType":
"org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean",
"javaType": "boolean", "defaultValue": false, "secret": false },
diff --git a/core/camel-main/src/main/docs/main.adoc
b/core/camel-main/src/main/docs/main.adoc
index eb81817ede0f..db63d99a5d65 100644
--- a/core/camel-main/src/main/docs/main.adoc
+++ b/core/camel-main/src/main/docs/main.adoc
@@ -19,7 +19,7 @@ The following tables lists all the options:
// main options: START
=== Camel Main configurations
-The camel.main supports 127 options, which are listed below.
+The camel.main supports 131 options, which are listed below.
[width="100%",cols="2,5,^1,2",options="header"]
|===
@@ -64,6 +64,10 @@ The camel.main supports 127 options, which are listed below.
| *camel.main.endpointBridgeError{zwsp}Handler* | Allows for bridging the
consumer to the Camel routing Error Handler, which mean any exceptions occurred
while the consumer is trying to pickup incoming messages, or the likes, will
now be processed as a message and handled by the routing Error Handler. By
default the consumer will use the org.apache.camel.spi.ExceptionHandler to deal
with exceptions, that will be logged at WARN/ERROR level and ignored. The
default value is false. | false [...]
| *camel.main.endpointLazyStart{zwsp}Producer* | Whether the producer should
be started lazy (on the first message). By starting lazy you can use this to
allow CamelContext and routes to startup in situations where a producer may
otherwise fail during starting and cause the route to fail being started. By
deferring this startup to be lazy then the startup failure can be handled
during routing messages via Camel's routing error handlers. Beware that when
the first message is processed the [...]
| *camel.main.endpointRuntime{zwsp}StatisticsEnabled* | Sets whether endpoint
runtime statistics is enabled (gathers runtime usage of each incoming and
outgoing endpoints). The default value is false. | false | boolean
+| *camel.main.errorRegistry{zwsp}Enabled* | Sets whether the error registry is
enabled to capture errors during message routing. This is by default disabled.
| false | boolean
+| *camel.main.errorRegistry{zwsp}MaximumEntries* | Sets the maximum number of
error entries to keep in the error registry. When the limit is exceeded, the
oldest entries are evicted. The default value is 100. | 100 | int
+| *camel.main.errorRegistryStack{zwsp}TraceEnabled* | Sets whether to capture
stack traces in the error registry. This is enabled by default. | true | boolean
+| *camel.main.errorRegistryTimeTo{zwsp}LiveSeconds* | Sets the time-to-live in
seconds for error entries in the error registry. Entries older than this are
evicted. The default value is 3600 (1 hour). | 3600 | int
| *camel.main.exchangeFactory* | Controls whether to pool (reuse) exchanges or
create new exchanges (prototype). Using pooled will reduce JVM garbage
collection overhead by avoiding to re-create Exchange instances per message
each consumer receives. The default is prototype mode. | default | String
| *camel.main.exchangeFactory{zwsp}Capacity* | The capacity the pool (for each
consumer) uses for storing exchanges. The default capacity is 100. | 100 | int
| *camel.main.exchangeFactory{zwsp}StatisticsEnabled* | Configures whether
statistics is enabled on exchange factory. | false | boolean
diff --git
a/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java
b/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java
index 7aed2befc757..578c0a507a70 100644
---
a/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java
+++
b/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel.main;
+import java.time.Duration;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
@@ -201,6 +202,11 @@ public final class DefaultConfigurationConfigurer {
camelContext.getInflightRepository().setInflightBrowseEnabled(config.isInflightRepositoryBrowseEnabled());
+
camelContext.getErrorRegistry().setEnabled(config.isErrorRegistryEnabled());
+
camelContext.getErrorRegistry().setMaximumEntries(config.getErrorRegistryMaximumEntries());
+
camelContext.getErrorRegistry().setTimeToLive(Duration.ofSeconds(config.getErrorRegistryTimeToLiveSeconds()));
+
camelContext.getErrorRegistry().setStackTraceEnabled(config.isErrorRegistryStackTraceEnabled());
+
if (config.getLogDebugMaxChars() != 0) {
camelContext.getGlobalOptions().put(Exchange.LOG_DEBUG_BODY_MAX_CHARS,
Integer.toString(config.getLogDebugMaxChars()));
diff --git
a/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationProperties.java
b/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationProperties.java
index ff8a5923b22c..b0428f11b6cf 100644
---
a/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationProperties.java
+++
b/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationProperties.java
@@ -47,6 +47,14 @@ public abstract class DefaultConfigurationProperties<T> {
private boolean shutdownRoutesInReverseOrder = true;
private boolean shutdownLogInflightExchangesOnTimeout = true;
private boolean inflightRepositoryBrowseEnabled;
+ @Metadata(defaultValue = "false")
+ private boolean errorRegistryEnabled;
+ @Metadata(defaultValue = "100")
+ private int errorRegistryMaximumEntries = 100;
+ @Metadata(defaultValue = "3600")
+ private int errorRegistryTimeToLiveSeconds = 3600;
+ @Metadata(defaultValue = "true")
+ private boolean errorRegistryStackTraceEnabled = true;
private String fileConfigurations;
private boolean jmxEnabled = true;
@Metadata(enums = "classic,default,short,simple,off", defaultValue =
"default")
@@ -321,6 +329,57 @@ public abstract class DefaultConfigurationProperties<T> {
this.inflightRepositoryBrowseEnabled = inflightRepositoryBrowseEnabled;
}
+ public boolean isErrorRegistryEnabled() {
+ return errorRegistryEnabled;
+ }
+
+ /**
+ * Sets whether the error registry is enabled to capture errors during
message routing.
+ *
+ * This is by default disabled.
+ */
+ public void setErrorRegistryEnabled(boolean errorRegistryEnabled) {
+ this.errorRegistryEnabled = errorRegistryEnabled;
+ }
+
+ public int getErrorRegistryMaximumEntries() {
+ return errorRegistryMaximumEntries;
+ }
+
+ /**
+ * Sets the maximum number of error entries to keep in the error registry.
When the limit is exceeded, the oldest
+ * entries are evicted.
+ *
+ * The default value is 100.
+ */
+ public void setErrorRegistryMaximumEntries(int
errorRegistryMaximumEntries) {
+ this.errorRegistryMaximumEntries = errorRegistryMaximumEntries;
+ }
+
+ public int getErrorRegistryTimeToLiveSeconds() {
+ return errorRegistryTimeToLiveSeconds;
+ }
+
+ /**
+ * Sets the time-to-live in seconds for error entries in the error
registry. Entries older than this are evicted.
+ *
+ * The default value is 3600 (1 hour).
+ */
+ public void setErrorRegistryTimeToLiveSeconds(int
errorRegistryTimeToLiveSeconds) {
+ this.errorRegistryTimeToLiveSeconds = errorRegistryTimeToLiveSeconds;
+ }
+
+ public boolean isErrorRegistryStackTraceEnabled() {
+ return errorRegistryStackTraceEnabled;
+ }
+
+ /**
+ * Sets whether to capture stack traces in the error registry. This is
enabled by default.
+ */
+ public void setErrorRegistryStackTraceEnabled(boolean
errorRegistryStackTraceEnabled) {
+ this.errorRegistryStackTraceEnabled = errorRegistryStackTraceEnabled;
+ }
+
public String getFileConfigurations() {
return fileConfigurations;
}
@@ -1824,6 +1883,44 @@ public abstract class DefaultConfigurationProperties<T> {
return (T) this;
}
+ /**
+ * Sets whether the error registry is enabled to capture errors during
message routing.
+ *
+ * This is by default disabled.
+ */
+ public T withErrorRegistryEnabled(boolean errorRegistryEnabled) {
+ this.errorRegistryEnabled = errorRegistryEnabled;
+ return (T) this;
+ }
+
+ /**
+ * Sets the maximum number of error entries to keep in the error registry.
+ *
+ * The default value is 100.
+ */
+ public T withErrorRegistryMaximumEntries(int errorRegistryMaximumEntries) {
+ this.errorRegistryMaximumEntries = errorRegistryMaximumEntries;
+ return (T) this;
+ }
+
+ /**
+ * Sets the time-to-live in seconds for error entries in the error
registry.
+ *
+ * The default value is 3600 (1 hour).
+ */
+ public T withErrorRegistryTimeToLiveSeconds(int
errorRegistryTimeToLiveSeconds) {
+ this.errorRegistryTimeToLiveSeconds = errorRegistryTimeToLiveSeconds;
+ return (T) this;
+ }
+
+ /**
+ * Sets whether to capture stack traces in the error registry.
+ */
+ public T withErrorRegistryStackTraceEnabled(boolean
errorRegistryStackTraceEnabled) {
+ this.errorRegistryStackTraceEnabled = errorRegistryStackTraceEnabled;
+ return (T) this;
+ }
+
/**
* Directory to load additional configuration files that contains
configuration values that takes precedence over
* any other configuration. This can be used to refer to files that may
have secret configuration that has been
diff --git
a/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/CamelOpenMBeanTypes.java
b/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/CamelOpenMBeanTypes.java
index 48e83e07b00a..00445f80aa20 100644
---
a/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/CamelOpenMBeanTypes.java
+++
b/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/CamelOpenMBeanTypes.java
@@ -369,4 +369,23 @@ public final class CamelOpenMBeanTypes {
SimpleType.LONG, SimpleType.LONG, SimpleType.LONG,
SimpleType.LONG, SimpleType.STRING });
}
+ public static TabularType listErrorRegistryTabularType() throws
OpenDataException {
+ CompositeType ct = listErrorRegistryCompositeType();
+ return new TabularType("listErrors", "Lists captured routing errors",
ct, new String[] { "exchangeId" });
+ }
+
+ public static CompositeType listErrorRegistryCompositeType() throws
OpenDataException {
+ return new CompositeType(
+ "errors", "Errors",
+ new String[] {
+ "exchangeId", "routeId", "endpointUri", "timestamp",
+ "handled", "exceptionType", "exceptionMessage" },
+ new String[] {
+ "Exchange Id", "Route Id", "Endpoint Uri", "Timestamp",
+ "Handled", "Exception Type", "Exception Message" },
+ new OpenType[] {
+ SimpleType.STRING, SimpleType.STRING,
SimpleType.STRING, SimpleType.STRING,
+ SimpleType.BOOLEAN, SimpleType.STRING,
SimpleType.STRING });
+ }
+
}
diff --git
a/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedErrorRegistryMBean.java
b/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedErrorRegistryMBean.java
new file mode 100644
index 000000000000..a88f1ad6848f
--- /dev/null
+++
b/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedErrorRegistryMBean.java
@@ -0,0 +1,64 @@
+/*
+ * 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.api.management.mbean;
+
+import javax.management.openmbean.TabularData;
+
+import org.apache.camel.api.management.ManagedAttribute;
+import org.apache.camel.api.management.ManagedOperation;
+
+public interface ManagedErrorRegistryMBean extends ManagedServiceMBean {
+
+ @ManagedAttribute(description = "Whether the error registry is enabled")
+ boolean isEnabled();
+
+ @ManagedAttribute(description = "Whether the error registry is enabled")
+ void setEnabled(boolean enabled);
+
+ @ManagedAttribute(description = "Current number of error entries in the
registry")
+ int getSize();
+
+ @ManagedAttribute(description = "Maximum number of error entries to keep")
+ int getMaximumEntries();
+
+ @ManagedAttribute(description = "Maximum number of error entries to keep")
+ void setMaximumEntries(int maximumEntries);
+
+ @ManagedAttribute(description = "Time-to-live in seconds for error
entries")
+ long getTimeToLiveSeconds();
+
+ @ManagedAttribute(description = "Time-to-live in seconds for error
entries")
+ void setTimeToLiveSeconds(long seconds);
+
+ @ManagedAttribute(description = "Whether stack trace capture is enabled")
+ boolean isStackTraceEnabled();
+
+ @ManagedAttribute(description = "Whether stack trace capture is enabled")
+ void setStackTraceEnabled(boolean stackTraceEnabled);
+
+ @ManagedOperation(description = "Browse all error entries")
+ TabularData browse();
+
+ @ManagedOperation(description = "Browse error entries with a limit")
+ TabularData browse(int limit);
+
+ @ManagedOperation(description = "Browse error entries for a specific
route")
+ TabularData browse(String routeId, int limit);
+
+ @ManagedOperation(description = "Clear all error entries")
+ void clear();
+}
diff --git
a/core/camel-management/src/main/java/org/apache/camel/management/JmxManagementLifecycleStrategy.java
b/core/camel-management/src/main/java/org/apache/camel/management/JmxManagementLifecycleStrategy.java
index b3629136e178..be5a14327610 100644
---
a/core/camel-management/src/main/java/org/apache/camel/management/JmxManagementLifecycleStrategy.java
+++
b/core/camel-management/src/main/java/org/apache/camel/management/JmxManagementLifecycleStrategy.java
@@ -63,6 +63,7 @@ import
org.apache.camel.management.mbean.ManagedDumpRouteStrategy;
import org.apache.camel.management.mbean.ManagedEndpoint;
import org.apache.camel.management.mbean.ManagedEndpointRegistry;
import org.apache.camel.management.mbean.ManagedEndpointServiceRegistry;
+import org.apache.camel.management.mbean.ManagedErrorRegistry;
import org.apache.camel.management.mbean.ManagedExchangeFactoryManager;
import org.apache.camel.management.mbean.ManagedInflightRepository;
import org.apache.camel.management.mbean.ManagedProducerCache;
@@ -97,6 +98,7 @@ import org.apache.camel.spi.DataFormat;
import org.apache.camel.spi.DumpRoutesStrategy;
import org.apache.camel.spi.EndpointRegistry;
import org.apache.camel.spi.EndpointServiceRegistry;
+import org.apache.camel.spi.ErrorRegistry;
import org.apache.camel.spi.EventNotifier;
import org.apache.camel.spi.ExchangeFactoryManager;
import org.apache.camel.spi.InflightRepository;
@@ -583,6 +585,8 @@ public class JmxManagementLifecycleStrategy extends
ServiceSupport implements Li
answer = new ManagedEndpointServiceRegistry(context,
endpointServiceRegistry);
} else if (service instanceof InflightRepository inflightRepository) {
answer = new ManagedInflightRepository(context,
inflightRepository);
+ } else if (service instanceof ErrorRegistry errorRegistry) {
+ answer = new ManagedErrorRegistry(context, errorRegistry);
} else if (service instanceof AsyncProcessorAwaitManager
asyncProcessorAwaitManager) {
answer = new ManagedAsyncProcessorAwaitManager(context,
asyncProcessorAwaitManager);
} else if (service instanceof RuntimeEndpointRegistry
runtimeEndpointRegistry) {
diff --git
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedErrorRegistry.java
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedErrorRegistry.java
new file mode 100644
index 000000000000..eab3bcccf54f
--- /dev/null
+++
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedErrorRegistry.java
@@ -0,0 +1,140 @@
+/*
+ * 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.management.mbean;
+
+import java.time.Duration;
+import java.util.Collection;
+
+import javax.management.openmbean.CompositeData;
+import javax.management.openmbean.CompositeDataSupport;
+import javax.management.openmbean.CompositeType;
+import javax.management.openmbean.TabularData;
+import javax.management.openmbean.TabularDataSupport;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.api.management.ManagedResource;
+import org.apache.camel.api.management.mbean.CamelOpenMBeanTypes;
+import org.apache.camel.api.management.mbean.ManagedErrorRegistryMBean;
+import org.apache.camel.spi.ErrorRegistry;
+import org.apache.camel.spi.ErrorRegistryEntry;
+
+@ManagedResource(description = "Managed ErrorRegistry")
+public class ManagedErrorRegistry extends ManagedService implements
ManagedErrorRegistryMBean {
+
+ private final ErrorRegistry errorRegistry;
+
+ public ManagedErrorRegistry(CamelContext context, ErrorRegistry
errorRegistry) {
+ super(context, errorRegistry);
+ this.errorRegistry = errorRegistry;
+ }
+
+ public ErrorRegistry getErrorRegistry() {
+ return errorRegistry;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return errorRegistry.isEnabled();
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ errorRegistry.setEnabled(enabled);
+ }
+
+ @Override
+ public int getSize() {
+ return errorRegistry.size();
+ }
+
+ @Override
+ public int getMaximumEntries() {
+ return errorRegistry.getMaximumEntries();
+ }
+
+ @Override
+ public void setMaximumEntries(int maximumEntries) {
+ errorRegistry.setMaximumEntries(maximumEntries);
+ }
+
+ @Override
+ public long getTimeToLiveSeconds() {
+ return errorRegistry.getTimeToLive().toSeconds();
+ }
+
+ @Override
+ public void setTimeToLiveSeconds(long seconds) {
+ errorRegistry.setTimeToLive(Duration.ofSeconds(seconds));
+ }
+
+ @Override
+ public boolean isStackTraceEnabled() {
+ return errorRegistry.isStackTraceEnabled();
+ }
+
+ @Override
+ public void setStackTraceEnabled(boolean stackTraceEnabled) {
+ errorRegistry.setStackTraceEnabled(stackTraceEnabled);
+ }
+
+ @Override
+ public TabularData browse() {
+ return browseEntries(errorRegistry.browse());
+ }
+
+ @Override
+ public TabularData browse(int limit) {
+ return browseEntries(errorRegistry.browse(limit));
+ }
+
+ @Override
+ public TabularData browse(String routeId, int limit) {
+ return browseEntries(errorRegistry.forRoute(routeId).browse(limit));
+ }
+
+ @Override
+ public void clear() {
+ errorRegistry.clear();
+ }
+
+ private static TabularData browseEntries(Collection<ErrorRegistryEntry>
entries) {
+ try {
+ TabularData answer = new
TabularDataSupport(CamelOpenMBeanTypes.listErrorRegistryTabularType());
+ for (ErrorRegistryEntry entry : entries) {
+ CompositeType ct =
CamelOpenMBeanTypes.listErrorRegistryCompositeType();
+ CompositeData data = new CompositeDataSupport(
+ ct,
+ new String[] {
+ "exchangeId", "routeId", "endpointUri",
"timestamp",
+ "handled", "exceptionType", "exceptionMessage"
},
+ new Object[] {
+ entry.exchangeId(),
+ entry.routeId(),
+ entry.endpointUri(),
+ entry.timestamp().toString(),
+ entry.handled(),
+ entry.exceptionType(),
+ entry.exceptionMessage() });
+ answer.put(data);
+ }
+ return answer;
+ } catch (Exception e) {
+ throw RuntimeCamelException.wrapRuntimeCamelException(e);
+ }
+ }
+}
diff --git
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedNonManagedServiceTest.java
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedNonManagedServiceTest.java
index c29db8af285d..5ca719293c80 100644
---
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedNonManagedServiceTest.java
+++
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedNonManagedServiceTest.java
@@ -34,7 +34,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
@DisabledOnOs(OS.AIX)
public class ManagedNonManagedServiceTest extends ManagementTestSupport {
- private static final int SERVICES = 17;
+ private static final int SERVICES = 18;
@Test
public void testService() throws Exception {
diff --git
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedProducerRouteAddRemoveRegisterAlwaysTest.java
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedProducerRouteAddRemoveRegisterAlwaysTest.java
index 0ff9794d08f0..4b4f23e0e14b 100644
---
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedProducerRouteAddRemoveRegisterAlwaysTest.java
+++
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedProducerRouteAddRemoveRegisterAlwaysTest.java
@@ -36,7 +36,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
@DisabledOnOs(OS.AIX)
public class ManagedProducerRouteAddRemoveRegisterAlwaysTest extends
ManagementTestSupport {
- private static final int SERVICES = 17;
+ private static final int SERVICES = 18;
@Override
protected CamelContext createCamelContext() throws Exception {
diff --git
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteAddRemoveTest.java
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteAddRemoveTest.java
index 1256143c2d60..7bcc688d0034 100644
---
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteAddRemoveTest.java
+++
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteAddRemoveTest.java
@@ -40,7 +40,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
@DisabledOnOs(OS.AIX)
public class ManagedRouteAddRemoveTest extends ManagementTestSupport {
- private static final int SERVICES = 17;
+ private static final int SERVICES = 18;
@Override
protected RouteBuilder createRouteBuilder() {
diff --git a/docs/user-manual/modules/ROOT/nav.adoc
b/docs/user-manual/modules/ROOT/nav.adoc
index b306976c40de..8dc47cfba8dc 100644
--- a/docs/user-manual/modules/ROOT/nav.adoc
+++ b/docs/user-manual/modules/ROOT/nav.adoc
@@ -23,6 +23,7 @@
** xref:examples.adoc[Examples]
** xref:graceful-shutdown.adoc[Graceful Shutdown]
** xref:error-handler.adoc[Error handler]
+** xref:error-registry.adoc[Error Registry]
** xref:using-propertyplaceholder.adoc[How to use Camel property placeholders]
** xref:variables.adoc[How to use Variables]
** xref:testing.adoc[Testing]
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_19.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_19.adoc
index f13cb995465c..8d1a3a836749 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_19.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_19.adoc
@@ -32,6 +32,11 @@ uses `InOnly` pattern.
Removed 2 deprecated methods in Java DSL for `throttler` EIP.
+==== ErrorRegistry
+
+A new `ErrorRegistry` SPI has been added to `CamelContext` for capturing
routing errors.
+See xref:error-registry.adoc[Error Registry] for more details.
+
==== Saga EIP
The Saga EIP has _fixed_ the model for how to configure completion and
compensation URIs.
diff --git a/docs/user-manual/modules/ROOT/pages/error-registry.adoc
b/docs/user-manual/modules/ROOT/pages/error-registry.adoc
new file mode 100644
index 000000000000..6ead556efaea
--- /dev/null
+++ b/docs/user-manual/modules/ROOT/pages/error-registry.adoc
@@ -0,0 +1,60 @@
+= Error Registry
+
+The `ErrorRegistry` SPI captures snapshots of exceptions that occur during
message routing,
+without retaining references to the original exchange or exception objects.
+
+When enabled, the registry stores error snapshots including the exception
type, message,
+stack trace, and message history (the trace of every node the message was
routed through).
+Error entries can be browsed globally or scoped to a specific route.
+
+== Enabling
+
+The error registry is disabled by default (opt-in). It can be enabled via Java:
+
+[source,java]
+----
+context.getErrorRegistry().setEnabled(true);
+----
+
+Or via configuration properties:
+
+[source,properties]
+----
+camel.main.errorRegistryEnabled = true
+----
+
+== Configuration
+
+[width="100%",cols="3,1,1,5",options="header"]
+|===
+| Option | Default | Type | Description
+| `camel.main.errorRegistryEnabled` | `false` | boolean | Whether the error
registry is enabled.
+| `camel.main.errorRegistryMaximumEntries` | `100` | int | Maximum number of
error entries to keep. When exceeded, the oldest entries are evicted.
+| `camel.main.errorRegistryTimeToLiveSeconds` | `3600` | int | Time-to-live
for error entries in seconds. Entries older than this are evicted.
+| `camel.main.errorRegistryStackTraceEnabled` | `true` | boolean | Whether to
capture stack traces.
+|===
+
+== Message History
+
+When xref:components:eips:message-history.adoc[Message History] is enabled on
the `CamelContext`, the error registry
+also captures the message history trace for each error. This shows every node
the message was
+routed through up until the point of failure, which is very useful for
debugging.
+
+== Dev Console
+
+A dev console named `errors` is available for browsing captured errors.
+
+Options:
+
+* `routeId` — filter by route id
+* `limit` — limit the number of entries displayed
+* `stackTrace` — set to `true` to include stack traces in the output (stack
traces are captured by default but not displayed unless this option is set, to
keep the output concise)
+
+== JMX
+
+A JMX MBean (`ManagedErrorRegistry`) is registered when JMX is enabled,
providing operations to:
+
+* Enable/disable the error registry
+* Browse error entries (globally or by route)
+* Configure maximum entries and time-to-live
+* Clear all entries