Repository: helidon-io/helidon (java)

Issue:
Logging and tracing correlation
Add support for logging a trace id through Helidon MDC. This can be a separate module, or just update to existing tracing implementation, where we would add the top level span traceId to Helidon Logging MDC. Then users could use one of our logging integration modules to log the trace id with every single log statement.

This feature is important for correlation of traces and log statements (such as when using Verrazzano)

Pull request:
Enhance MDC to allow mapping a key to a supplier of string; enhance tracing to add `trace_id` 
### Description
Resolves #5741 

## Release note
____
If you use tracing, Helidon now automatically provides a logging MDC (mapped diagnostic context) key `trace_id` which you can use in logging messages to display the trace ID of the current span if there is one.

Also, Helidon's MDC provider implementation now permits MDC keys to be added with a `Supplier<String>` value or a `String`. (Previously only `String` values could be added.)
____


## PR Overview
(simplified since the original PR)

Two parts:

### Changes in logging
Previously, `HelidonMdc` (the main entry point for Helidon and user code to manage MDC) would simply delegate each method to the corresponding method on all `MdcProvider` instances. The providers would use their own context storage or context storage provided by the underlying logging implementation to store key/value pairs.

Now, `HelidonMdc` keeps its own `Map<String, Supplier<String>>` and first tries to resolve a look-up using that. If that map does not contain a supplier for a key, `HelidonMdc#get` delegates to the available MDC providers as it used to. 

The PR also includes changes to tests for the new supplier feature.

### Changes in tracing
The Helidon `TracerProviderHelper` class has always been initialized with whatever specific tracing provider library the user adds to her project. This init code now registers the `trace_id` MDC key with a supplier that gets the current span (if there is one) and returns the trace ID from it. 

This code uses the Helidon neutral tracing API to get the current span and, from it, the span context (which contains the trace ID. But this works because the Helidon neutral code to get the current span delegates to the underlying implementation (OpenTelemetry, Zipkin, etc.) so the `Supplier<String>` will always retrieve the current span, even if user code has set the current span using the underlying library's API, not the Helidon one.

The PR also adds a test to the provider tests that all providers run. It tests the MDC `trace_id`  functionality.

### Documentation
Depending on timing, PR #10137 could include a note about this or the new logging page that PR creates could be separately updated to describe the new `tracer_id` supported added by this PR.

Patch:
diff --git a/logging/common/pom.xml b/logging/common/pom.xml
index e183cc106b..6cbb40566f 100644
--- a/logging/common/pom.xml
+++ b/logging/common/pom.xml
@@ -32,5 +32,9 @@
             <groupId>io.helidon.common</groupId>
             <artifactId>helidon-common</artifactId>
         </dependency>
+        <dependency>
+            <groupId>io.helidon.common</groupId>
+            <artifactId>helidon-common-context</artifactId>
+        </dependency>
     </dependencies>
 </project>
diff --git a/logging/common/src/main/java/io/helidon/logging/common/HelidonMdc.java b/logging/common/src/main/java/io/helidon/logging/common/HelidonMdc.java
index c0a56a4f30..38a539f421 100644
--- a/logging/common/src/main/java/io/helidon/logging/common/HelidonMdc.java
+++ b/logging/common/src/main/java/io/helidon/logging/common/HelidonMdc.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020, 2022 Oracle and/or its affiliates.
+ * Copyright (c) 2020, 2025 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,22 +15,34 @@
  */
 package io.helidon.logging.common;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.ServiceLoader;
+import java.util.function.Supplier;
 
 import io.helidon.common.HelidonServiceLoader;
 import io.helidon.logging.common.spi.MdcProvider;
 
 /**
  * Helidon MDC delegates values across all of the supported logging frameworks on the classpath.
+ * <p>
+ * Helidon permits adding MDC entries using {@code Supplier<String>} values as well as direct {@code String} values.
+ * Although some logging implementations provide their own context maps (for example {@code ThreadContext} in Log4J and
+ * {@code MDC} in SLF4J), they map MDC keys to {@code String} values, not to arbitrary objects that would accommodate
+ * {@code Supplier<String>}. Therefore, Helidon not only propagates every {@code set} operation to the loaded MDC providers,
+ * but also manages its own map of key/supplier pairs. Helidon resolves each lookup using that map
+ * if possible, delegating a look-up to the loaded MDC providers only if there is no supplier for a key.
  */
 public class HelidonMdc {
 
     private static final List<MdcProvider> MDC_PROVIDERS = HelidonServiceLoader
             .builder(ServiceLoader.load(MdcProvider.class)).build().asList();
 
+    private static final ThreadLocal<Map<String, Supplier<String>>> SUPPLIERS = ThreadLocal.withInitial(HashMap::new);
+
     private HelidonMdc() {
         throw new UnsupportedOperationException("This class cannot be instantiated");
     }
@@ -45,12 +57,37 @@ public class HelidonMdc {
         MDC_PROVIDERS.forEach(provider -> provider.put(key, value));
     }
 
+    /**
+     * Propagate the value supplier to all {@link MdcProvider} instances registered.
+     *
+     * @param key entry key
+     * @param valueSupplier supplier of the entry value
+     */
+    public static void set(String key, Supplier<String> valueSupplier) {
+        SUPPLIERS.get().put(key, valueSupplier);
+        MDC_PROVIDERS.forEach(provider -> provider.put(key, valueSupplier.get()));
+    }
+
+    /**
+     * Sets a value supplier <em>without</em> immediately getting the value and propagating the value to
+     * underlying logging implementations.
+     * <p>
+     * Normally, user code should use {@link #set(String, java.util.function.Supplier)} instead.
+     *
+     * @param key entry key
+     * @param valueSupplier  supplier of the entry value
+     */
+    public static void setDeferred(String key, Supplier<String> valueSupplier) {
+        SUPPLIERS.get().put(key, valueSupplier);
+    }
+
     /**
      * Remove value with the specific key from all of the instances of {@link MdcProvider}.
      *
      * @param key key
      */
     public static void remove(String key) {
+        SUPPLIERS.get().remove(key);
         MDC_PROVIDERS.forEach(provider -> provider.remove(key));
     }
 
@@ -58,6 +95,7 @@ public class HelidonMdc {
      * Remove all of the entries bound to the current thread from the instances of {@link MdcProvider}.
      */
     public static void clear() {
+        SUPPLIERS.get().clear();
         MDC_PROVIDERS.forEach(MdcProvider::clear);
     }
 
@@ -68,10 +106,29 @@ public class HelidonMdc {
      * @return found value bound to key
      */
     public static Optional<String> get(String key) {
-        return MDC_PROVIDERS.stream()
-                .map(provider -> provider.get(key))
-                .filter(Objects::nonNull)
-                .findFirst();
+        /*
+        User or 3rd-party code might have added values directly to the logger's own context store. So look in other
+        providers if our data structure cannot resolve the key.
+         */
+        return SUPPLIERS.get().containsKey(key)
+                ? Optional.of(SUPPLIERS.get().get(key).get())
+                : MDC_PROVIDERS.stream()
+                        .map(provider -> provider.get(key))
+                        .filter(Objects::nonNull)
+                        .findFirst();
+    }
+
+    static Map<String, Supplier<String>> suppliers() {
+        return new HashMap<>(SUPPLIERS.get());
+    }
+
+    static void suppliers(Map<String, Supplier<String>> suppliers) {
+        SUPPLIERS.get().clear();
+        SUPPLIERS.get().putAll(suppliers);
+    }
+
+    static void clearSuppliers() {
+        SUPPLIERS.get().clear();
     }
 
 }
diff --git a/logging/common/src/main/java/io/helidon/logging/common/MdcSupplierPropagator.java b/logging/common/src/main/java/io/helidon/logging/common/MdcSupplierPropagator.java
new file mode 100644
index 0000000000..7c322de8cd
--- /dev/null
+++ b/logging/common/src/main/java/io/helidon/logging/common/MdcSupplierPropagator.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.logging.common;
+
+import java.util.Map;
+import java.util.function.Supplier;
+
+import io.helidon.common.context.spi.DataPropagationProvider;
+
+/**
+ * Data propagator for key/supplier MDC data.
+ */
+public class MdcSupplierPropagator implements DataPropagationProvider<Map<String, Supplier<String>>> {
+
+    /**
+     * For service loading.
+     */
+    public MdcSupplierPropagator() {
+    }
+
+    @Override
+    public Map<String, Supplier<String>> data() {
+        return HelidonMdc.suppliers();
+    }
+
+    @Override
+    public void propagateData(Map<String, Supplier<String>> data) {
+        HelidonMdc.suppliers(data);
+    }
+
+    @Override
+    public void clearData(Map<String, Supplier<String>> data) {
+        HelidonMdc.clear();
+    }
+}
diff --git a/logging/common/src/main/java/module-info.java b/logging/common/src/main/java/module-info.java
index 54f7ecf298..d6dfd84634 100644
--- a/logging/common/src/main/java/module-info.java
+++ b/logging/common/src/main/java/module-info.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020, 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2020, 2025 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -20,11 +20,14 @@
 module io.helidon.logging.common {
 
     requires io.helidon.common;
+    requires io.helidon.common.context;
 
     exports io.helidon.logging.common;
     exports io.helidon.logging.common.spi;
 
     uses io.helidon.logging.common.spi.MdcProvider;
     uses io.helidon.logging.common.spi.LoggingProvider;
+
+    provides io.helidon.common.context.spi.DataPropagationProvider with io.helidon.logging.common.MdcSupplierPropagator;
 	
 }
\ No newline at end of file
diff --git a/tracing/providers/opentelemetry/pom.xml b/tracing/providers/opentelemetry/pom.xml
index ec0cee1d1a..7ac5d56312 100644
--- a/tracing/providers/opentelemetry/pom.xml
+++ b/tracing/providers/opentelemetry/pom.xml
@@ -117,6 +117,7 @@
                         <configuration>
                             <excludes>
                                 <exclude>**/TestGlobalTracerAssignment.java</exclude>
+                                <exclude>**/TestMdc.java</exclude>
                             </excludes>
                         </configuration>
                     </execution>
@@ -131,6 +132,17 @@
                             </includes>
                         </configuration>
                     </execution>
+                    <execution>
+                        <id>mdc-test</id>
+                        <goals>
+                            <goal>test</goal>
+                        </goals>
+                        <configuration>
+                            <includes>
+                                <include>**/TestMdc.java</include>
+                            </includes>
+                        </configuration>
+                    </execution>
                 </executions>
             </plugin>
         </plugins>
diff --git a/tracing/providers/opentracing/pom.xml b/tracing/providers/opentracing/pom.xml
index 44c85bb077..61b32337ff 100644
--- a/tracing/providers/opentracing/pom.xml
+++ b/tracing/providers/opentracing/pom.xml
@@ -141,6 +141,19 @@
                         </configurationParameters>
                     </properties>
                 </configuration>
+                <executions>
+                    <execution>
+                        <id>mdc-test</id>
+                        <goals>
+                            <goal>test</goal>
+                        </goals>
+                        <configuration>
+                            <includes>
+                                <include>**/TestMdc.java</include>
+                            </includes>
+                        </configuration>
+                    </execution>
+                </executions>
             </plugin>
         </plugins>
     </build>
diff --git a/tracing/providers/zipkin/pom.xml b/tracing/providers/zipkin/pom.xml
index c94c3f65a5..e492d3cc2e 100644
--- a/tracing/providers/zipkin/pom.xml
+++ b/tracing/providers/zipkin/pom.xml
@@ -154,6 +154,7 @@
                         <configuration>
                             <excludes>
                                 <exclude>io.helidon.tracing.providers.tests.TestTracerAndSpanPropagation.java</exclude>
+                                <exclude>**/TestMdc.java</exclude>
                             </excludes>
                         </configuration>
                     </execution>
@@ -168,6 +169,17 @@
                             </includes>
                         </configuration>
                     </execution>
+                    <execution>
+                        <id>mdc-test</id>
+                        <goals>
+                            <goal>test</goal>
+                        </goals>
+                        <configuration>
+                            <includes>
+                                <include>**/TestMdc.java</include>
+                            </includes>
+                        </configuration>
+                    </execution>
                 </executions>
             </plugin>
         </plugins>
diff --git a/tracing/tracing/src/main/java/io/helidon/tracing/TracerProviderHelper.java b/tracing/tracing/src/main/java/io/helidon/tracing/TracerProviderHelper.java
index f15ef614c8..23ae34727b 100644
--- a/tracing/tracing/src/main/java/io/helidon/tracing/TracerProviderHelper.java
+++ b/tracing/tracing/src/main/java/io/helidon/tracing/TracerProviderHelper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, 2024 Oracle and/or its affiliates.
+ * Copyright (c) 2019, 2025 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import java.util.Optional;
 import java.util.ServiceLoader;
 
 import io.helidon.common.HelidonServiceLoader;
+import io.helidon.logging.common.HelidonMdc;
 import io.helidon.tracing.spi.TracerProvider;
 
 /**
@@ -52,6 +53,17 @@ final class TracerProviderHelper {
         }
 
         TRACER_PROVIDER = provider == null ? new NoOpTracerProvider() : provider;
+
+        /*
+        To obtain the current span, the Helidon tracing providers delegate to the underlying implementations.
+        Therefore we can centrally set up MDC support for the trace ID here, using the neutral API, and still correctly
+        retrieve the current trace ID for logging even if the user's code has used the underlying tracing library directly to
+        activate a span.
+         */
+        HelidonMdc.setDeferred("trace_id", () -> currentSpan()
+                .map(Span::context)
+                .map(SpanContext::traceId)
+                .orElse("none"));
     }
 
     private TracerProviderHelper() {
diff --git a/tracing/tracing/src/main/java/module-info.java b/tracing/tracing/src/main/java/module-info.java
index b6c754b654..d9bca2951b 100644
--- a/tracing/tracing/src/main/java/module-info.java
+++ b/tracing/tracing/src/main/java/module-info.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2018, 2024 Oracle and/or its affiliates.
+ * Copyright (c) 2018, 2025 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -29,12 +29,11 @@ import io.helidon.common.features.api.HelidonFlavor;
 )
 module io.helidon.tracing {
 
-    requires io.helidon.common;
-
     requires static io.helidon.common.features.api;
     requires static io.helidon.config.metadata;
 
     requires transitive io.helidon.common.config;
+    requires io.helidon.logging.common;
 
     exports io.helidon.tracing;
     exports io.helidon.tracing.spi;


Test patch:
diff --git a/logging/jul/src/test/java/io/helidon/logging/jul/JulMdcTest.java b/logging/jul/src/test/java/io/helidon/logging/jul/JulMdcTest.java
index ead7958b1f..e2e4b26afb 100644
--- a/logging/jul/src/test/java/io/helidon/logging/jul/JulMdcTest.java
+++ b/logging/jul/src/test/java/io/helidon/logging/jul/JulMdcTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020, 2024 Oracle and/or its affiliates.
+ * Copyright (c) 2020, 2025 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -45,6 +45,8 @@ public class JulMdcTest {
 
     private static final String TEST_KEY = "test";
     private static final String TEST_VALUE = "value";
+    private static final String TEST_SUPPLIED_KEY = "supplied_test";
+    private static final String TEST_SUPPLIED_VALUE = "supplied_value";
 
     @Test
     public void testMdc() {
@@ -54,19 +56,22 @@ public class JulMdcTest {
             LogConfig.initClass();
             OUTPUT_STREAM.reset();
             HelidonMdc.set(TEST_KEY, TEST_VALUE);
+            HelidonMdc.set(TEST_SUPPLIED_KEY, () -> TEST_SUPPLIED_VALUE);
             String message = "This is test logging message";
             String thread = Thread.currentThread().toString();
             String logMessage = logMessage(message);
-            assertThat(logMessage, endsWith(thread + ": " + message + " " + TEST_VALUE + System.lineSeparator()));
+            assertThat(logMessage, endsWith(thread + ": " + message + " " + TEST_VALUE + " " + TEST_SUPPLIED_VALUE + System.lineSeparator()));
 
             HelidonMdc.remove(TEST_KEY);
+            HelidonMdc.remove(TEST_SUPPLIED_KEY);
             logMessage = logMessage(message);
-            assertThat(logMessage, endsWith(thread + ": " + message + " " + System.lineSeparator()));
+            assertThat(logMessage, endsWith(thread + ": " + message + " " + " " + System.lineSeparator()));
 
             HelidonMdc.set(TEST_KEY, TEST_VALUE);
+            HelidonMdc.set(TEST_SUPPLIED_KEY, () -> TEST_SUPPLIED_VALUE);
             HelidonMdc.clear();
             logMessage = logMessage(message);
-            assertThat(logMessage, endsWith(thread + ": " + message + " " + System.lineSeparator()));
+            assertThat(logMessage, endsWith(thread + ": " + message + " " + " " + System.lineSeparator()));
         } finally {
             System.setOut(original);
         }
@@ -84,13 +89,14 @@ public class JulMdcTest {
     @Test
     public void testThreadPropagationWithContext() {
         HelidonMdc.set(TEST_KEY, TEST_VALUE);
+        HelidonMdc.set(TEST_SUPPLIED_KEY, () -> TEST_SUPPLIED_VALUE);
         Context context = Context.create();
         ExecutorService executor = Contexts.wrap(Executors.newFixedThreadPool(1));
 
         Contexts.runInContext(context, () -> {
             try {
                 String value = executor.submit(new TestCallable()).get();
-                assertThat(value, is(TEST_VALUE));
+                assertThat(value, is(TEST_VALUE + "/" + TEST_SUPPLIED_VALUE));
             } catch (Exception e) {
                 throw new ExecutorException("failed to execute", e);
             }
@@ -100,11 +106,12 @@ public class JulMdcTest {
     @Test
     public void testThreadPropagationWithoutContext() {
         HelidonMdc.set(TEST_KEY, TEST_VALUE);
+        HelidonMdc.set(TEST_SUPPLIED_KEY, () -> TEST_SUPPLIED_VALUE);
         ExecutorService executor = Contexts.wrap(Executors.newFixedThreadPool(1));
 
         try {
             String value = executor.submit(new TestCallable()).get();
-            assertThat(value, is(TEST_VALUE));
+            assertThat(value, is(TEST_VALUE + "/" + TEST_SUPPLIED_VALUE));
         } catch (Exception e) {
             throw new ExecutorException("failed to execute", e);
         }
@@ -114,7 +121,7 @@ public class JulMdcTest {
 
         @Override
         public String call() {
-            return JulMdc.get(TEST_KEY);
+            return JulMdc.get(TEST_KEY) + "/" + JulMdc.get(TEST_SUPPLIED_KEY);
         }
     }
 
diff --git a/logging/jul/src/test/resources/logging.properties b/logging/jul/src/test/resources/logging.properties
index c5cbd7ed37..22346db701 100644
--- a/logging/jul/src/test/resources/logging.properties
+++ b/logging/jul/src/test/resources/logging.properties
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2020 Oracle and/or its affiliates.
+# Copyright (c) 2020, 2025 Oracle and/or its affiliates.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -18,7 +18,7 @@
 handlers=io.helidon.logging.jul.HelidonConsoleHandler
 
 # HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread
-java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s %X{test}%n
+java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s %X{test} %X{supplied_test}%n
 
 # Global logging level. Can be overridden by specific loggers
 .level=INFO
diff --git a/tracing/provider-tests/pom.xml b/tracing/provider-tests/pom.xml
index 1cf63faea3..9428c8c396 100644
--- a/tracing/provider-tests/pom.xml
+++ b/tracing/provider-tests/pom.xml
@@ -50,6 +50,10 @@
             <groupId>io.helidon.common.testing</groupId>
             <artifactId>helidon-common-testing-junit5</artifactId>
         </dependency>
+        <dependency>
+            <groupId>io.helidon.logging</groupId>
+            <artifactId>helidon-logging-jul</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter-api</artifactId>
diff --git a/tracing/provider-tests/src/main/java/io/helidon/tracing/providers/tests/TestMdc.java b/tracing/provider-tests/src/main/java/io/helidon/tracing/providers/tests/TestMdc.java
new file mode 100644
index 0000000000..673d0d3378
--- /dev/null
+++ b/tracing/provider-tests/src/main/java/io/helidon/tracing/providers/tests/TestMdc.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.tracing.providers.tests;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
+
+import io.helidon.common.testing.junit5.InMemoryLoggingHandler;
+import io.helidon.logging.jul.HelidonFormatter;
+import io.helidon.tracing.Scope;
+import io.helidon.tracing.Span;
+import io.helidon.tracing.Tracer;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+
+class TestMdc {
+
+    @BeforeAll
+    static void beforeAll() throws IOException {
+        String loggingConfig = "java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s "
+                + "!thread!: %5$s%6$s trace_id %X{trace_id}%n\n";
+
+        LogManager.getLogManager().readConfiguration(new ByteArrayInputStream(loggingConfig.getBytes(StandardCharsets.UTF_8)));
+    }
+
+    @Test
+    void testTraceId() {
+
+        Logger logger = Logger.getLogger(TestMdc.class.getName());
+        HelidonFormatter helidonFormatter = new HelidonFormatter();
+        try (InMemoryLoggingHandler loggingHandler = InMemoryLoggingHandler.create(logger)) {
+            loggingHandler.setFormatter(helidonFormatter);
+            Span span = Tracer.global().spanBuilder("logging-test-span").start();
+            String expectedTraceId = span.context().traceId();
+            String formattedMessage;
+            try (Scope ignored = span.activate()) {
+                logger.log(Level.INFO, "Test log message");
+                formattedMessage = helidonFormatter.format(loggingHandler.logRecords().getFirst());
+            }
+
+            assertThat("MDC-processed log message",
+                       formattedMessage,
+                       containsString("trace_id " + expectedTraceId));
+        }
+
+    }
+}
diff --git a/tracing/provider-tests/src/main/java/module-info.java b/tracing/provider-tests/src/main/java/module-info.java
index dd4c2b9701..6b03f34efd 100644
--- a/tracing/provider-tests/src/main/java/module-info.java
+++ b/tracing/provider-tests/src/main/java/module-info.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2024 Oracle and/or its affiliates.
+ * Copyright (c) 2024, 2025 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
 module io.helidon.tracing.provider.tests {
 
     requires java.logging;
+    requires io.helidon.logging.jul;
     requires io.helidon.tracing;
     requires io.helidon.common.context;
     requires io.helidon.common.testing.junit5;