This is an automated email from the ASF dual-hosted git repository. gnodet pushed a commit to branch CAMEL-23079/error-registry in repository https://gitbox.apache.org/repos/asf/camel.git
commit 4feda3e37163d53e018692a1e3c7a870feab7782 Author: Guillaume Nodet <[email protected]> AuthorDate: Fri Mar 6 10:40:13 2026 +0100 CAMEL-23079: Add ErrorRegistry SPI for capturing routing errors Add a new ErrorRegistry service that captures exceptions during message routing and stores immutable snapshots in bounded in-memory storage. - ErrorRegistry SPI with ErrorRegistryEntry and ErrorRegistryView interfaces - DefaultErrorRegistry implementation using EventNotifier pattern - Configurable max entries, TTL, and optional stack trace capture - Route-scoped views via forRoute(routeId) - JMX MBean for management and monitoring - Dev Console for browsing errors - Configuration properties (camel.main.errorRegistry*) - Disabled by default, opt-in via setEnabled(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 | 69 +++++ .../org/apache/camel/spi/ErrorRegistryView.java | 47 ++++ .../camel/impl/engine/AbstractCamelContext.java | 19 ++ .../impl/engine/DefaultCamelContextExtension.java | 20 ++ .../camel/impl/engine/DefaultErrorRegistry.java | 289 +++++++++++++++++++++ .../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 | 134 ++++++++++ .../apache/camel/impl/CamelContextConfigurer.java | 6 + .../org/apache/camel/impl/ErrorRegistryTest.java | 174 +++++++++++++ .../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 | 95 +++++++ .../api/management/mbean/CamelOpenMBeanTypes.java | 19 ++ .../mbean/ManagedErrorRegistryMBean.java | 64 +++++ .../management/JmxManagementLifecycleStrategy.java | 4 + .../management/mbean/ManagedErrorRegistry.java | 140 ++++++++++ 26 files changed, 1278 insertions(+), 2 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 e0e8c6b15390..2656901d4cb7 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 disabled by default to reduce memory usage.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "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..7f00f7ebe6ac --- /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 disabled by default to reduce memory usage. + */ + 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..a48e62895032 --- /dev/null +++ b/core/camel-api/src/main/java/org/apache/camel/spi/ErrorRegistryEntry.java @@ -0,0 +1,69 @@ +/* + * 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(); +} 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..50e8a470bc59 --- /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 read-only 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..2a8f006d60df --- /dev/null +++ b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java @@ -0,0 +1,289 @@ +/* + * 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.NonManagedService; +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, NonManagedService { + + 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; + + public DefaultErrorRegistry() { + // only listen to exchange failure events + setIgnoreCamelContextEvents(true); + setIgnoreCamelContextInitEvents(true); + setIgnoreRouteEvents(true); + setIgnoreServiceEvents(true); + setIgnoreExchangeCreatedEvent(true); + setIgnoreExchangeCompletedEvent(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; + + DefaultErrorRegistryEntry entry = new DefaultErrorRegistryEntry( + exchangeId, routeId, endpointUri, Instant.now(), + handled, exceptionType, exceptionMessage, stackTrace); + + entries.addFirst(entry); + evict(); + } + + private static String[] captureStackTrace(Throwable exception) { + StringWriter writer = new StringWriter(); + exception.printStackTrace(new PrintWriter(writer, true)); + return writer.toString().split("\n"); + } + + 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; + } + + @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) + implements + ErrorRegistryEntry { + } +} 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..7dd8229f071e --- /dev/null +++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/ErrorRegistryConsole.java @@ -0,0 +1,134 @@ +/* + * 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); + String limit = (String) options.get(LIMIT); + int max = limit == null ? Integer.MAX_VALUE : Integer.parseInt(limit); + 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 (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); + String limit = (String) options.get(LIMIT); + int max = limit == null ? Integer.MAX_VALUE : Integer.parseInt(limit); + 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 (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; + } +} 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..f06ca81b5ef1 --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryTest.java @@ -0,0 +1,174 @@ +/* + * 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.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); + 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()); + assertNull(entry.stackTrace()); + } + + @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 { + context.getErrorRegistry().setStackTraceEnabled(true); + + 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 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()); + } + + @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")); + } + }; + } +} 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 5ee376adff5d..7bd63898f023 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); @@ -233,6 +237,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": @@ -491,6 +503,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": @@ -745,6 +765,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 e0e8c6b15390..2656901d4cb7 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 disabled by default to reduce memory usage.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "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 179bc82bb8e3..4484d57b1015 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 126 options, which are listed below. +The camel.main supports 130 options, which are listed below. [width="100%",cols="2,5,^1,2",options="header"] |=== @@ -64,6 +64,10 @@ The camel.main supports 126 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 disabled by default to reduce memory usage. | false | 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..ed3c25bede42 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,12 @@ public abstract class DefaultConfigurationProperties<T> { private boolean shutdownRoutesInReverseOrder = true; private boolean shutdownLogInflightExchangesOnTimeout = true; private boolean inflightRepositoryBrowseEnabled; + private boolean errorRegistryEnabled; + @Metadata(defaultValue = "100") + private int errorRegistryMaximumEntries = 100; + @Metadata(defaultValue = "3600") + private int errorRegistryTimeToLiveSeconds = 3600; + private boolean errorRegistryStackTraceEnabled; private String fileConfigurations; private boolean jmxEnabled = true; @Metadata(enums = "classic,default,short,simple,off", defaultValue = "default") @@ -321,6 +327,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 disabled by default to reduce memory usage. + */ + public void setErrorRegistryStackTraceEnabled(boolean errorRegistryStackTraceEnabled) { + this.errorRegistryStackTraceEnabled = errorRegistryStackTraceEnabled; + } + public String getFileConfigurations() { return fileConfigurations; } @@ -1824,6 +1881,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); + } + } +}
