This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-23533 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 6212f67ee1c62376cf0afb6c0711c594d763ddc7 Author: Claus Ibsen <[email protected]> AuthorDate: Wed May 27 11:36:59 2026 +0200 CAMEL-23533: ErrorRegistry - capture rich exchange snapshots following BacklogTracer pattern Replaces ErrorRegistryEntry with BacklogErrorEventMessage extending a new shared BacklogEventMessage base interface. Error snapshots now capture detached exchange data (body, headers, properties, variables), message history, route group, endpoint URI, and the full exception. Configuration moves from camel.main.errorRegistry* to a dedicated camel.errorRegistry.* property group with expanded options. Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../main/camel-main-configuration-metadata.json | 15 +- .../apache/camel/spi/BacklogErrorEventMessage.java | 68 ++++ .../org/apache/camel/spi/BacklogEventMessage.java | 115 +++++++ .../camel/spi/BacklogTracerEventMessage.java | 2 +- .../java/org/apache/camel/spi/ErrorRegistry.java | 67 +++- .../org/apache/camel/spi/ErrorRegistryEntry.java | 82 ----- .../org/apache/camel/spi/ErrorRegistryView.java | 4 +- .../camel/impl/engine/DefaultErrorRegistry.java | 349 +++++++++++++++++---- .../camel/impl/console/ErrorRegistryConsole.java | 59 ++-- .../org/apache/camel/impl/ErrorRegistryTest.java | 137 ++++---- ...rRegistryConfigurationPropertiesConfigurer.java | 109 +++++++ .../MainConfigurationPropertiesConfigurer.java | 28 -- .../camel-main-configuration-metadata.json | 15 +- ...camel.main.ErrorRegistryConfigurationProperties | 2 + core/camel-main/src/main/docs/main.adoc | 23 +- .../org/apache/camel/main/BaseMainSupport.java | 43 ++- .../camel/main/DefaultConfigurationConfigurer.java | 6 +- .../camel/main/DefaultConfigurationProperties.java | 97 ------ .../main/ErrorRegistryConfigurationProperties.java | 222 +++++++++++++ .../camel/main/MainConfigurationProperties.java | 23 ++ .../api/management/mbean/CamelOpenMBeanTypes.java | 7 +- .../mbean/ManagedErrorRegistryMBean.java | 32 +- .../management/mbean/ManagedErrorRegistry.java | 72 ++++- .../ROOT/pages/camel-4x-upgrade-guide-4_21.adoc | 29 ++ .../modules/ROOT/pages/error-registry.adoc | 42 ++- .../maven/packaging/PrepareCamelMainMojo.java | 5 + 26 files changed, 1232 insertions(+), 421 deletions(-) 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 bdf518fd508a..8699514fd905 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 @@ -27,7 +27,8 @@ { "name": "camel.metrics", "description": "Camel Micrometer Metrics configurations", "sourceType": "org.apache.camel.main.MetricsConfigurationProperties" }, { "name": "camel.faulttolerance", "description": "Fault Tolerance EIP Circuit Breaker configurations", "sourceType": "org.apache.camel.main.FaultToleranceConfigurationProperties" }, { "name": "camel.resilience4j", "description": "Resilience4j EIP Circuit Breaker configurations", "sourceType": "org.apache.camel.main.Resilience4jConfigurationProperties" }, - { "name": "camel.lra", "description": "Camel Saga EIP (Long Running Actions) configurations", "sourceType": "org.apache.camel.main.LraConfigurationProperties" } + { "name": "camel.lra", "description": "Camel Saga EIP (Long Running Actions) configurations", "sourceType": "org.apache.camel.main.LraConfigurationProperties" }, + { "name": "camel.errorRegistry", "description": "Camel Error Registry configurations", "sourceType": "org.apache.camel.main.ErrorRegistryConfigurationProperties" } ], "properties": [ { "name": "camel.main.additionalSensitiveKeywords", "required": false, "description": "Camel comes with a default set of sensitive keywords which are automatically masked. This option allows to add additional custom keywords to be masked as well. Multiple keywords can be separated by comma.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "string", "javaType": "java.lang.String", "secret": false }, @@ -70,10 +71,6 @@ { "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 }, @@ -178,6 +175,14 @@ { "name": "camel.debug.singleStepIncludeStartEnd", "required": false, "description": "In single step mode, then when the exchange is created and completed, then simulate a breakpoint at start and end, that allows to suspend and watch the incoming\/complete exchange at the route (you can see message body as response, failed exception etc).", "sourceType": "org.apache.camel.main.DebuggerConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": [...] { "name": "camel.debug.standby", "required": false, "description": "To set the debugger in standby mode, where the debugger will be installed by not automatic enabled. The debugger can then later be enabled explicit from Java, JMX or tooling.", "sourceType": "org.apache.camel.main.DebuggerConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, { "name": "camel.debug.waitForAttach", "required": false, "description": "Whether the debugger should suspend on startup, and wait for a remote debugger to attach. This is what the IDEA and VSCode tooling is using.", "sourceType": "org.apache.camel.main.DebuggerConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, + { "name": "camel.errorRegistry.bodyIncludeFiles", "required": false, "description": "Whether to include the message body of file based messages. The overhead is that the file content has to be read from the file.", "sourceType": "org.apache.camel.main.ErrorRegistryConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": true, "secret": false }, + { "name": "camel.errorRegistry.bodyIncludeStreams", "required": false, "description": "Whether to include the message body of stream based messages. If enabled then beware the stream may not be re-readable later. See more about Stream Caching.", "sourceType": "org.apache.camel.main.ErrorRegistryConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, + { "name": "camel.errorRegistry.bodyMaxChars", "required": false, "description": "To limit the message body to a maximum size in the captured error data. Use 0 or negative value to use unlimited size.", "sourceType": "org.apache.camel.main.ErrorRegistryConfigurationProperties", "type": "integer", "javaType": "int", "defaultValue": 32768, "secret": false }, + { "name": "camel.errorRegistry.enabled", "required": false, "description": "Whether the error registry is enabled to capture errors during message routing.", "sourceType": "org.apache.camel.main.ErrorRegistryConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, + { "name": "camel.errorRegistry.includeExchangeProperties", "required": false, "description": "Whether to include the exchange properties in the captured error data.", "sourceType": "org.apache.camel.main.ErrorRegistryConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": true, "secret": false }, + { "name": "camel.errorRegistry.includeExchangeVariables", "required": false, "description": "Whether to include the exchange variables in the captured error data.", "sourceType": "org.apache.camel.main.ErrorRegistryConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": true, "secret": false }, + { "name": "camel.errorRegistry.maximumEntries", "required": false, "description": "The maximum number of error entries to keep in the registry. When the limit is exceeded, the oldest entries are evicted.", "sourceType": "org.apache.camel.main.ErrorRegistryConfigurationProperties", "type": "integer", "javaType": "int", "defaultValue": 100, "secret": false }, + { "name": "camel.errorRegistry.timeToLiveSeconds", "required": false, "description": "The time-to-live in seconds for error entries. Entries older than this are evicted.", "sourceType": "org.apache.camel.main.ErrorRegistryConfigurationProperties", "type": "integer", "javaType": "int", "defaultValue": 3600, "secret": false }, { "name": "camel.faulttolerance.bulkheadEnabled", "required": false, "description": "Whether bulkhead is enabled or not on the circuit breaker. Default is false.", "sourceType": "org.apache.camel.main.FaultToleranceConfigurationProperties", "type": "boolean", "javaType": "java.lang.Boolean", "defaultValue": false, "secret": false }, { "name": "camel.faulttolerance.bulkheadMaxConcurrentCalls", "required": false, "description": "Configures the max amount of concurrent calls the bulkhead will support. Default value is 10.", "sourceType": "org.apache.camel.main.FaultToleranceConfigurationProperties", "type": "integer", "javaType": "java.lang.Integer", "defaultValue": 10, "secret": false }, { "name": "camel.faulttolerance.bulkheadWaitingTaskQueue", "required": false, "description": "Configures the task queue size for holding waiting tasks to be processed by the bulkhead. Default value is 10.", "sourceType": "org.apache.camel.main.FaultToleranceConfigurationProperties", "type": "integer", "javaType": "java.lang.Integer", "defaultValue": 10, "secret": false }, diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/BacklogErrorEventMessage.java b/core/camel-api/src/main/java/org/apache/camel/spi/BacklogErrorEventMessage.java new file mode 100644 index 000000000000..0418c1d15e65 --- /dev/null +++ b/core/camel-api/src/main/java/org/apache/camel/spi/BacklogErrorEventMessage.java @@ -0,0 +1,68 @@ +/* + * 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 org.jspecify.annotations.Nullable; + +/** + * Represents an error event captured by the {@link ErrorRegistry}. + * <p/> + * This extends {@link BacklogEventMessage} with error-specific information such as the exception, whether the error was + * handled by an error handler, the route group, and the message history trace. + * + * @since 4.21 + */ +public interface BacklogErrorEventMessage extends BacklogEventMessage { + + /** + * The route group of the route where the error occurred, or {@code null} if the route has no group assigned. + */ + @Nullable + String getRouteGroup(); + + /** + * The actual exception that caused the error. This is the live {@link Throwable} instance, not a serialized copy. + */ + Throwable getException(); + + /** + * Whether the error was handled by an error handler (e.g. dead letter channel, onException) so the exchange + * completed without propagating the exception to the caller. {@code false} means the exception was not handled and + * the exchange failed. + */ + boolean isHandled(); + + /** + * The fully qualified class name of the exception (e.g. "java.lang.IllegalArgumentException"). + */ + String getExceptionType(); + + /** + * The exception's detail message from {@link Throwable#getMessage()}, or {@code null} if the exception has no + * message. + */ + @Nullable + String getExceptionMessage(); + + /** + * The message history trace captured at the time of the error, or {@code null} if message history is not enabled on + * the CamelContext. + * <p/> + * Each element represents one step in the routing history in the format "routeId[nodeId] (elapsed ms)". + */ + String @Nullable [] getMessageHistory(); +} diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/BacklogEventMessage.java b/core/camel-api/src/main/java/org/apache/camel/spi/BacklogEventMessage.java new file mode 100644 index 000000000000..d8e26e282aa2 --- /dev/null +++ b/core/camel-api/src/main/java/org/apache/camel/spi/BacklogEventMessage.java @@ -0,0 +1,115 @@ +/* + * 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.Map; + +import org.jspecify.annotations.Nullable; + +/** + * Base interface for backlog event messages that capture exchange snapshots at a point in time, without retaining + * references to the live {@link org.apache.camel.Exchange}. + * <p/> + * This is the shared contract between {@link BacklogTracerEventMessage} and {@link BacklogErrorEventMessage}. + * + * @since 4.21 + */ +public interface BacklogEventMessage { + + /** + * Unique monotonically increasing identifier for this event message. + */ + long getUid(); + + /** + * Timestamp when the event was captured (milliseconds since epoch). + */ + long getTimestamp(); + + /** + * Source file location of the node where the event occurred (e.g. "MyRoute.java:42"), or {@code null} if source + * location tracking is not enabled. + */ + @Nullable + String getLocation(); + + /** + * The id of the route where the event occurred. For errors this is the route where the failure happened, which may + * differ from {@link #getFromRouteId()} if the exchange was routed across multiple routes. + */ + String getRouteId(); + + /** + * The id of the route where the exchange originally started (the consumer route), or {@code null} if not available. + */ + @Nullable + String getFromRouteId(); + + /** + * The unique exchange identifier. + */ + String getExchangeId(); + + /** + * The URI of the endpoint the exchange was being sent to (producer endpoint) when the event was captured. This can + * be {@code null} when the failure occurs before any send attempt (e.g. a direct {@code throwException} in the + * route). This does <b>not</b> refer to the route's consumer (from) endpoint. + */ + @Nullable + String getEndpointUri(); + + /** + * The id of the EIP processor node where the event occurred, or {@code null} if not available. + */ + @Nullable + String getToNode(); + + /** + * The name of the thread that was processing the exchange when this event was captured. + */ + String getProcessingThreadName(); + + /** + * A detached snapshot of the exchange message content as JSon, including the message body, headers, and optionally + * exchange properties and variables. + */ + String getMessageAsJSon(); + + /** + * Whether this event has an associated exception. + */ + boolean hasException(); + + /** + * The exception details as JSon (type, message, and stack trace), or {@code null} if no exception is present. + */ + @Nullable + String getExceptionAsJSon(); + + /** + * Dumps the full event message as a pretty-printed JSon string. + * + * @param indent number of spaces to indent + * @return JSon representation of this event + */ + String toJSon(int indent); + + /** + * The full event message as a {@link Map} suitable for JSon serialization, containing all fields of this event. + */ + Map<String, Object> asJSon(); +} diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/BacklogTracerEventMessage.java b/core/camel-api/src/main/java/org/apache/camel/spi/BacklogTracerEventMessage.java index 219db97a54ba..7956e8587e53 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/BacklogTracerEventMessage.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/BacklogTracerEventMessage.java @@ -26,7 +26,7 @@ import org.jspecify.annotations.Nullable; * * @since 4.0 */ -public interface BacklogTracerEventMessage { +public interface BacklogTracerEventMessage extends BacklogEventMessage { String ROOT_TAG = "backlogTracerEventMessage"; String TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; 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 index 67005e0bfb88..4dc302572f90 100644 --- 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 @@ -23,15 +23,16 @@ 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. + * This is an opt-in feature that must be enabled. When enabled, the registry captures error snapshots including + * exception details, exchange data (headers, body, properties, variables), and routing context — without retaining + * references to the original exchange 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 BacklogErrorEventMessage * @see ErrorRegistryView * @since 4.19 */ @@ -85,13 +86,65 @@ public interface ErrorRegistry extends ErrorRegistryView, StaticService { */ void setTimeToLive(Duration timeToLive); + // -- Exchange data capture options -- + + /** + * Maximum number of characters to keep for the message body (to prevent storing very big payloads). + */ + int getBodyMaxChars(); + + /** + * Sets the maximum number of characters to keep for the message body. + * <p/> + * The default value is 32768 (32kb). + */ + void setBodyMaxChars(int bodyMaxChars); + + /** + * Whether to include message body from streams. + */ + boolean isBodyIncludeStreams(); + + /** + * Sets whether to include message body from streams. + * <p/> + * This is by default disabled because reading from a stream is a destructive operation. + */ + void setBodyIncludeStreams(boolean bodyIncludeStreams); + /** - * Whether stack trace capture is enabled + * Whether to include message body from files. */ - boolean isStackTraceEnabled(); + boolean isBodyIncludeFiles(); /** - * Sets whether to capture stack traces. This is enabled by default. + * Sets whether to include message body from files. + * <p/> + * This is by default enabled. + */ + void setBodyIncludeFiles(boolean bodyIncludeFiles); + + /** + * Whether to include exchange properties in the captured error data. + */ + boolean isIncludeExchangeProperties(); + + /** + * Sets whether to include exchange properties. + * <p/> + * This is by default enabled. + */ + void setIncludeExchangeProperties(boolean includeExchangeProperties); + + /** + * Whether to include exchange variables in the captured error data. + */ + boolean isIncludeExchangeVariables(); + + /** + * Sets whether to include exchange variables. + * <p/> + * This is by default enabled. */ - void setStackTraceEnabled(boolean stackTraceEnabled); + void setIncludeExchangeVariables(boolean includeExchangeVariables); } 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 deleted file mode 100644 index fb2537da3feb..000000000000 --- a/core/camel-api/src/main/java/org/apache/camel/spi/ErrorRegistryEntry.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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; - -import org.jspecify.annotations.Nullable; - -/** - * 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. - * - * @since 4.19 - */ -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) - */ - @Nullable - 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 - */ - @Nullable - 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 @Nullable [] 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 @Nullable [] 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 index c7c5b4cea36d..f336b92bb941 100644 --- 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 @@ -33,14 +33,14 @@ public interface ErrorRegistryView { /** * Browse all error entries, sorted by most recent first */ - Collection<ErrorRegistryEntry> browse(); + Collection<BacklogErrorEventMessage> 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); + Collection<BacklogErrorEventMessage> browse(int limit); /** * Clear all error entries in this view diff --git a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java index 93509f5c8227..d362482d0ffd 100644 --- a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java +++ b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java @@ -16,44 +16,51 @@ */ 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.Map; import java.util.Objects; import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicLong; import org.apache.camel.Exchange; import org.apache.camel.ExchangePropertyKey; import org.apache.camel.MessageHistory; +import org.apache.camel.spi.BacklogErrorEventMessage; 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; +import org.apache.camel.support.MessageHelper; +import org.apache.camel.util.json.JsonObject; +import org.apache.camel.util.json.Jsonable; +import org.apache.camel.util.json.Jsoner; /** * 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 final ConcurrentLinkedDeque<BacklogErrorEventMessage> entries = new ConcurrentLinkedDeque<>(); + private final AtomicLong uidCounter = new AtomicLong(); private volatile boolean enabled; private volatile int maximumEntries = 100; private volatile Duration timeToLive = Duration.ofHours(1); - private volatile boolean stackTraceEnabled = true; + private volatile int bodyMaxChars = 32 * 1024; + private volatile boolean bodyIncludeStreams; + private volatile boolean bodyIncludeFiles = true; + private volatile boolean includeExchangeProperties = true; + private volatile boolean includeExchangeVariables = 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); @@ -90,7 +97,6 @@ public class DefaultErrorRegistry extends EventNotifierSupport implements ErrorR 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(); @@ -99,31 +105,47 @@ public class DefaultErrorRegistry extends EventNotifierSupport implements ErrorR return; } + long uid = uidCounter.incrementAndGet(); + long timestamp = System.currentTimeMillis(); String exchangeId = exchange.getExchangeId(); String routeId = exchange.getProperty(ExchangePropertyKey.FAILURE_ROUTE_ID, String.class); if (routeId == null) { routeId = exchange.getFromRouteId(); } + String fromRouteId = exchange.getFromRouteId(); + String routeGroup = null; + if (routeId != null) { + org.apache.camel.Route route = exchange.getContext().getRoute(routeId); + if (route != null) { + routeGroup = route.getGroup(); + } + } String endpointUri = exchange.getProperty(ExchangePropertyKey.FAILURE_ENDPOINT, String.class); - String exceptionType = exception.getClass().getName(); - String exceptionMessage = exception.getMessage(); - String[] stackTrace = stackTraceEnabled ? captureStackTrace(exception) : null; + + // capture node location from exchange extension + String toNode = exchange.getExchangeExtension().getHistoryNodeId(); + String location = exchange.getExchangeExtension().getHistoryNodeSource(); + + // capture exchange data snapshot + JsonObject data = MessageHelper.dumpAsJSonObject( + exchange.getMessage(), + includeExchangeProperties, includeExchangeVariables, + true, true, + bodyIncludeStreams, bodyIncludeFiles, bodyMaxChars); + + // capture message history String[] messageHistory = captureMessageHistory(exchange); - DefaultErrorRegistryEntry entry = new DefaultErrorRegistryEntry( - exchangeId, routeId, endpointUri, Instant.now(), - handled, exceptionType, exceptionMessage, stackTrace, messageHistory); + String threadName = Thread.currentThread().getName(); + + DefaultBacklogErrorEventMessage entry = new DefaultBacklogErrorEventMessage( + uid, timestamp, location, routeId, fromRouteId, routeGroup, exchangeId, + endpointUri, toNode, threadName, data, exception, handled, 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 @@ -146,15 +168,13 @@ public class DefaultErrorRegistry extends EventNotifierSupport implements ErrorR } 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)) { + BacklogErrorEventMessage last = entries.peekLast(); + if (last != null && Instant.ofEpochMilli(last.getTimestamp()).isBefore(cutoff)) { entries.pollLast(); } else { break; @@ -171,19 +191,19 @@ public class DefaultErrorRegistry extends EventNotifierSupport implements ErrorR } @Override - public Collection<ErrorRegistryEntry> browse() { + public Collection<BacklogErrorEventMessage> browse() { return browse(-1); } @Override - public Collection<ErrorRegistryEntry> browse(int limit) { + public Collection<BacklogErrorEventMessage> browse(int limit) { evict(); if (limit <= 0) { return Collections.unmodifiableList(new ArrayList<>(entries)); } - List<ErrorRegistryEntry> result = new ArrayList<>(Math.min(limit, entries.size())); + List<BacklogErrorEventMessage> result = new ArrayList<>(Math.min(limit, entries.size())); int count = 0; - for (ErrorRegistryEntry entry : entries) { + for (BacklogErrorEventMessage entry : entries) { if (count >= limit) { break; } @@ -215,11 +235,9 @@ public class DefaultErrorRegistry extends EventNotifierSupport implements ErrorR @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); } } @@ -245,13 +263,53 @@ public class DefaultErrorRegistry extends EventNotifierSupport implements ErrorR } @Override - public boolean isStackTraceEnabled() { - return stackTraceEnabled; + public int getBodyMaxChars() { + return bodyMaxChars; + } + + @Override + public void setBodyMaxChars(int bodyMaxChars) { + this.bodyMaxChars = bodyMaxChars; + } + + @Override + public boolean isBodyIncludeStreams() { + return bodyIncludeStreams; } @Override - public void setStackTraceEnabled(boolean stackTraceEnabled) { - this.stackTraceEnabled = stackTraceEnabled; + public void setBodyIncludeStreams(boolean bodyIncludeStreams) { + this.bodyIncludeStreams = bodyIncludeStreams; + } + + @Override + public boolean isBodyIncludeFiles() { + return bodyIncludeFiles; + } + + @Override + public void setBodyIncludeFiles(boolean bodyIncludeFiles) { + this.bodyIncludeFiles = bodyIncludeFiles; + } + + @Override + public boolean isIncludeExchangeProperties() { + return includeExchangeProperties; + } + + @Override + public void setIncludeExchangeProperties(boolean includeExchangeProperties) { + this.includeExchangeProperties = includeExchangeProperties; + } + + @Override + public boolean isIncludeExchangeVariables() { + return includeExchangeVariables; + } + + @Override + public void setIncludeExchangeVariables(boolean includeExchangeVariables) { + this.includeExchangeVariables = includeExchangeVariables; } /** @@ -269,8 +327,8 @@ public class DefaultErrorRegistry extends EventNotifierSupport implements ErrorR public int size() { evict(); int count = 0; - for (ErrorRegistryEntry entry : entries) { - if (routeId.equals(entry.routeId())) { + for (BacklogErrorEventMessage entry : entries) { + if (routeId.equals(entry.getRouteId())) { count++; } } @@ -278,16 +336,16 @@ public class DefaultErrorRegistry extends EventNotifierSupport implements ErrorR } @Override - public Collection<ErrorRegistryEntry> browse() { + public Collection<BacklogErrorEventMessage> browse() { return browse(-1); } @Override - public Collection<ErrorRegistryEntry> browse(int limit) { + public Collection<BacklogErrorEventMessage> browse(int limit) { evict(); - List<ErrorRegistryEntry> result = new ArrayList<>(); - for (ErrorRegistryEntry entry : entries) { - if (routeId.equals(entry.routeId())) { + List<BacklogErrorEventMessage> result = new ArrayList<>(); + for (BacklogErrorEventMessage entry : entries) { + if (routeId.equals(entry.getRouteId())) { result.add(entry); if (limit > 0 && result.size() >= limit) { break; @@ -299,34 +357,209 @@ public class DefaultErrorRegistry extends EventNotifierSupport implements ErrorR @Override public void clear() { - entries.removeIf(entry -> routeId.equals(entry.routeId())); + entries.removeIf(entry -> routeId.equals(entry.getRouteId())); } } /** - * Immutable snapshot of an error. + * Default implementation of {@link BacklogErrorEventMessage}. */ - private record DefaultErrorRegistryEntry( - String exchangeId, - String routeId, - String endpointUri, - Instant timestamp, - boolean handled, - String exceptionType, - String exceptionMessage, - String[] stackTrace, - String[] messageHistory) - implements - ErrorRegistryEntry { + static final class DefaultBacklogErrorEventMessage implements BacklogErrorEventMessage { + + private final long uid; + private final long timestamp; + private final String location; + private final String routeId; + private final String fromRouteId; + private final String routeGroup; + private final String exchangeId; + private final String endpointUri; + private final String toNode; + private final String threadName; + private final JsonObject data; + private final Throwable exception; + private final boolean handled; + private final String[] messageHistory; + + private volatile String dataAsJson; + private volatile String exceptionAsJSon; + + DefaultBacklogErrorEventMessage( + long uid, long timestamp, String location, String routeId, String fromRouteId, + String routeGroup, + String exchangeId, String endpointUri, String toNode, String threadName, + JsonObject data, Throwable exception, boolean handled, String[] messageHistory) { + this.uid = uid; + this.timestamp = timestamp; + this.location = location; + this.routeId = routeId; + this.fromRouteId = fromRouteId; + this.routeGroup = routeGroup; + this.exchangeId = exchangeId; + this.endpointUri = endpointUri; + this.toNode = toNode; + this.threadName = threadName; + this.data = data; + this.exception = exception; + this.handled = handled; + this.messageHistory = messageHistory; + } + + @Override + public long getUid() { + return uid; + } + + @Override + public long getTimestamp() { + return timestamp; + } + + @Override + public String getLocation() { + return location; + } + + @Override + public String getRouteId() { + return routeId; + } + + @Override + public String getFromRouteId() { + return fromRouteId; + } + + @Override + public String getRouteGroup() { + return routeGroup; + } @Override - public String[] stackTrace() { - return stackTrace != null ? stackTrace.clone() : null; + public String getExchangeId() { + return exchangeId; } @Override - public String[] messageHistory() { + public String getEndpointUri() { + return endpointUri; + } + + @Override + public String getToNode() { + return toNode; + } + + @Override + public String getProcessingThreadName() { + return threadName; + } + + @Override + public String getMessageAsJSon() { + if (dataAsJson == null) { + dataAsJson = data.toJson(); + } + return dataAsJson; + } + + @Override + public boolean hasException() { + return exception != null; + } + + @Override + public String getExceptionAsJSon() { + if (exceptionAsJSon == null && exception != null) { + exceptionAsJSon = MessageHelper.dumpExceptionAsJSon(exception, 4, true); + } + return exceptionAsJSon; + } + + @Override + public Throwable getException() { + return exception; + } + + @Override + public boolean isHandled() { + return handled; + } + + @Override + public String getExceptionType() { + return exception.getClass().getName(); + } + + @Override + public String getExceptionMessage() { + return exception.getMessage(); + } + + @Override + public String[] getMessageHistory() { return messageHistory != null ? messageHistory.clone() : null; } + + @Override + public String toJSon(int indent) { + Jsonable jo = (Jsonable) asJSon(); + if (indent > 0) { + return Jsoner.prettyPrint(jo.toJson(), indent); + } else { + return Jsoner.prettyPrint(jo.toJson()); + } + } + + @Override + public Map<String, Object> asJSon() { + JsonObject jo = new JsonObject(); + jo.put("uid", uid); + jo.put("timestamp", timestamp); + if (location != null) { + jo.put("location", location); + } + if (routeId != null) { + jo.put("routeId", routeId); + } + if (fromRouteId != null) { + jo.put("fromRouteId", fromRouteId); + } + if (routeGroup != null) { + jo.put("routeGroup", routeGroup); + } + if (exchangeId != null) { + jo.put("exchangeId", exchangeId); + } + if (endpointUri != null) { + jo.put("endpointUri", endpointUri); + } + if (toNode != null) { + jo.put("nodeId", toNode); + } + jo.put("threadName", threadName); + jo.put("handled", handled); + // message data + jo.put("message", data.getMap("message")); + // exception + if (exception != null) { + try { + JsonObject exObj = MessageHelper.dumpExceptionAsJSonObject(exception); + jo.put("exception", exObj.get("exception")); + } catch (Exception e) { + // ignore + } + } + // message history + if (messageHistory != null) { + jo.put("messageHistory", List.of(messageHistory)); + } + return jo; + } + + @Override + public String toString() { + return "DefaultBacklogErrorEventMessage[" + exchangeId + " at " + routeId + "]"; + } } } 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 index b6676edc50a4..6b5477fb033f 100644 --- 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 @@ -19,8 +19,8 @@ package org.apache.camel.impl.console; import java.util.Collection; import java.util.Map; +import org.apache.camel.spi.BacklogErrorEventMessage; 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; @@ -60,27 +60,31 @@ public class ErrorRegistryConsole extends AbstractDevConsole { sb.append(String.format("%n Enabled: %s", registry.isEnabled())); sb.append(String.format("%n Size: %s", registry.size())); - Collection<ErrorRegistryEntry> entries; + Collection<BacklogErrorEventMessage> 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) { + for (BacklogErrorEventMessage entry : entries) { + sb.append(String.format("%n %s (route: %s, node: %s, endpoint: %s, handled: %s)", + entry.getExchangeId(), entry.getRouteId(), entry.getToNode(), entry.getEndpointUri(), + entry.isHandled())); + sb.append(String.format("%n Exception: %s - %s", + entry.getExceptionType(), entry.getExceptionMessage())); + sb.append(String.format("%n Timestamp: %s, Thread: %s", + entry.getTimestamp(), entry.getProcessingThreadName())); + if (entry.getMessageHistory() != null) { sb.append(String.format("%n Message History:")); - for (String step : entry.messageHistory()) { + for (String step : entry.getMessageHistory()) { sb.append(String.format("%n %s", step)); } } - if (includeStackTrace && entry.stackTrace() != null) { - for (String line : entry.stackTrace()) { - sb.append(String.format("%n %s", line)); + if (includeStackTrace) { + sb.append(String.format("%n Stack Trace:")); + for (StackTraceElement ste : entry.getException().getStackTrace()) { + sb.append(String.format("%n %s", ste)); } } } @@ -101,9 +105,8 @@ public class ErrorRegistryConsole extends AbstractDevConsole { root.put("size", registry.size()); root.put("maximumEntries", registry.getMaximumEntries()); root.put("timeToLive", registry.getTimeToLive().toString()); - root.put("stackTraceEnabled", registry.isStackTraceEnabled()); - Collection<ErrorRegistryEntry> entries; + Collection<BacklogErrorEventMessage> entries; if (routeId != null) { entries = registry.forRoute(routeId).browse(max); } else { @@ -111,28 +114,14 @@ public class ErrorRegistryConsole extends AbstractDevConsole { } final JsonArray list = new JsonArray(); - 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); + for (BacklogErrorEventMessage entry : entries) { + JsonObject jo = (JsonObject) entry.asJSon(); + if (!includeStackTrace) { + // remove stack trace from the exception sub-object to keep output concise + Object ex = jo.get("exception"); + if (ex instanceof JsonObject exObj) { + exObj.remove("stackTrace"); } - 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); } diff --git a/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryTest.java b/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryTest.java index 68d2d5f3fdf4..38de8560ac2a 100644 --- a/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryTest.java @@ -17,19 +17,19 @@ package org.apache.camel.impl; import java.util.Collection; +import java.util.Map; import org.apache.camel.CamelContext; import org.apache.camel.ContextTestSupport; import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.spi.BacklogErrorEventMessage; 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 { @@ -51,19 +51,20 @@ public class ErrorRegistryTest extends ContextTestSupport { assertMockEndpointsSatisfied(); ErrorRegistry registry = context.getErrorRegistry(); - Collection<ErrorRegistryEntry> entries = registry.browse(); + Collection<BacklogErrorEventMessage> 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); + BacklogErrorEventMessage entry = entries.iterator().next(); + assertNotNull(entry.getExchangeId()); + assertEquals("foo", entry.getRouteId()); + assertTrue(entry.getTimestamp() > 0); + assertTrue(entry.isHandled()); + assertEquals("java.lang.IllegalArgumentException", entry.getExceptionType()); + assertEquals("Forced error", entry.getExceptionMessage()); + assertNotNull(entry.getException()); + assertTrue(entry.getException() instanceof IllegalArgumentException); + assertTrue(entry.getUid() > 0); + assertNotNull(entry.getProcessingThreadName()); } @Test @@ -89,16 +90,14 @@ public class ErrorRegistryTest extends ContextTestSupport { 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()); + BacklogErrorEventMessage fooEntry = fooView.browse().iterator().next(); + assertEquals("foo", fooEntry.getRouteId()); ErrorRegistryView barView = registry.forRoute("bar"); assertEquals(1, barView.size()); - // clear only foo route fooView.clear(); assertEquals(1, registry.size()); assertEquals(0, fooView.size()); @@ -117,34 +116,9 @@ public class ErrorRegistryTest extends ContextTestSupport { 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); @@ -179,15 +153,15 @@ public class ErrorRegistryTest extends ContextTestSupport { } ErrorRegistry registry = context.getErrorRegistry(); - Collection<ErrorRegistryEntry> entries = registry.browse(); + Collection<BacklogErrorEventMessage> 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()); + BacklogErrorEventMessage entry = entries.iterator().next(); + assertNotNull(entry.getExchangeId()); + assertEquals("unhandled", entry.getRouteId()); + assertFalse(entry.isHandled()); + assertEquals("java.lang.IllegalArgumentException", entry.getExceptionType()); + assertEquals("Unhandled error", entry.getExceptionMessage()); } @Test @@ -196,10 +170,10 @@ public class ErrorRegistryTest extends ContextTestSupport { 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()); + BacklogErrorEventMessage entry = context.getErrorRegistry().browse().iterator().next(); + assertNotNull(entry.getEndpointUri()); + assertTrue(entry.getEndpointUri().contains("direct://fail"), + "Expected endpoint URI to contain direct://fail but was: " + entry.getEndpointUri()); } @Test @@ -208,11 +182,58 @@ public class ErrorRegistryTest extends ContextTestSupport { 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"); + BacklogErrorEventMessage entry = context.getErrorRegistry().browse().iterator().next(); + assertNotNull(entry.getMessageHistory(), "Message history should be captured when enabled"); + assertTrue(entry.getMessageHistory().length > 0, "Message history should have at least one entry"); + assertTrue(entry.getMessageHistory()[0].contains("foo"), "Message history should contain route id"); + } + + @Test + public void testErrorRegistryCapturesExchangeData() throws Exception { + getMockEndpoint("mock:dead").expectedMessageCount(1); + template.sendBodyAndHeader("direct:start", "Hello World", "MyHeader", "MyValue"); + assertMockEndpointsSatisfied(); + + BacklogErrorEventMessage entry = context.getErrorRegistry().browse().iterator().next(); + assertNotNull(entry.getMessageAsJSon()); + assertTrue(entry.getMessageAsJSon().contains("MyHeader"), + "Message JSON should contain header name"); + assertTrue(entry.getMessageAsJSon().contains("MyValue"), + "Message JSON should contain header value"); + assertTrue(entry.getMessageAsJSon().contains("Hello World"), + "Message JSON should contain body"); + } + + @Test + public void testErrorRegistryToJson() throws Exception { + getMockEndpoint("mock:dead").expectedMessageCount(1); + template.sendBody("direct:start", "Hello World"); + assertMockEndpointsSatisfied(); + + BacklogErrorEventMessage entry = context.getErrorRegistry().browse().iterator().next(); + String json = entry.toJSon(2); + assertNotNull(json); + assertTrue(json.contains("\"exchangeId\"")); + assertTrue(json.contains("\"routeId\"")); + assertTrue(json.contains("\"handled\"")); + assertTrue(json.contains("\"exception\"")); + assertTrue(json.contains("\"message\"")); + } + + @Test + public void testErrorRegistryAsJson() throws Exception { + getMockEndpoint("mock:dead").expectedMessageCount(1); + template.sendBody("direct:start", "Hello World"); + assertMockEndpointsSatisfied(); + + BacklogErrorEventMessage entry = context.getErrorRegistry().browse().iterator().next(); + Map<String, Object> json = entry.asJSon(); + assertNotNull(json); + assertEquals("foo", json.get("routeId")); + assertEquals(true, json.get("handled")); + assertNotNull(json.get("exchangeId")); + assertNotNull(json.get("exception")); + assertNotNull(json.get("message")); } @Override diff --git a/core/camel-main/src/generated/java/org/apache/camel/main/ErrorRegistryConfigurationPropertiesConfigurer.java b/core/camel-main/src/generated/java/org/apache/camel/main/ErrorRegistryConfigurationPropertiesConfigurer.java new file mode 100644 index 000000000000..a2fb37bf6e69 --- /dev/null +++ b/core/camel-main/src/generated/java/org/apache/camel/main/ErrorRegistryConfigurationPropertiesConfigurer.java @@ -0,0 +1,109 @@ +/* Generated by camel build tools - do NOT edit this file! */ +package org.apache.camel.main; + +import javax.annotation.processing.Generated; +import java.util.Map; + +import org.apache.camel.CamelContext; +import org.apache.camel.spi.ExtendedPropertyConfigurerGetter; +import org.apache.camel.spi.PropertyConfigurerGetter; +import org.apache.camel.spi.ConfigurerStrategy; +import org.apache.camel.spi.GeneratedPropertyConfigurer; +import org.apache.camel.util.CaseInsensitiveMap; +import org.apache.camel.main.ErrorRegistryConfigurationProperties; + +/** + * Generated by camel build tools - do NOT edit this file! + */ +@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo") +@SuppressWarnings("unchecked") +public class ErrorRegistryConfigurationPropertiesConfigurer extends org.apache.camel.support.component.PropertyConfigurerSupport implements GeneratedPropertyConfigurer, ExtendedPropertyConfigurerGetter { + + private static final Map<String, Object> ALL_OPTIONS; + static { + Map<String, Object> map = new CaseInsensitiveMap(); + map.put("BodyIncludeFiles", boolean.class); + map.put("BodyIncludeStreams", boolean.class); + map.put("BodyMaxChars", int.class); + map.put("Enabled", boolean.class); + map.put("IncludeExchangeProperties", boolean.class); + map.put("IncludeExchangeVariables", boolean.class); + map.put("MaximumEntries", int.class); + map.put("TimeToLiveSeconds", int.class); + ALL_OPTIONS = map; + } + + @Override + public boolean configure(CamelContext camelContext, Object obj, String name, Object value, boolean ignoreCase) { + org.apache.camel.main.ErrorRegistryConfigurationProperties target = (org.apache.camel.main.ErrorRegistryConfigurationProperties) obj; + switch (ignoreCase ? name.toLowerCase() : name) { + case "bodyincludefiles": + case "bodyIncludeFiles": target.setBodyIncludeFiles(property(camelContext, boolean.class, value)); return true; + case "bodyincludestreams": + case "bodyIncludeStreams": target.setBodyIncludeStreams(property(camelContext, boolean.class, value)); return true; + case "bodymaxchars": + case "bodyMaxChars": target.setBodyMaxChars(property(camelContext, int.class, value)); return true; + case "enabled": target.setEnabled(property(camelContext, boolean.class, value)); return true; + case "includeexchangeproperties": + case "includeExchangeProperties": target.setIncludeExchangeProperties(property(camelContext, boolean.class, value)); return true; + case "includeexchangevariables": + case "includeExchangeVariables": target.setIncludeExchangeVariables(property(camelContext, boolean.class, value)); return true; + case "maximumentries": + case "maximumEntries": target.setMaximumEntries(property(camelContext, int.class, value)); return true; + case "timetoliveseconds": + case "timeToLiveSeconds": target.setTimeToLiveSeconds(property(camelContext, int.class, value)); return true; + default: return false; + } + } + + @Override + public Map<String, Object> getAllOptions(Object target) { + return ALL_OPTIONS; + } + + @Override + public Class<?> getOptionType(String name, boolean ignoreCase) { + switch (ignoreCase ? name.toLowerCase() : name) { + case "bodyincludefiles": + case "bodyIncludeFiles": return boolean.class; + case "bodyincludestreams": + case "bodyIncludeStreams": return boolean.class; + case "bodymaxchars": + case "bodyMaxChars": return int.class; + case "enabled": return boolean.class; + case "includeexchangeproperties": + case "includeExchangeProperties": return boolean.class; + case "includeexchangevariables": + case "includeExchangeVariables": return boolean.class; + case "maximumentries": + case "maximumEntries": return int.class; + case "timetoliveseconds": + case "timeToLiveSeconds": return int.class; + default: return null; + } + } + + @Override + public Object getOptionValue(Object obj, String name, boolean ignoreCase) { + org.apache.camel.main.ErrorRegistryConfigurationProperties target = (org.apache.camel.main.ErrorRegistryConfigurationProperties) obj; + switch (ignoreCase ? name.toLowerCase() : name) { + case "bodyincludefiles": + case "bodyIncludeFiles": return target.isBodyIncludeFiles(); + case "bodyincludestreams": + case "bodyIncludeStreams": return target.isBodyIncludeStreams(); + case "bodymaxchars": + case "bodyMaxChars": return target.getBodyMaxChars(); + case "enabled": return target.isEnabled(); + case "includeexchangeproperties": + case "includeExchangeProperties": return target.isIncludeExchangeProperties(); + case "includeexchangevariables": + case "includeExchangeVariables": return target.isIncludeExchangeVariables(); + case "maximumentries": + case "maximumEntries": return target.getMaximumEntries(); + case "timetoliveseconds": + case "timeToLiveSeconds": return target.getTimeToLiveSeconds(); + default: return null; + } + } +} + 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 d21a1ab3029f..4f842638172a 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,10 +62,6 @@ 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); @@ -240,14 +236,6 @@ 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": @@ -512,14 +500,6 @@ 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": @@ -780,14 +760,6 @@ 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 bdf518fd508a..8699514fd905 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 @@ -27,7 +27,8 @@ { "name": "camel.metrics", "description": "Camel Micrometer Metrics configurations", "sourceType": "org.apache.camel.main.MetricsConfigurationProperties" }, { "name": "camel.faulttolerance", "description": "Fault Tolerance EIP Circuit Breaker configurations", "sourceType": "org.apache.camel.main.FaultToleranceConfigurationProperties" }, { "name": "camel.resilience4j", "description": "Resilience4j EIP Circuit Breaker configurations", "sourceType": "org.apache.camel.main.Resilience4jConfigurationProperties" }, - { "name": "camel.lra", "description": "Camel Saga EIP (Long Running Actions) configurations", "sourceType": "org.apache.camel.main.LraConfigurationProperties" } + { "name": "camel.lra", "description": "Camel Saga EIP (Long Running Actions) configurations", "sourceType": "org.apache.camel.main.LraConfigurationProperties" }, + { "name": "camel.errorRegistry", "description": "Camel Error Registry configurations", "sourceType": "org.apache.camel.main.ErrorRegistryConfigurationProperties" } ], "properties": [ { "name": "camel.main.additionalSensitiveKeywords", "required": false, "description": "Camel comes with a default set of sensitive keywords which are automatically masked. This option allows to add additional custom keywords to be masked as well. Multiple keywords can be separated by comma.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "string", "javaType": "java.lang.String", "secret": false }, @@ -70,10 +71,6 @@ { "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 }, @@ -178,6 +175,14 @@ { "name": "camel.debug.singleStepIncludeStartEnd", "required": false, "description": "In single step mode, then when the exchange is created and completed, then simulate a breakpoint at start and end, that allows to suspend and watch the incoming\/complete exchange at the route (you can see message body as response, failed exception etc).", "sourceType": "org.apache.camel.main.DebuggerConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": [...] { "name": "camel.debug.standby", "required": false, "description": "To set the debugger in standby mode, where the debugger will be installed by not automatic enabled. The debugger can then later be enabled explicit from Java, JMX or tooling.", "sourceType": "org.apache.camel.main.DebuggerConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, { "name": "camel.debug.waitForAttach", "required": false, "description": "Whether the debugger should suspend on startup, and wait for a remote debugger to attach. This is what the IDEA and VSCode tooling is using.", "sourceType": "org.apache.camel.main.DebuggerConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, + { "name": "camel.errorRegistry.bodyIncludeFiles", "required": false, "description": "Whether to include the message body of file based messages. The overhead is that the file content has to be read from the file.", "sourceType": "org.apache.camel.main.ErrorRegistryConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": true, "secret": false }, + { "name": "camel.errorRegistry.bodyIncludeStreams", "required": false, "description": "Whether to include the message body of stream based messages. If enabled then beware the stream may not be re-readable later. See more about Stream Caching.", "sourceType": "org.apache.camel.main.ErrorRegistryConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, + { "name": "camel.errorRegistry.bodyMaxChars", "required": false, "description": "To limit the message body to a maximum size in the captured error data. Use 0 or negative value to use unlimited size.", "sourceType": "org.apache.camel.main.ErrorRegistryConfigurationProperties", "type": "integer", "javaType": "int", "defaultValue": 32768, "secret": false }, + { "name": "camel.errorRegistry.enabled", "required": false, "description": "Whether the error registry is enabled to capture errors during message routing.", "sourceType": "org.apache.camel.main.ErrorRegistryConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, + { "name": "camel.errorRegistry.includeExchangeProperties", "required": false, "description": "Whether to include the exchange properties in the captured error data.", "sourceType": "org.apache.camel.main.ErrorRegistryConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": true, "secret": false }, + { "name": "camel.errorRegistry.includeExchangeVariables", "required": false, "description": "Whether to include the exchange variables in the captured error data.", "sourceType": "org.apache.camel.main.ErrorRegistryConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": true, "secret": false }, + { "name": "camel.errorRegistry.maximumEntries", "required": false, "description": "The maximum number of error entries to keep in the registry. When the limit is exceeded, the oldest entries are evicted.", "sourceType": "org.apache.camel.main.ErrorRegistryConfigurationProperties", "type": "integer", "javaType": "int", "defaultValue": 100, "secret": false }, + { "name": "camel.errorRegistry.timeToLiveSeconds", "required": false, "description": "The time-to-live in seconds for error entries. Entries older than this are evicted.", "sourceType": "org.apache.camel.main.ErrorRegistryConfigurationProperties", "type": "integer", "javaType": "int", "defaultValue": 3600, "secret": false }, { "name": "camel.faulttolerance.bulkheadEnabled", "required": false, "description": "Whether bulkhead is enabled or not on the circuit breaker. Default is false.", "sourceType": "org.apache.camel.main.FaultToleranceConfigurationProperties", "type": "boolean", "javaType": "java.lang.Boolean", "defaultValue": false, "secret": false }, { "name": "camel.faulttolerance.bulkheadMaxConcurrentCalls", "required": false, "description": "Configures the max amount of concurrent calls the bulkhead will support. Default value is 10.", "sourceType": "org.apache.camel.main.FaultToleranceConfigurationProperties", "type": "integer", "javaType": "java.lang.Integer", "defaultValue": 10, "secret": false }, { "name": "camel.faulttolerance.bulkheadWaitingTaskQueue", "required": false, "description": "Configures the task queue size for holding waiting tasks to be processed by the bulkhead. Default value is 10.", "sourceType": "org.apache.camel.main.FaultToleranceConfigurationProperties", "type": "integer", "javaType": "java.lang.Integer", "defaultValue": 10, "secret": false }, diff --git a/core/camel-main/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.main.ErrorRegistryConfigurationProperties b/core/camel-main/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.main.ErrorRegistryConfigurationProperties new file mode 100644 index 000000000000..b4b94899362c --- /dev/null +++ b/core/camel-main/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.main.ErrorRegistryConfigurationProperties @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.main.ErrorRegistryConfigurationPropertiesConfigurer diff --git a/core/camel-main/src/main/docs/main.adoc b/core/camel-main/src/main/docs/main.adoc index 3e5f2d95c857..30f68a3321a1 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 133 options, which are listed below. +The camel.main supports 129 options, which are listed below. [width="100%",cols="2,5,^1,2",options="header"] |=== @@ -64,10 +64,6 @@ The camel.main supports 133 options, which are listed below. | *camel.main.endpointBridgeErrorHandler* | 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 | boolean | *camel.main.endpointLazyStartProducer* | 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 then crea [...] | *camel.main.endpointRuntimeStatisticsEnabled* | 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.errorRegistryEnabled* | Sets whether the error registry is enabled to capture errors during message routing. This is by default disabled. | false | boolean -| *camel.main.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. | 100 | int -| *camel.main.errorRegistryStackTraceEnabled* | Sets whether to capture stack traces in the error registry. This is enabled by default. | true | boolean -| *camel.main.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). | 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.exchangeFactoryCapacity* | The capacity the pool (for each consumer) uses for storing exchanges. The default capacity is 100. | 100 | int | *camel.main.exchangeFactoryStatisticsEnabled* | Configures whether statistics is enabled on exchange factory. | false | boolean @@ -720,6 +716,23 @@ The camel.lra supports 5 options, which are listed below. | *camel.lra.localParticipantContextPath* | The context-path for the local participant. Is default /lra-participant | /lra-participant | String | *camel.lra.localParticipantUrl* | The URL for the local participant | | String |=== + + +=== Camel Error Registry configurations +The camel.errorRegistry supports 8 options, which are listed below. + +[width="100%",cols="2,5,^1,2",options="header"] +|=== +| Name | Description | Default | Type +| *camel.errorRegistry.bodyIncludeFiles* | Whether to include the message body of file based messages. The overhead is that the file content has to be read from the file. | true | boolean +| *camel.errorRegistry.bodyIncludeStreams* | Whether to include the message body of stream based messages. If enabled then beware the stream may not be re-readable later. See more about Stream Caching. | false | boolean +| *camel.errorRegistry.bodyMaxChars* | To limit the message body to a maximum size in the captured error data. Use 0 or negative value to use unlimited size. | 32768 | int +| *camel.errorRegistry.enabled* | Whether the error registry is enabled to capture errors during message routing. | false | boolean +| *camel.errorRegistry.includeExchangeProperties* | Whether to include the exchange properties in the captured error data. | true | boolean +| *camel.errorRegistry.includeExchangeVariables* | Whether to include the exchange variables in the captured error data. | true | boolean +| *camel.errorRegistry.maximumEntries* | The maximum number of error entries to keep in the registry. When the limit is exceeded, the oldest entries are evicted. | 100 | int +| *camel.errorRegistry.timeToLiveSeconds* | The time-to-live in seconds for error entries. Entries older than this are evicted. | 3600 | int +|=== // main options: END == Package Scanning diff --git a/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java b/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java index 79520eb3f9c5..ee3e97966cfb 100644 --- a/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java +++ b/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java @@ -27,6 +27,7 @@ import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.security.KeyStore; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; @@ -74,6 +75,7 @@ import org.apache.camel.spi.CompileStrategy; import org.apache.camel.spi.DataFormat; import org.apache.camel.spi.Debugger; import org.apache.camel.spi.DebuggerFactory; +import org.apache.camel.spi.ErrorRegistry; import org.apache.camel.spi.Language; import org.apache.camel.spi.LifecycleStrategy; import org.apache.camel.spi.PackageScanClassResolver; @@ -146,6 +148,7 @@ public abstract class BaseMainSupport extends BaseService { private static final String PREFIX_DEBUG = "camel.debug."; private static final String PREFIX_TRACE = "camel.trace."; private static final String PREFIX_ROUTE_CONTROLLER = "camel.routeController."; + private static final String PREFIX_ERROR_REGISTRY = "camel.errorRegistry."; private static final String[] GROUP_PREFIXES = new String[] { "camel.context.", "camel.resilience4j.", "camel.faulttolerance.", @@ -154,7 +157,7 @@ public abstract class BaseMainSupport extends BaseService { "camel.telemetryDev.", "camel.management.", "camel.mdc.", "camel.metrics.", "camel.routeTemplate", "camel.devConsole.", "camel.variable.", "camel.beans.", "camel.globalOptions.", PREFIX_SERVER, PREFIX_SSL, PREFIX_SECURITY, PREFIX_DEBUG, PREFIX_TRACE, - PREFIX_ROUTE_CONTROLLER }; + PREFIX_ROUTE_CONTROLLER, PREFIX_ERROR_REGISTRY }; protected final List<MainListener> listeners = new ArrayList<>(); protected volatile CamelContext camelContext; @@ -1409,6 +1412,7 @@ public abstract class BaseMainSupport extends BaseService { OrderedLocationProperties debuggerProperties = new OrderedLocationProperties(); OrderedLocationProperties tracerProperties = new OrderedLocationProperties(); OrderedLocationProperties routeControllerProperties = new OrderedLocationProperties(); + OrderedLocationProperties errorRegistryProperties = new OrderedLocationProperties(); for (String key : prop.stringPropertyNames()) { String loc = prop.getLocation(key); @@ -1556,6 +1560,12 @@ public abstract class BaseMainSupport extends BaseService { String option = key.substring(22); validateOptionAndValue(key, option, value); routeControllerProperties.put(loc, optionKey(option), value); + } else if (startsWithIgnoreCase(key, PREFIX_ERROR_REGISTRY)) { + // grab the value + String value = prop.getProperty(key); + String option = key.substring(20); + validateOptionAndValue(key, option, value); + errorRegistryProperties.put(loc, optionKey(option), value); } } @@ -1698,6 +1708,12 @@ public abstract class BaseMainSupport extends BaseService { mainConfigurationProperties.isAutoConfigurationFailFast(), autoConfiguredProperties); } + if (!errorRegistryProperties.isEmpty() || mainConfigurationProperties.hasErrorRegistryConfiguration()) { + LOG.debug("Auto-configuring Error Registry from loaded properties: {}", errorRegistryProperties.size()); + setErrorRegistryProperties(camelContext, errorRegistryProperties, + mainConfigurationProperties.isAutoConfigurationFailFast(), + autoConfiguredProperties); + } // configure which requires access to the model MainSupportModelConfigurer.configureModelCamelContext(camelContext, mainConfigurationProperties, @@ -1762,6 +1778,11 @@ public abstract class BaseMainSupport extends BaseService { LOG.warn("Property not auto-configured: camel.routeController.{}={}", k, v); }); } + if (!errorRegistryProperties.isEmpty()) { + errorRegistryProperties.forEach((k, v) -> { + LOG.warn("Property not auto-configured: camel.errorRegistry.{}={}", k, v); + }); + } if (!devConsoleProperties.isEmpty()) { devConsoleProperties.forEach((k, v) -> { LOG.warn("Property not auto-configured: camel.devConsole.{}={}", k, v); @@ -2482,6 +2503,26 @@ public abstract class BaseMainSupport extends BaseService { } } + private void setErrorRegistryProperties( + CamelContext camelContext, OrderedLocationProperties properties, + boolean failIfNotSet, OrderedLocationProperties autoConfiguredProperties) + throws Exception { + + ErrorRegistryConfigurationProperties config = mainConfigurationProperties.errorRegistryConfig(); + setPropertiesOnTarget(camelContext, config, properties, PREFIX_ERROR_REGISTRY, + failIfNotSet, true, autoConfiguredProperties); + + ErrorRegistry registry = camelContext.getErrorRegistry(); + registry.setEnabled(config.isEnabled()); + registry.setMaximumEntries(config.getMaximumEntries()); + registry.setTimeToLive(Duration.ofSeconds(config.getTimeToLiveSeconds())); + registry.setBodyMaxChars(config.getBodyMaxChars()); + registry.setBodyIncludeStreams(config.isBodyIncludeStreams()); + registry.setBodyIncludeFiles(config.isBodyIncludeFiles()); + registry.setIncludeExchangeProperties(config.isIncludeExchangeProperties()); + registry.setIncludeExchangeVariables(config.isIncludeExchangeVariables()); + } + private void bindBeansToRegistry( CamelContext camelContext, OrderedLocationProperties properties, String optionPrefix, boolean failIfNotSet, boolean logSummary, boolean ignoreCase, 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 6ecd82204c1b..ad5dbd32cdd8 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,7 +16,6 @@ */ package org.apache.camel.main; -import java.time.Duration; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; @@ -203,10 +202,7 @@ 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()); + // error registry is configured via ErrorRegistryConfigurationProperties (camel.errorRegistry.*) if (config.getLogDebugMaxChars() != 0) { camelContext.getGlobalOptions().put(Exchange.LOG_DEBUG_BODY_MAX_CHARS, 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 38c3c3a62e0c..8a2fa86a0809 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,14 +47,6 @@ 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") @@ -335,57 +327,6 @@ 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; } @@ -1929,44 +1870,6 @@ 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-main/src/main/java/org/apache/camel/main/ErrorRegistryConfigurationProperties.java b/core/camel-main/src/main/java/org/apache/camel/main/ErrorRegistryConfigurationProperties.java new file mode 100644 index 000000000000..aa7dd909d7d3 --- /dev/null +++ b/core/camel-main/src/main/java/org/apache/camel/main/ErrorRegistryConfigurationProperties.java @@ -0,0 +1,222 @@ +/* + * 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.main; + +import org.apache.camel.spi.BootstrapCloseable; +import org.apache.camel.spi.Configurer; +import org.apache.camel.spi.Metadata; + +/** + * Error Registry configuration. + */ +@Configurer(extended = true) +public class ErrorRegistryConfigurationProperties implements BootstrapCloseable { + + private MainConfigurationProperties parent; + + @Metadata + private boolean enabled; + @Metadata(defaultValue = "100") + private int maximumEntries = 100; + @Metadata(defaultValue = "3600") + private int timeToLiveSeconds = 3600; + @Metadata(label = "advanced", defaultValue = "32768") + private int bodyMaxChars = 32 * 1024; + @Metadata + private boolean bodyIncludeStreams; + @Metadata(defaultValue = "true") + private boolean bodyIncludeFiles = true; + @Metadata(defaultValue = "true") + private boolean includeExchangeProperties = true; + @Metadata(defaultValue = "true") + private boolean includeExchangeVariables = true; + + public ErrorRegistryConfigurationProperties(MainConfigurationProperties parent) { + this.parent = parent; + } + + public MainConfigurationProperties end() { + return parent; + } + + @Override + public void close() { + parent = null; + } + + public boolean isEnabled() { + return enabled; + } + + /** + * Whether the error registry is enabled to capture errors during message routing. + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public int getMaximumEntries() { + return maximumEntries; + } + + /** + * The maximum number of error entries to keep in the registry. When the limit is exceeded, the oldest entries are + * evicted. + */ + public void setMaximumEntries(int maximumEntries) { + this.maximumEntries = maximumEntries; + } + + public int getTimeToLiveSeconds() { + return timeToLiveSeconds; + } + + /** + * The time-to-live in seconds for error entries. Entries older than this are evicted. + */ + public void setTimeToLiveSeconds(int timeToLiveSeconds) { + this.timeToLiveSeconds = timeToLiveSeconds; + } + + public int getBodyMaxChars() { + return bodyMaxChars; + } + + /** + * To limit the message body to a maximum size in the captured error data. Use 0 or negative value to use unlimited + * size. + */ + public void setBodyMaxChars(int bodyMaxChars) { + this.bodyMaxChars = bodyMaxChars; + } + + public boolean isBodyIncludeStreams() { + return bodyIncludeStreams; + } + + /** + * Whether to include the message body of stream based messages. If enabled then beware the stream may not be + * re-readable later. See more about Stream Caching. + */ + public void setBodyIncludeStreams(boolean bodyIncludeStreams) { + this.bodyIncludeStreams = bodyIncludeStreams; + } + + public boolean isBodyIncludeFiles() { + return bodyIncludeFiles; + } + + /** + * Whether to include the message body of file based messages. The overhead is that the file content has to be read + * from the file. + */ + public void setBodyIncludeFiles(boolean bodyIncludeFiles) { + this.bodyIncludeFiles = bodyIncludeFiles; + } + + public boolean isIncludeExchangeProperties() { + return includeExchangeProperties; + } + + /** + * Whether to include the exchange properties in the captured error data. + */ + public void setIncludeExchangeProperties(boolean includeExchangeProperties) { + this.includeExchangeProperties = includeExchangeProperties; + } + + public boolean isIncludeExchangeVariables() { + return includeExchangeVariables; + } + + /** + * Whether to include the exchange variables in the captured error data. + */ + public void setIncludeExchangeVariables(boolean includeExchangeVariables) { + this.includeExchangeVariables = includeExchangeVariables; + } + + // -- fluent builder methods -- + + /** + * Whether the error registry is enabled to capture errors during message routing. + */ + public ErrorRegistryConfigurationProperties withEnabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + /** + * The maximum number of error entries to keep in the registry. When the limit is exceeded, the oldest entries are + * evicted. + */ + public ErrorRegistryConfigurationProperties withMaximumEntries(int maximumEntries) { + this.maximumEntries = maximumEntries; + return this; + } + + /** + * The time-to-live in seconds for error entries. Entries older than this are evicted. + */ + public ErrorRegistryConfigurationProperties withTimeToLiveSeconds(int timeToLiveSeconds) { + this.timeToLiveSeconds = timeToLiveSeconds; + return this; + } + + /** + * To limit the message body to a maximum size in the captured error data. Use 0 or negative value to use unlimited + * size. + */ + public ErrorRegistryConfigurationProperties withBodyMaxChars(int bodyMaxChars) { + this.bodyMaxChars = bodyMaxChars; + return this; + } + + /** + * Whether to include the message body of stream based messages. If enabled then beware the stream may not be + * re-readable later. See more about Stream Caching. + */ + public ErrorRegistryConfigurationProperties withBodyIncludeStreams(boolean bodyIncludeStreams) { + this.bodyIncludeStreams = bodyIncludeStreams; + return this; + } + + /** + * Whether to include the message body of file based messages. The overhead is that the file content has to be read + * from the file. + */ + public ErrorRegistryConfigurationProperties withBodyIncludeFiles(boolean bodyIncludeFiles) { + this.bodyIncludeFiles = bodyIncludeFiles; + return this; + } + + /** + * Whether to include the exchange properties in the captured error data. + */ + public ErrorRegistryConfigurationProperties withIncludeExchangeProperties(boolean includeExchangeProperties) { + this.includeExchangeProperties = includeExchangeProperties; + return this; + } + + /** + * Whether to include the exchange variables in the captured error data. + */ + public ErrorRegistryConfigurationProperties withIncludeExchangeVariables(boolean includeExchangeVariables) { + this.includeExchangeVariables = includeExchangeVariables; + return this; + } +} diff --git a/core/camel-main/src/main/java/org/apache/camel/main/MainConfigurationProperties.java b/core/camel-main/src/main/java/org/apache/camel/main/MainConfigurationProperties.java index 9ff565e55a25..5a6da245a292 100644 --- a/core/camel-main/src/main/java/org/apache/camel/main/MainConfigurationProperties.java +++ b/core/camel-main/src/main/java/org/apache/camel/main/MainConfigurationProperties.java @@ -77,6 +77,7 @@ public class MainConfigurationProperties extends DefaultConfigurationProperties< private DebuggerConfigurationProperties debuggerConfigurationProperties; private TracerConfigurationProperties tracerConfigurationProperties; private RouteControllerConfigurationProperties routeControllerConfigurationProperties; + private ErrorRegistryConfigurationProperties errorRegistryConfigurationProperties; @Override public void close() { @@ -156,6 +157,10 @@ public class MainConfigurationProperties extends DefaultConfigurationProperties< routeControllerConfigurationProperties.close(); routeControllerConfigurationProperties = null; } + if (errorRegistryConfigurationProperties != null) { + errorRegistryConfigurationProperties.close(); + errorRegistryConfigurationProperties = null; + } if (routesBuilders != null) { routesBuilders.clear(); routesBuilders = null; @@ -377,6 +382,24 @@ public class MainConfigurationProperties extends DefaultConfigurationProperties< return tracerConfigurationProperties != null; } + /** + * To configure Error Registry. + */ + public ErrorRegistryConfigurationProperties errorRegistryConfig() { + if (errorRegistryConfigurationProperties == null) { + errorRegistryConfigurationProperties = new ErrorRegistryConfigurationProperties(this); + } + + return errorRegistryConfigurationProperties; + } + + /** + * Whether there has been any Error Registry configuration specified. + */ + public boolean hasErrorRegistryConfiguration() { + return errorRegistryConfigurationProperties != null; + } + /** * To configure Route Controller. */ 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 74095faf878a..966217eea084 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 @@ -386,13 +386,14 @@ public final class CamelOpenMBeanTypes { return new CompositeType( "errors", "Errors", new String[] { - "exchangeId", "routeId", "endpointUri", "timestamp", + "exchangeId", "routeId", "routeGroup", "nodeId", "endpointUri", "timestamp", "handled", "exceptionType", "exceptionMessage" }, new String[] { - "Exchange Id", "Route Id", "Endpoint Uri", "Timestamp", + "Exchange Id", "Route Id", "Route Group", "Node Id", "Endpoint Uri", "Timestamp", "Handled", "Exception Type", "Exception Message" }, new OpenType[] { - SimpleType.STRING, SimpleType.STRING, SimpleType.STRING, SimpleType.STRING, + SimpleType.STRING, SimpleType.STRING, 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 index a88f1ad6848f..b9a57809b161 100644 --- 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 @@ -44,11 +44,35 @@ public interface ManagedErrorRegistryMBean extends ManagedServiceMBean { @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 = "Maximum number of characters for the message body") + int getBodyMaxChars(); - @ManagedAttribute(description = "Whether stack trace capture is enabled") - void setStackTraceEnabled(boolean stackTraceEnabled); + @ManagedAttribute(description = "Maximum number of characters for the message body") + void setBodyMaxChars(int bodyMaxChars); + + @ManagedAttribute(description = "Whether to include stream-based message bodies") + boolean isBodyIncludeStreams(); + + @ManagedAttribute(description = "Whether to include stream-based message bodies") + void setBodyIncludeStreams(boolean bodyIncludeStreams); + + @ManagedAttribute(description = "Whether to include file-based message bodies") + boolean isBodyIncludeFiles(); + + @ManagedAttribute(description = "Whether to include file-based message bodies") + void setBodyIncludeFiles(boolean bodyIncludeFiles); + + @ManagedAttribute(description = "Whether to include exchange properties") + boolean isIncludeExchangeProperties(); + + @ManagedAttribute(description = "Whether to include exchange properties") + void setIncludeExchangeProperties(boolean includeExchangeProperties); + + @ManagedAttribute(description = "Whether to include exchange variables") + boolean isIncludeExchangeVariables(); + + @ManagedAttribute(description = "Whether to include exchange variables") + void setIncludeExchangeVariables(boolean includeExchangeVariables); @ManagedOperation(description = "Browse all error entries") TabularData browse(); 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 index eab3bcccf54f..5f5886b8ba65 100644 --- 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 @@ -30,8 +30,8 @@ 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.BacklogErrorEventMessage; import org.apache.camel.spi.ErrorRegistry; -import org.apache.camel.spi.ErrorRegistryEntry; @ManagedResource(description = "Managed ErrorRegistry") public class ManagedErrorRegistry extends ManagedService implements ManagedErrorRegistryMBean { @@ -83,13 +83,53 @@ public class ManagedErrorRegistry extends ManagedService implements ManagedError } @Override - public boolean isStackTraceEnabled() { - return errorRegistry.isStackTraceEnabled(); + public int getBodyMaxChars() { + return errorRegistry.getBodyMaxChars(); } @Override - public void setStackTraceEnabled(boolean stackTraceEnabled) { - errorRegistry.setStackTraceEnabled(stackTraceEnabled); + public void setBodyMaxChars(int bodyMaxChars) { + errorRegistry.setBodyMaxChars(bodyMaxChars); + } + + @Override + public boolean isBodyIncludeStreams() { + return errorRegistry.isBodyIncludeStreams(); + } + + @Override + public void setBodyIncludeStreams(boolean bodyIncludeStreams) { + errorRegistry.setBodyIncludeStreams(bodyIncludeStreams); + } + + @Override + public boolean isBodyIncludeFiles() { + return errorRegistry.isBodyIncludeFiles(); + } + + @Override + public void setBodyIncludeFiles(boolean bodyIncludeFiles) { + errorRegistry.setBodyIncludeFiles(bodyIncludeFiles); + } + + @Override + public boolean isIncludeExchangeProperties() { + return errorRegistry.isIncludeExchangeProperties(); + } + + @Override + public void setIncludeExchangeProperties(boolean includeExchangeProperties) { + errorRegistry.setIncludeExchangeProperties(includeExchangeProperties); + } + + @Override + public boolean isIncludeExchangeVariables() { + return errorRegistry.isIncludeExchangeVariables(); + } + + @Override + public void setIncludeExchangeVariables(boolean includeExchangeVariables) { + errorRegistry.setIncludeExchangeVariables(includeExchangeVariables); } @Override @@ -112,24 +152,26 @@ public class ManagedErrorRegistry extends ManagedService implements ManagedError errorRegistry.clear(); } - private static TabularData browseEntries(Collection<ErrorRegistryEntry> entries) { + private static TabularData browseEntries(Collection<BacklogErrorEventMessage> entries) { try { TabularData answer = new TabularDataSupport(CamelOpenMBeanTypes.listErrorRegistryTabularType()); - for (ErrorRegistryEntry entry : entries) { + for (BacklogErrorEventMessage entry : entries) { CompositeType ct = CamelOpenMBeanTypes.listErrorRegistryCompositeType(); CompositeData data = new CompositeDataSupport( ct, new String[] { - "exchangeId", "routeId", "endpointUri", "timestamp", + "exchangeId", "routeId", "routeGroup", "nodeId", "endpointUri", "timestamp", "handled", "exceptionType", "exceptionMessage" }, new Object[] { - entry.exchangeId(), - entry.routeId(), - entry.endpointUri(), - entry.timestamp().toString(), - entry.handled(), - entry.exceptionType(), - entry.exceptionMessage() }); + entry.getExchangeId(), + entry.getRouteId(), + entry.getRouteGroup(), + entry.getToNode(), + entry.getEndpointUri(), + String.valueOf(entry.getTimestamp()), + entry.isHandled(), + entry.getExceptionType(), + entry.getExceptionMessage() }); answer.put(data); } return answer; diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc index 8c0a5e1bfc26..caa54c89abb5 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc @@ -28,6 +28,35 @@ your own code or tooling, add `org.jspecify:jspecify` explicitly to your project The `org.apache.camel.support.DefaultHeaderFilterStrategy` changed default setting for lowercase from `false` to `true`. +==== Error Registry SPI changes + +The `ErrorRegistry` SPI has been enhanced to capture rich exchange data snapshots at error time, +following the same pattern as BacklogTracer. The following breaking changes apply: + +- The `org.apache.camel.spi.ErrorRegistryEntry` interface has been removed and replaced by `BacklogErrorEventMessage`, + which extends a new `BacklogEventMessage` base interface shared with `BacklogTracerEventMessage`. +- The `ErrorRegistryView.browse()` methods now return `Collection<BacklogErrorEventMessage>` instead of + `Collection<ErrorRegistryEntry>`. +- The `stackTraceEnabled` option on `ErrorRegistry` has been removed. The full `Throwable` is now always captured. +- Error registry configuration properties have moved from `camel.main.errorRegistryXxx` to the grouped + `camel.errorRegistry.*` prefix. The old flat properties (`camel.main.errorRegistryEnabled`, + `camel.main.errorRegistryMaximumEntries`, `camel.main.errorRegistryTimeToLiveSeconds`, + `camel.main.errorRegistryStackTraceEnabled`) are removed. + +New configuration properties under `camel.errorRegistry.*`: + +[options="header"] +|=== +| Old property | New property +| `camel.main.errorRegistryEnabled` | `camel.errorRegistry.enabled` +| `camel.main.errorRegistryMaximumEntries` | `camel.errorRegistry.maximumEntries` +| `camel.main.errorRegistryTimeToLiveSeconds` | `camel.errorRegistry.timeToLiveSeconds` +| `camel.main.errorRegistryStackTraceEnabled` | _(removed, always captures full exception)_ +|=== + +New capture options: `bodyMaxChars`, `bodyIncludeStreams`, `bodyIncludeFiles`, +`includeExchangeProperties`, `includeExchangeVariables` — matching the BacklogTracer configuration. + ==== Virtual threads: `maxQueueSize` now honored in `threads()` EIP When virtual threads are enabled (`camel.threads.virtual.enabled=true`), the `threads()` EIP now honors `maxQueueSize` diff --git a/docs/user-manual/modules/ROOT/pages/error-registry.adoc b/docs/user-manual/modules/ROOT/pages/error-registry.adoc index 6ead556efaea..06392008178b 100644 --- a/docs/user-manual/modules/ROOT/pages/error-registry.adoc +++ b/docs/user-manual/modules/ROOT/pages/error-registry.adoc @@ -1,11 +1,11 @@ = Error Registry The `ErrorRegistry` SPI captures snapshots of exceptions that occur during message routing, -without retaining references to the original exchange or exception objects. +including the exception, exchange data (headers, body, properties, variables), and message history. -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. +When enabled, the registry stores rich error snapshots that can be browsed globally or scoped to a specific route. +The exchange data is captured as a detached snapshot at the time of the error, so the original exchange +can be safely garbage collected after routing completes. == Enabling @@ -20,7 +20,7 @@ Or via configuration properties: [source,properties] ---- -camel.main.errorRegistryEnabled = true +camel.errorRegistry.enabled = true ---- == Configuration @@ -28,12 +28,34 @@ camel.main.errorRegistryEnabled = true [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. +| `camel.errorRegistry.enabled` | `false` | boolean | Whether the error registry is enabled. +| `camel.errorRegistry.maximumEntries` | `100` | int | Maximum number of error entries to keep. When exceeded, the oldest entries are evicted. +| `camel.errorRegistry.timeToLiveSeconds` | `3600` | int | Time-to-live for error entries in seconds. Entries older than this are evicted. +| `camel.errorRegistry.bodyMaxChars` | `32768` | int | Maximum number of characters for the message body in the snapshot. +| `camel.errorRegistry.bodyIncludeStreams` | `false` | boolean | Whether to include stream-based message bodies. +| `camel.errorRegistry.bodyIncludeFiles` | `true` | boolean | Whether to include file-based message bodies. +| `camel.errorRegistry.includeExchangeProperties` | `true` | boolean | Whether to include exchange properties in the snapshot. +| `camel.errorRegistry.includeExchangeVariables` | `true` | boolean | Whether to include exchange variables in the snapshot. |=== +== Captured Data + +Each error entry captures: + +- *Exchange ID* — the unique exchange identifier +- *Route ID* — the route where the error occurred (which may differ from the from-route if the exchange was routed across multiple routes) +- *From Route ID* — the originating route where the exchange was created (the consumer route) +- *Route Group* — the group of the route where the error occurred (if set) +- *Node ID* — the EIP processor node where processing failed +- *Location* — the source file location of the failing node (e.g. "MyRoute.java:42"), requires source location to be enabled +- *Endpoint URI* — the producer endpoint the exchange was being sent to when the failure occurred. This can be null when the error occurs before any send attempt (e.g. a direct `throwException` in the route). This does _not_ refer to the route's consumer (from) endpoint. +- *Timestamp* — when the error occurred (milliseconds since epoch) +- *Exception* — the full exception instance including type, message, and stack trace +- *Handled* — whether the error was handled by an error handler (e.g. dead letter channel, onException) or is unhandled +- *Message data* — a detached JSON snapshot of the exchange message (body, headers, and optionally properties and variables) +- *Message history* — the trace of every node the message was routed through up until the failure point (requires message history to be enabled) +- *Processing thread* — the name of the thread that was processing the exchange at the time of the error + == Message History When xref:components:eips:message-history.adoc[Message History] is enabled on the `CamelContext`, the error registry @@ -56,5 +78,5 @@ A JMX MBean (`ManagedErrorRegistry`) is registered when JMX is enabled, providin * Enable/disable the error registry * Browse error entries (globally or by route) -* Configure maximum entries and time-to-live +* Configure maximum entries, time-to-live, and capture options * Clear all entries diff --git a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCamelMainMojo.java b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCamelMainMojo.java index efb6430e7fbd..c2ada8247cb2 100644 --- a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCamelMainMojo.java +++ b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCamelMainMojo.java @@ -240,6 +240,8 @@ public class PrepareCamelMainMojo extends AbstractGeneratorMojo { prefix = "camel.security."; } else if (file.getName().contains("SSLConfigurationProperties")) { prefix = "camel.ssl."; + } else if (file.getName().contains("ErrorRegistryConfigurationProperties")) { + prefix = "camel.errorRegistry."; } else if (file.getName().contains("DebuggerConfigurationProperties")) { prefix = "camel.debug."; } else if (file.getName().contains("TracerConfigurationProperties")) { @@ -472,6 +474,9 @@ public class PrepareCamelMainMojo extends AbstractGeneratorMojo { model.getGroups().add(new MainGroupModel( "camel.lra", "Camel Saga EIP (Long Running Actions) configurations", "org.apache.camel.main.LraConfigurationProperties")); + model.getGroups().add(new MainGroupModel( + "camel.errorRegistry", "Camel Error Registry configurations", + "org.apache.camel.main.ErrorRegistryConfigurationProperties")); String json = JsonMapper.createJsonSchema(model);
