This is an automated email from the ASF dual-hosted git repository. pcongiusti pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
commit b5f0dda145585d617da1b6ac90a922b1573f5087 Author: Pasquale Congiusti <[email protected]> AuthorDate: Thu Sep 25 14:44:57 2025 +0200 feat(component): MDC docs --- .../main/camel-main-configuration-metadata.json | 3 + .../org/apache/camel/catalog/others/mdc.json | 2 +- components/camel-mdc/pom.xml | 2 +- .../services/org/apache/camel/other.properties | 2 +- .../camel-mdc/src/generated/resources/mdc.json | 2 +- components/camel-mdc/src/main/docs/mdc.adoc | 59 ++++++++++++- .../camel/mdc/MDCProcessorsInterceptStrategy.java | 63 ++++++++++---- .../main/java/org/apache/camel/mdc/MDCService.java | 3 + .../java/org/apache/camel/mdc/MDCAsyncTest.java | 93 ++++++++------------ ...{MDCAsyncTest.java => MDCAsyncWiretapTest.java} | 22 +++-- .../org/apache/camel/mdc/MyAsyncComponent.java | 46 ++++++++++ .../java/org/apache/camel/mdc/MyAsyncEndpoint.java | 98 ++++++++++++++++++++++ .../java/org/apache/camel/mdc/MyAsyncProducer.java | 75 +++++++++++++++++ .../camel-mdc/src/test/resources/log4j2.properties | 4 +- .../main/MdcConfigurationPropertiesConfigurer.java | 32 ++++++- .../camel-main-configuration-metadata.json | 3 + core/camel-main/src/main/docs/main.adoc | 5 +- .../org/apache/camel/main/BaseMainSupport.java | 2 +- .../camel/main/MdcConfigurationProperties.java | 51 ++++++++++- proposals/mdc.adoc | 8 +- .../maven/packaging/PrepareCamelMainMojo.java | 2 + 21 files changed, 472 insertions(+), 105 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 6dbbe63d36a..fae5155bcac 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 @@ -217,6 +217,9 @@ { "name": "camel.management.uploadEnabled", "required": false, "description": "Whether to enable file upload via HTTP (not intended for production use). This functionality is for development to be able to reload Camel routes and code with source changes (if reload is enabled). If enabled then you can upload\/delete files via HTTP PUT\/DELETE on context-path: \/q\/upload\/{name}. You must also configure the uploadSourceDir option.", "sourceType": "org.apache.camel.main.HttpManagementS [...] { "name": "camel.management.uploadSourceDir", "required": false, "description": "Source directory when upload is enabled.", "sourceType": "org.apache.camel.main.HttpManagementServerConfigurationProperties", "type": "string", "javaType": "java.lang.String", "secret": false }, { "name": "camel.management.useGlobalSslContextParameters", "required": false, "description": "Whether to use global SSL configuration for securing the embedded HTTP server.", "sourceType": "org.apache.camel.main.HttpManagementServerConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, + { "name": "camel.mdc.customExchangeHeaders", "required": false, "description": "Provide the headers you would like to use in the logging. Use * value to include all available headers", "sourceType": "org.apache.camel.main.MdcConfigurationProperties", "type": "string", "javaType": "java.lang.String", "secret": false }, + { "name": "camel.mdc.customExchangeProperties", "required": false, "description": "Provide the properties you would like to use in the logging. Use * value to include all available properties", "sourceType": "org.apache.camel.main.MdcConfigurationProperties", "type": "string", "javaType": "java.lang.String", "secret": false }, + { "name": "camel.mdc.enabled", "required": false, "description": "To enable MDC service", "sourceType": "org.apache.camel.main.MdcConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, { "name": "camel.metrics.baseEndpointURIExchangeEventNotifier", "required": false, "description": "Whether to use static or dynamic values for Endpoint Name tags in captured metrics. By default, static values are used. When using dynamic tags, then a dynamic to (toD) can compute many different endpoint URIs that, can lead to many tags as the URI is dynamic, so use this with care if setting this option to false.", "sourceType": "org.apache.camel.main.MetricsConfigurationProperties", " [...] { "name": "camel.metrics.binders", "required": false, "description": "Additional Micrometer binders to include such as jvm-memory, processor, jvm-thread, and so forth. Multiple binders can be separated by comma. The following binders currently is available from Micrometer: class-loader, commons-object-pool2, file-descriptor, hystrix-metrics-binder, jvm-compilation, jvm-gc, jvm-heap-pressure, jvm-info, jvm-memory, jvm-thread, log4j2, logback, processor, uptime", "sourceType": "org.apa [...] { "name": "camel.metrics.clearOnReload", "required": false, "description": "Clear the captured metrics data when Camel is reloading routes such as when using Camel JBang.", "sourceType": "org.apache.camel.main.MetricsConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": true, "secret": false }, diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others/mdc.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others/mdc.json index 073d9a5633d..47ba69df888 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others/mdc.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others/mdc.json @@ -3,7 +3,7 @@ "kind": "other", "name": "mdc", "title": "Mdc", - "description": "Mapped Diagnostic Context component", + "description": "Logging MDC (Mapped Diagnostic Context) Service", "deprecated": false, "firstVersion": "4.15.0", "label": "logging", diff --git a/components/camel-mdc/pom.xml b/components/camel-mdc/pom.xml index 978c9d6892c..602ee335ab8 100644 --- a/components/camel-mdc/pom.xml +++ b/components/camel-mdc/pom.xml @@ -36,7 +36,7 @@ <artifactId>camel-mdc</artifactId> <packaging>jar</packaging> <name>Camel :: MDC</name> - <description>Mapped Diagnostic Context component</description> + <description>Logging MDC (Mapped Diagnostic Context) Service</description> <dependencies> <dependency> diff --git a/components/camel-mdc/src/generated/resources/META-INF/services/org/apache/camel/other.properties b/components/camel-mdc/src/generated/resources/META-INF/services/org/apache/camel/other.properties index 2f66413827f..7862c26646d 100644 --- a/components/camel-mdc/src/generated/resources/META-INF/services/org/apache/camel/other.properties +++ b/components/camel-mdc/src/generated/resources/META-INF/services/org/apache/camel/other.properties @@ -4,4 +4,4 @@ groupId=org.apache.camel artifactId=camel-mdc version=4.15.0-SNAPSHOT projectName=Camel :: MDC -projectDescription=Mapped Diagnostic Context component +projectDescription=Logging MDC (Mapped Diagnostic Context) Service diff --git a/components/camel-mdc/src/generated/resources/mdc.json b/components/camel-mdc/src/generated/resources/mdc.json index 073d9a5633d..47ba69df888 100644 --- a/components/camel-mdc/src/generated/resources/mdc.json +++ b/components/camel-mdc/src/generated/resources/mdc.json @@ -3,7 +3,7 @@ "kind": "other", "name": "mdc", "title": "Mdc", - "description": "Mapped Diagnostic Context component", + "description": "Logging MDC (Mapped Diagnostic Context) Service", "deprecated": false, "firstVersion": "4.15.0", "label": "logging", diff --git a/components/camel-mdc/src/main/docs/mdc.adoc b/components/camel-mdc/src/main/docs/mdc.adoc index 03ccd90ea33..b61f2af086c 100644 --- a/components/camel-mdc/src/main/docs/mdc.adoc +++ b/components/camel-mdc/src/main/docs/mdc.adoc @@ -2,9 +2,66 @@ :doctitle: Mdc :shortname: mdc :artifactid: camel-mdc -:description: Mapped Diagnostic Context component +:description: Logging MDC (Mapped Diagnostic Context) Service :since: 4.15 :supportlevel: Preview :tabs-sync-option: *Since Camel {since}* + +This component is in charge to provide the configuration required to work with Mapped Diagnostic Context (MDC), a specification adopted by the major logging providers to add diagnostic information in the logging traces. Camel uses the API exposed by https://www.slf4j.org/manual.html#mdc[SLF4J logging facade]. The concrete implementation will depend on the logging system adopted. + +When you enable the MDC Service provided in this component, you will instruct your application to automatically store certain Camel Headers into the MDC storage. You can additionally provide more headers and properties through the component configuration. The MDC is normally sharing the storage per each thread. Given the multi threading nature of Camel, the component deals with the multithreading setting the information into the context and cleaning them once they have been used (general [...] + +== Usage + +If you want to use the feature, you need to include the `camel-mdc` dependency in your `pom.xml` and configure it with the parameters in the `application.properties` configuration file (at least set `camel.mdc.enabled=true`). + +The goal of this component is to avoid to work on low level API in Java. In older MDC implementations you had to hack into the code to include MDC such as: + +```java + org.slf4j.MDC.put("myCustomMDCKey", "myCustomKeyValue"); +``` + +And later you had to make sure to provide MDC context propagation in async components (eg, `wiretap`) in order to make sure to have such context available in the new executing async thread. With this new service, the only thing to do is to add the value as a Camel Exchange header (or a property), for example: + +```java + .setHeader("myCustomMDCKey", simple("myCustomKeyValue")) +``` +include the MDC service and additionally instruct the Camel application to treat that header as a MDC trace (via `camel.mdc.customHeaders=myCustomMDCKey` or `*` to include all headers). You won't need any longer to worry about context propagation as the propagation will be done via Camel Exchange instead. + +NOTE: you won't also need any longer to hack the code using Java DSL, as you can put headers in any Camel DSL. + +Depending on the logging technology used, you can now include the MDC parameters you want to trace in your logging configuration. For example, in `log4j2` configuration you can include them as shown below: + +``` +... [%X{camel.contextId}, %X{camel.routeId}, %X{camel.exchangeId}, %X{camel.messageId}, %X{customHead}, %X{customProp}] +``` + +During the execution you can verify the output of the log to see the traces appended to your logger. + +== Default headers + +This is the list of default MDC values included in each execution: + +* camel.breadcrumbId +* camel.exchangeId +* camel.messageId +* camel.correlationId +* camel.routeId +* camel.contextId +* camel.threadId + +If they exists in the Exchange, then, they will be included in the MDC. You can use `camel.mdc.customHeaders` and `camel.mdc.customProperties` to include any further header and property you need to trace. + +== Configuration + +The configuration properties for the MDC component are: + +[width="100%",cols="10%,10%,80%",options="header",] +|======================================================================= +|Option |Default |Description +|`camel.mdc.enabled`| false | Enable the MDC logging. +|`camel.mdc.customHeaders` | | Provide the headers you would like to use in the logging. Use `*` value to include all available headers. +|`camel.mdc.customProperties` | | Provide the properties you would like to use in the logging. Use `*` value to include all available properties. +|======================================================================= diff --git a/components/camel-mdc/src/main/java/org/apache/camel/mdc/MDCProcessorsInterceptStrategy.java b/components/camel-mdc/src/main/java/org/apache/camel/mdc/MDCProcessorsInterceptStrategy.java index 3306a40e90d..02c755f8401 100644 --- a/components/camel-mdc/src/main/java/org/apache/camel/mdc/MDCProcessorsInterceptStrategy.java +++ b/components/camel-mdc/src/main/java/org/apache/camel/mdc/MDCProcessorsInterceptStrategy.java @@ -16,12 +16,16 @@ */ package org.apache.camel.mdc; +import java.util.concurrent.CompletableFuture; + +import org.apache.camel.AsyncCallback; +import org.apache.camel.AsyncProcessor; import org.apache.camel.CamelContext; import org.apache.camel.Exchange; import org.apache.camel.NamedNode; import org.apache.camel.Processor; import org.apache.camel.spi.InterceptStrategy; -import org.apache.camel.support.processor.DelegateAsyncProcessor; +import org.apache.camel.support.AsyncProcessorConverterHelper; /** * MDCProcessorsInterceptStrategy is used to wrap each processor calls and generate the MDC context for each process @@ -37,28 +41,51 @@ public class MDCProcessorsInterceptStrategy implements InterceptStrategy { @Override public Processor wrapProcessorInInterceptors( - CamelContext camelContext, - NamedNode processorDefinition, Processor target, Processor nextTarget) + final CamelContext context, + final NamedNode definition, + final Processor target, + final Processor nextTarget) throws Exception { - return new DelegateAsyncProcessor(new TraceProcessor(target)); - } - private class TraceProcessor implements Processor { - private final Processor target; + final AsyncProcessor asyncProcessor = AsyncProcessorConverterHelper.convert(target); + + return new AsyncProcessor() { + + @Override + public boolean process(Exchange exchange, AsyncCallback callback) { + mdcService.setMDC(exchange); + return asyncProcessor.process(exchange, doneSync -> { + mdcService.unsetMDC(); + callback.done(doneSync); + }); + } + + @Override + public void process(Exchange exchange) throws Exception { + mdcService.setMDC(exchange); + try { + asyncProcessor.process(exchange); + } finally { + mdcService.unsetMDC(); + } + } - public TraceProcessor(Processor target) { - this.target = target; - } + @Override + public CompletableFuture<Exchange> processAsync(Exchange exchange) { + CompletableFuture<Exchange> future = new CompletableFuture<>(); + mdcService.setMDC(exchange); + asyncProcessor.process(exchange, doneSync -> { + if (exchange.getException() != null) { + future.completeExceptionally(exchange.getException()); + } else { + future.complete(exchange); + } + mdcService.unsetMDC(); + }); - @Override - public void process(Exchange exchange) throws Exception { - mdcService.setMDC(exchange); - try { - target.process(exchange); - } finally { - mdcService.unsetMDC(); + return future; } - } + }; } } diff --git a/components/camel-mdc/src/main/java/org/apache/camel/mdc/MDCService.java b/components/camel-mdc/src/main/java/org/apache/camel/mdc/MDCService.java index 5ac3a429881..47191e79586 100644 --- a/components/camel-mdc/src/main/java/org/apache/camel/mdc/MDCService.java +++ b/components/camel-mdc/src/main/java/org/apache/camel/mdc/MDCService.java @@ -39,6 +39,7 @@ public class MDCService extends ServiceSupport implements CamelMDCService { static String MDC_MESSAGE_ID = "camel.messageId"; static String MDC_CORRELATION_ID = "camel.correlationId"; static String MDC_ROUTE_ID = "camel.routeId"; + static String MDC_CAMEL_THREAD_ID = "camel.threadId"; static String MDC_CAMEL_CONTEXT_ID = "camel.contextId"; private static final Logger LOG = LoggerFactory.getLogger(MDCService.class); @@ -149,6 +150,8 @@ public class MDCService extends ServiceSupport implements CamelMDCService { MDC.put(MDC_EXCHANGE_ID, exchange.getExchangeId()); MDC.put(MDC_MESSAGE_ID, exchange.getMessage().getMessageId()); MDC.put(MDC_CAMEL_CONTEXT_ID, exchange.getContext().getName()); + // Useful to make sure aync execution is properly propagating context + MDC.put(MDC_CAMEL_THREAD_ID, Thread.currentThread().getName()); // Backward compatibility: this info may not be longer widely used String corrId = exchange.getProperty(ExchangePropertyKey.CORRELATION_ID, String.class); if (corrId != null) { diff --git a/components/camel-mdc/src/test/java/org/apache/camel/mdc/MDCAsyncTest.java b/components/camel-mdc/src/test/java/org/apache/camel/mdc/MDCAsyncTest.java index 7a5c2284a1d..5b94104bf3f 100644 --- a/components/camel-mdc/src/test/java/org/apache/camel/mdc/MDCAsyncTest.java +++ b/components/camel-mdc/src/test/java/org/apache/camel/mdc/MDCAsyncTest.java @@ -16,28 +16,21 @@ */ package org.apache.camel.mdc; -import java.io.IOException; - import org.apache.camel.CamelContext; import org.apache.camel.CamelContextAware; -import org.apache.camel.RoutesBuilder; +import org.apache.camel.Exchange; +import org.apache.camel.Processor; import org.apache.camel.builder.RouteBuilder; -import org.apache.camel.component.mock.MockEndpoint; import org.apache.camel.test.junit5.ExchangeTestSupport; import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.slf4j.MDC; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; public class MDCAsyncTest extends ExchangeTestSupport { - private static final Logger LOG = LoggerFactory.getLogger(MDCAsyncTest.class); - @Override protected CamelContext createCamelContext() throws Exception { MDCService mdcSvc = new MDCService(); @@ -50,69 +43,49 @@ public class MDCAsyncTest extends ExchangeTestSupport { } @Test - void testRouteSingleRequest() throws IOException, InterruptedException { - MockEndpoint mock = getMockEndpoint("mock:end"); - mock.expectedMessageCount(1); - mock.setAssertPeriod(5000); - context.createProducerTemplate().sendBody("direct:start", null); - mock.assertIsSatisfied(1000); - // We should get no MDC after the route has been executed - assertEquals(0, MDC.getCopyOfContextMap().size()); + public void testAsyncEndpoint() throws Exception { + getMockEndpoint("mock:before").expectedBodiesReceived("Hello Camel"); + getMockEndpoint("mock:after").expectedBodiesReceived("Bye Camel"); + getMockEndpoint("mock:result").expectedBodiesReceived("Bye Camel"); + + String reply = template.requestBody("direct:start", "Hello Camel", String.class); + assertEquals("Bye Camel", reply); } @Override - protected RoutesBuilder createRouteBuilder() { + protected RouteBuilder createRouteBuilder() { return new RouteBuilder() { @Override public void configure() { - from("direct:start") - .setBody() - .simple("start") - .log("start: ${exchangeId}") - .to("direct:a") - .wireTap("direct:b"); + context.addComponent("async", new MyAsyncComponent()); - from("direct:a") + from("direct:start").to("mock:before").to("log:before") .setProperty("prop1", simple("Property1")) .setHeader("head", simple("Header1")) - .process(exchange -> { - LOG.info("Direct:a process"); - assertNotNull(MDC.get(MDCService.MDC_MESSAGE_ID)); - assertNotNull(MDC.get(MDCService.MDC_EXCHANGE_ID)); - assertNotNull(MDC.get(MDCService.MDC_ROUTE_ID)); - assertNotNull(MDC.get(MDCService.MDC_CAMEL_CONTEXT_ID)); - assertEquals("Header1", MDC.get("head")); - assertEquals("Property1", MDC.get("prop1")); - assertNull(MDC.get("prop2")); - // We store the exchange of this execution in a property - // as we will use this property to evaluate the exchange in the direct:b execution - exchange.setProperty("directa-exchange", exchange.getExchangeId()); - }) - .setBody() - .simple("Direct a") - .log("directa: ${exchangeId}"); + .process(new Processor() { + public void process(Exchange exchange) { + assertEquals("Header1", MDC.get("head")); + assertEquals("Property1", MDC.get("prop1")); + assertNull(MDC.get("prop2")); + // We store the threadId of this execution in a property + // as we will use it to assert the thread is different in the direct:b execution + exchange.setProperty("thread-a", MDC.get(MDCService.MDC_CAMEL_THREAD_ID)); + } + }).recipientList(constant("direct:foo")); - from("direct:b") + from("direct:foo").to("async:bye:camel") .setProperty("prop2", simple("Property2")) .setHeader("head", simple("Header2")) - .process(exchange -> { - LOG.info("Direct:b process"); - assertNotNull(MDC.get(MDCService.MDC_MESSAGE_ID)); - assertNotNull(MDC.get(MDCService.MDC_EXCHANGE_ID)); - assertNotNull(MDC.get(MDCService.MDC_ROUTE_ID)); - assertNotNull(MDC.get(MDCService.MDC_CAMEL_CONTEXT_ID)); - assertEquals("Header2", MDC.get("head")); - // NOTE: properties are shared - assertEquals("Property1", MDC.get("prop1")); - assertEquals("Property2", MDC.get("prop2")); - // We use as support storage the same properties - assertNotEquals(exchange.getProperty("directa-exchange"), MDC.get(MDCService.MDC_EXCHANGE_ID)); - }) - .delay(2000) - .setBody() - .simple("Direct b") - .log("directb: ${exchangeId}") - .to("mock:end"); + .process(new Processor() { + public void process(Exchange exchange) { + // Make sure this execution is spanned in a different thread + // but still the context (in this case the properties) is propagated + assertNotEquals(exchange.getProperty("thread-a"), MDC.get(MDCService.MDC_CAMEL_THREAD_ID)); + assertEquals("Header2", MDC.get("head")); + assertEquals("Property1", MDC.get("prop1")); + assertEquals("Property2", MDC.get("prop2")); + } + }).to("log:after").to("mock:after").to("mock:result"); } }; } diff --git a/components/camel-mdc/src/test/java/org/apache/camel/mdc/MDCAsyncTest.java b/components/camel-mdc/src/test/java/org/apache/camel/mdc/MDCAsyncWiretapTest.java similarity index 85% copy from components/camel-mdc/src/test/java/org/apache/camel/mdc/MDCAsyncTest.java copy to components/camel-mdc/src/test/java/org/apache/camel/mdc/MDCAsyncWiretapTest.java index 7a5c2284a1d..f549a830dd9 100644 --- a/components/camel-mdc/src/test/java/org/apache/camel/mdc/MDCAsyncTest.java +++ b/components/camel-mdc/src/test/java/org/apache/camel/mdc/MDCAsyncWiretapTest.java @@ -34,9 +34,9 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -public class MDCAsyncTest extends ExchangeTestSupport { +public class MDCAsyncWiretapTest extends ExchangeTestSupport { - private static final Logger LOG = LoggerFactory.getLogger(MDCAsyncTest.class); + private static final Logger LOG = LoggerFactory.getLogger(MDCAsyncWiretapTest.class); @Override protected CamelContext createCamelContext() throws Exception { @@ -70,6 +70,7 @@ public class MDCAsyncTest extends ExchangeTestSupport { .simple("start") .log("start: ${exchangeId}") .to("direct:a") + // MUST be any async component .wireTap("direct:b"); from("direct:a") @@ -81,12 +82,13 @@ public class MDCAsyncTest extends ExchangeTestSupport { assertNotNull(MDC.get(MDCService.MDC_EXCHANGE_ID)); assertNotNull(MDC.get(MDCService.MDC_ROUTE_ID)); assertNotNull(MDC.get(MDCService.MDC_CAMEL_CONTEXT_ID)); + assertNotNull(MDC.get(MDCService.MDC_CAMEL_THREAD_ID)); assertEquals("Header1", MDC.get("head")); assertEquals("Property1", MDC.get("prop1")); assertNull(MDC.get("prop2")); - // We store the exchange of this execution in a property - // as we will use this property to evaluate the exchange in the direct:b execution - exchange.setProperty("directa-exchange", exchange.getExchangeId()); + // We store the threadId of this execution in a property + // as we will use it to assert the thread is different in the direct:b execution + exchange.setProperty("thread-a", MDC.get(MDCService.MDC_CAMEL_THREAD_ID)); }) .setBody() .simple("Direct a") @@ -97,16 +99,12 @@ public class MDCAsyncTest extends ExchangeTestSupport { .setHeader("head", simple("Header2")) .process(exchange -> { LOG.info("Direct:b process"); - assertNotNull(MDC.get(MDCService.MDC_MESSAGE_ID)); - assertNotNull(MDC.get(MDCService.MDC_EXCHANGE_ID)); - assertNotNull(MDC.get(MDCService.MDC_ROUTE_ID)); - assertNotNull(MDC.get(MDCService.MDC_CAMEL_CONTEXT_ID)); + // Make sure this execution is spanned in a different thread + // but still the context (in this case the properties) is propagated + assertNotEquals(exchange.getProperty("thread-a"), MDC.get(MDCService.MDC_CAMEL_THREAD_ID)); assertEquals("Header2", MDC.get("head")); - // NOTE: properties are shared assertEquals("Property1", MDC.get("prop1")); assertEquals("Property2", MDC.get("prop2")); - // We use as support storage the same properties - assertNotEquals(exchange.getProperty("directa-exchange"), MDC.get(MDCService.MDC_EXCHANGE_ID)); }) .delay(2000) .setBody() diff --git a/components/camel-mdc/src/test/java/org/apache/camel/mdc/MyAsyncComponent.java b/components/camel-mdc/src/test/java/org/apache/camel/mdc/MyAsyncComponent.java new file mode 100644 index 00000000000..141a3041a0c --- /dev/null +++ b/components/camel-mdc/src/test/java/org/apache/camel/mdc/MyAsyncComponent.java @@ -0,0 +1,46 @@ +/* + * 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.mdc; + +import java.util.Map; + +import org.apache.camel.Endpoint; +import org.apache.camel.support.DefaultComponent; +import org.apache.camel.util.StringHelper; + +public class MyAsyncComponent extends DefaultComponent { + + @Override + protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception { + MyAsyncEndpoint answer = new MyAsyncEndpoint(uri, this); + answer.setReply(prepareReply(remaining)); + setProperties(answer, parameters); + return answer; + } + + private String prepareReply(String value) { + // to make URIs valid we make the conventions of using ':' for ' ' and + // capitalize words + String[] words = value.split(":"); + StringBuilder result = new StringBuilder(); + for (String word : words) { + result.append(result.isEmpty() ? "" : " "); + result.append(StringHelper.capitalize(word)); + } + return result.toString(); + } +} diff --git a/components/camel-mdc/src/test/java/org/apache/camel/mdc/MyAsyncEndpoint.java b/components/camel-mdc/src/test/java/org/apache/camel/mdc/MyAsyncEndpoint.java new file mode 100644 index 00000000000..71f0474e112 --- /dev/null +++ b/components/camel-mdc/src/test/java/org/apache/camel/mdc/MyAsyncEndpoint.java @@ -0,0 +1,98 @@ +/* + * 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.mdc; + +import org.apache.camel.Component; +import org.apache.camel.Consumer; +import org.apache.camel.Processor; +import org.apache.camel.Producer; +import org.apache.camel.support.DefaultEndpoint; +import org.apache.camel.support.SynchronousDelegateProducer; + +public class MyAsyncEndpoint extends DefaultEndpoint { + + private boolean append; + private String reply; + private long delay = 25; + private int failFirstAttempts; + private boolean synchronous; + + public MyAsyncEndpoint(String endpointUri, Component component) { + super(endpointUri, component); + } + + @Override + public Producer createProducer() { + Producer answer = new MyAsyncProducer(this); + if (isSynchronous()) { + // force it to be synchronously + return new SynchronousDelegateProducer(answer); + } else { + return answer; + } + } + + @Override + public Consumer createConsumer(Processor processor) { + throw new UnsupportedOperationException("Consumer not supported"); + } + + @Override + public boolean isSingleton() { + return true; + } + + public boolean isSynchronous() { + return synchronous; + } + + public void setSynchronous(boolean synchronous) { + this.synchronous = synchronous; + } + + public String getReply() { + return reply; + } + + public void setReply(String reply) { + this.reply = reply; + } + + public long getDelay() { + return delay; + } + + public void setDelay(long delay) { + this.delay = delay; + } + + public int getFailFirstAttempts() { + return failFirstAttempts; + } + + public void setFailFirstAttempts(int failFirstAttempts) { + this.failFirstAttempts = failFirstAttempts; + } + + public boolean isAppend() { + return append; + } + + public void setAppend(boolean append) { + this.append = append; + } +} diff --git a/components/camel-mdc/src/test/java/org/apache/camel/mdc/MyAsyncProducer.java b/components/camel-mdc/src/test/java/org/apache/camel/mdc/MyAsyncProducer.java new file mode 100644 index 00000000000..4e99a798a4c --- /dev/null +++ b/components/camel-mdc/src/test/java/org/apache/camel/mdc/MyAsyncProducer.java @@ -0,0 +1,75 @@ +/* + * 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.mdc; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.camel.AsyncCallback; +import org.apache.camel.CamelExchangeException; +import org.apache.camel.Exchange; +import org.apache.camel.support.DefaultAsyncProducer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MyAsyncProducer extends DefaultAsyncProducer { + + private static final Logger LOG = LoggerFactory.getLogger(MyAsyncProducer.class); + private final ExecutorService executor; + private final AtomicInteger counter = new AtomicInteger(); + + public MyAsyncProducer(MyAsyncEndpoint endpoint) { + super(endpoint); + this.executor = endpoint.getCamelContext().getExecutorServiceManager().newDefaultThreadPool(this, "MyProducer"); + } + + @Override + public MyAsyncEndpoint getEndpoint() { + return (MyAsyncEndpoint) super.getEndpoint(); + } + + @Override + public boolean process(final Exchange exchange, final AsyncCallback callback) { + executor.submit(new Callable<Object>() { + public Object call() throws Exception { + LOG.info("Simulating a task which takes {} millis to reply", getEndpoint().getDelay()); + Thread.sleep(getEndpoint().getDelay()); + + int count = counter.incrementAndGet(); + if (getEndpoint().getFailFirstAttempts() >= count) { + LOG.info("Simulating a failure at attempt {}", count); + exchange.setException(new CamelExchangeException("Simulated error at attempt " + count, exchange)); + } else { + String reply = getEndpoint().getReply(); + reply = getEndpoint().isAppend() ? exchange.getIn().getBody() + " " + reply : reply; + exchange.getMessage().setBody(reply); + LOG.info("Setting reply {}", reply); + } + + LOG.info("Callback done(false)"); + callback.done(false); + return null; + } + }); + + // indicate from this point forward its being routed asynchronously + LOG.info("Task submitted, now tell Camel routing engine to that this Exchange is being continued asynchronously"); + return false; + } + +} diff --git a/components/camel-mdc/src/test/resources/log4j2.properties b/components/camel-mdc/src/test/resources/log4j2.properties index 590933e3e26..731b4840a10 100644 --- a/components/camel-mdc/src/test/resources/log4j2.properties +++ b/components/camel-mdc/src/test/resources/log4j2.properties @@ -17,14 +17,14 @@ appender.console.type = Console appender.console.name = console appender.console.layout.type = PatternLayout -appender.console.layout.pattern = %d [%-15.15t] %-5p %-30.30c{1} - %m [%X{camel.contextId}, %X{camel.routeId}, %X{camel.exchangeId}, %X{camel.messageId}, %X{head1}, %X{prop1}, %X{head2}, %X{prop2}]%n +appender.console.layout.pattern = %d [%-15.15t] %-5p %-30.30c{1} - %m [%X{camel.threadId}, %X{camel.contextId}, %X{camel.routeId}, %X{camel.exchangeId}, %X{camel.messageId}, %X{head1}, %X{prop1}, %X{head2}, %X{prop2}]%n appender.file.type = File appender.file.name = file appender.file.fileName = target/camel-mdc-test.log appender.file.append = true appender.file.layout.type = PatternLayout -appender.file.layout.pattern = %d [%-15.15t] %-5p %-30.30c{1} - %m [%X{camel.contextId}, %X{camel.routeId}, %X{camel.exchangeId}, %X{camel.messageId}, %X{head1}, %X{prop1}, %X{head2}, %X{prop2}]%n +appender.file.layout.pattern = %d [%-15.15t] %-5p %-30.30c{1} - %m [%X{camel.threadId}, %X{camel.contextId}, %X{camel.routeId}, %X{camel.exchangeId}, %X{camel.messageId}, %X{head1}, %X{prop1}, %X{head2}, %X{prop2}]%n rootLogger.level = INFO rootLogger.appenderRef.file.ref = file diff --git a/core/camel-main/src/generated/java/org/apache/camel/main/MdcConfigurationPropertiesConfigurer.java b/core/camel-main/src/generated/java/org/apache/camel/main/MdcConfigurationPropertiesConfigurer.java index 9707c284526..abc3e624f75 100644 --- a/core/camel-main/src/generated/java/org/apache/camel/main/MdcConfigurationPropertiesConfigurer.java +++ b/core/camel-main/src/generated/java/org/apache/camel/main/MdcConfigurationPropertiesConfigurer.java @@ -22,12 +22,23 @@ public class MdcConfigurationPropertiesConfigurer extends org.apache.camel.suppo private static final Map<String, Object> ALL_OPTIONS; static { Map<String, Object> map = new CaseInsensitiveMap(); + map.put("CustomExchangeHeaders", java.lang.String.class); + map.put("CustomExchangeProperties", java.lang.String.class); + map.put("Enabled", boolean.class); ALL_OPTIONS = map; } @Override public boolean configure(CamelContext camelContext, Object obj, String name, Object value, boolean ignoreCase) { - return false; + org.apache.camel.main.MdcConfigurationProperties target = (org.apache.camel.main.MdcConfigurationProperties) obj; + switch (ignoreCase ? name.toLowerCase() : name) { + case "customexchangeheaders": + case "customExchangeHeaders": target.setCustomExchangeHeaders(property(camelContext, java.lang.String.class, value)); return true; + case "customexchangeproperties": + case "customExchangeProperties": target.setCustomExchangeProperties(property(camelContext, java.lang.String.class, value)); return true; + case "enabled": target.setEnabled(property(camelContext, boolean.class, value)); return true; + default: return false; + } } @Override @@ -37,12 +48,27 @@ public class MdcConfigurationPropertiesConfigurer extends org.apache.camel.suppo @Override public Class<?> getOptionType(String name, boolean ignoreCase) { - return null; + switch (ignoreCase ? name.toLowerCase() : name) { + case "customexchangeheaders": + case "customExchangeHeaders": return java.lang.String.class; + case "customexchangeproperties": + case "customExchangeProperties": return java.lang.String.class; + case "enabled": return boolean.class; + default: return null; + } } @Override public Object getOptionValue(Object obj, String name, boolean ignoreCase) { - return null; + org.apache.camel.main.MdcConfigurationProperties target = (org.apache.camel.main.MdcConfigurationProperties) obj; + switch (ignoreCase ? name.toLowerCase() : name) { + case "customexchangeheaders": + case "customExchangeHeaders": return target.getCustomExchangeHeaders(); + case "customexchangeproperties": + case "customExchangeProperties": return target.getCustomExchangeProperties(); + case "enabled": return target.isEnabled(); + default: return null; + } } } 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 6dbbe63d36a..fae5155bcac 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 @@ -217,6 +217,9 @@ { "name": "camel.management.uploadEnabled", "required": false, "description": "Whether to enable file upload via HTTP (not intended for production use). This functionality is for development to be able to reload Camel routes and code with source changes (if reload is enabled). If enabled then you can upload\/delete files via HTTP PUT\/DELETE on context-path: \/q\/upload\/{name}. You must also configure the uploadSourceDir option.", "sourceType": "org.apache.camel.main.HttpManagementS [...] { "name": "camel.management.uploadSourceDir", "required": false, "description": "Source directory when upload is enabled.", "sourceType": "org.apache.camel.main.HttpManagementServerConfigurationProperties", "type": "string", "javaType": "java.lang.String", "secret": false }, { "name": "camel.management.useGlobalSslContextParameters", "required": false, "description": "Whether to use global SSL configuration for securing the embedded HTTP server.", "sourceType": "org.apache.camel.main.HttpManagementServerConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, + { "name": "camel.mdc.customExchangeHeaders", "required": false, "description": "Provide the headers you would like to use in the logging. Use * value to include all available headers", "sourceType": "org.apache.camel.main.MdcConfigurationProperties", "type": "string", "javaType": "java.lang.String", "secret": false }, + { "name": "camel.mdc.customExchangeProperties", "required": false, "description": "Provide the properties you would like to use in the logging. Use * value to include all available properties", "sourceType": "org.apache.camel.main.MdcConfigurationProperties", "type": "string", "javaType": "java.lang.String", "secret": false }, + { "name": "camel.mdc.enabled", "required": false, "description": "To enable MDC service", "sourceType": "org.apache.camel.main.MdcConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, { "name": "camel.metrics.baseEndpointURIExchangeEventNotifier", "required": false, "description": "Whether to use static or dynamic values for Endpoint Name tags in captured metrics. By default, static values are used. When using dynamic tags, then a dynamic to (toD) can compute many different endpoint URIs that, can lead to many tags as the URI is dynamic, so use this with care if setting this option to false.", "sourceType": "org.apache.camel.main.MetricsConfigurationProperties", " [...] { "name": "camel.metrics.binders", "required": false, "description": "Additional Micrometer binders to include such as jvm-memory, processor, jvm-thread, and so forth. Multiple binders can be separated by comma. The following binders currently is available from Micrometer: class-loader, commons-object-pool2, file-descriptor, hystrix-metrics-binder, jvm-compilation, jvm-gc, jvm-heap-pressure, jvm-info, jvm-memory, jvm-thread, log4j2, logback, processor, uptime", "sourceType": "org.apa [...] { "name": "camel.metrics.clearOnReload", "required": false, "description": "Clear the captured metrics data when Camel is reloading routes such as when using Camel JBang.", "sourceType": "org.apache.camel.main.MetricsConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": true, "secret": false }, diff --git a/core/camel-main/src/main/docs/main.adoc b/core/camel-main/src/main/docs/main.adoc index 78a79e8b33a..c922a5d7d95 100644 --- a/core/camel-main/src/main/docs/main.adoc +++ b/core/camel-main/src/main/docs/main.adoc @@ -563,11 +563,14 @@ The camel.telemetryDev supports 4 options, which are listed below. === Camel MDC configurations -The camel.mdc supports 0 options, which are listed below. +The camel.mdc supports 3 options, which are listed below. [width="100%",cols="2,5,^1,2",options="header"] |=== | Name | Description | Default | Type +| *camel.mdc.customExchange{zwsp}Headers* | Provide the headers you would like to use in the logging. Use * value to include all available headers | | String +| *camel.mdc.customExchange{zwsp}Properties* | Provide the properties you would like to use in the logging. Use * value to include all available properties | | String +| *camel.mdc.enabled* | To enable MDC service | false | boolean |=== 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 84e69be5b48..b7857dd07d9 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 @@ -2790,7 +2790,7 @@ public abstract class BaseMainSupport extends BaseService { answer = camelContext.getCamelContextExtension().getBootstrapFactoryFinder() .newInstance("mdc-service", CamelMDCService.class) .orElseThrow(() -> new IllegalArgumentException( - "Cannot find OpenTelemetryTracer2 on classpath. Add camel-opentelemetry2 to classpath.")); + "Cannot find CamelMDCService on classpath. Add camel-mdc to classpath.")); } return answer; } diff --git a/core/camel-main/src/main/java/org/apache/camel/main/MdcConfigurationProperties.java b/core/camel-main/src/main/java/org/apache/camel/main/MdcConfigurationProperties.java index 4d678032669..1d570b320fb 100644 --- a/core/camel-main/src/main/java/org/apache/camel/main/MdcConfigurationProperties.java +++ b/core/camel-main/src/main/java/org/apache/camel/main/MdcConfigurationProperties.java @@ -28,6 +28,8 @@ public class MdcConfigurationProperties implements BootstrapCloseable { private MainConfigurationProperties parent; private boolean enabled; + private String customExchangeHeaders; + private String customExchangeProperties; public MdcConfigurationProperties(MainConfigurationProperties parent) { this.parent = parent; @@ -47,11 +49,58 @@ public class MdcConfigurationProperties implements BootstrapCloseable { } /** - * To enable Mdc + * To enable MDC service + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getCustomExchangeHeaders() { + return customExchangeHeaders; + } + + /** + * Provide the headers you would like to use in the logging. Use `*` value to include all available headers + */ + public void setCustomExchangeHeaders(String customExchangeHeaders) { + this.customExchangeHeaders = customExchangeHeaders; + } + + public String getCustomExchangeProperties() { + return customExchangeProperties; + } + + /** + * Provide the properties you would like to use in the logging. Use `*` value to include all available + * properties + */ + public void setCustomExchangeProperties(String customExchangeProperties) { + this.customExchangeProperties = customExchangeProperties; + } + + /** + * To enable MDC service */ public MdcConfigurationProperties withEnabled(boolean enabled) { this.enabled = enabled; return this; } + /** + * Provide the headers you would like to use in the logging. Use `*` value to include all available headers + */ + public MdcConfigurationProperties withCustomExchangeHeaders(String customExchangeHeaders) { + this.customExchangeHeaders = customExchangeHeaders; + return this; + } + + /** + * Provide the properties you would like to use in the logging. Use `*` value to include all available + * properties + */ + public MdcConfigurationProperties withCustomExchangeProperties(String customExchangeProperties) { + this.customExchangeProperties = customExchangeProperties; + return this; + } + } diff --git a/proposals/mdc.adoc b/proposals/mdc.adoc index a65c8fd6655..4fb883afbe9 100644 --- a/proposals/mdc.adoc +++ b/proposals/mdc.adoc @@ -5,8 +5,8 @@ authors: reviewers: ["@davsclaus"] approvers: ["@davsclaus"] creation-date: 2025-03-07 -last-updated: 2025-03-21 -status: implementable +last-updated: 2025-09-25 +status: implemented see-also: [] replaces: [] superseded-by: [] @@ -93,3 +93,7 @@ If the above is proven to work effectively, then, in any future major version we == Development This design proposals should not introduce any breaking compatibility changes. The old and new MDC mechanism can coexist, although it will be recommendable to deprecate the old one once the new one proves to work correctly. + +=== `camel-mdc` component (2025-09-25) + +Developed a component which cover the design specifications. See component documentation for more details. \ No newline at end of file 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 324dd3d4176..39bff690a96 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 @@ -233,6 +233,8 @@ public class PrepareCamelMainMojo extends AbstractGeneratorMojo { prefix = "camel.opentelemetry."; } else if (file.getName().contains("TelemetryDev")) { prefix = "camel.telemetryDev."; + } else if (file.getName().contains("MdcConfigurationProperties")) { + prefix = "camel.mdc."; } else if (file.getName().contains("Metrics")) { prefix = "camel.metrics."; } else if (file.getName().contains("HttpServer")) {
