diff --git a/logging/common/pom.xml b/logging/common/pom.xml index e183cc106b6..6cbb40566f7 100644 --- a/logging/common/pom.xml +++ b/logging/common/pom.xml @@ -32,5 +32,9 @@ io.helidon.common helidon-common + + io.helidon.common + helidon-common-context + 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 c0a56a4f30e..38a539f4213 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. + *

+ * Helidon permits adding MDC entries using {@code Supplier} 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}. 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 MDC_PROVIDERS = HelidonServiceLoader .builder(ServiceLoader.load(MdcProvider.class)).build().asList(); + private static final ThreadLocal>> SUPPLIERS = ThreadLocal.withInitial(HashMap::new); + private HelidonMdc() { throw new UnsupportedOperationException("This class cannot be instantiated"); } @@ -45,12 +57,37 @@ public static void set(String key, String value) { 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 valueSupplier) { + SUPPLIERS.get().put(key, valueSupplier); + MDC_PROVIDERS.forEach(provider -> provider.put(key, valueSupplier.get())); + } + + /** + * Sets a value supplier without immediately getting the value and propagating the value to + * underlying logging implementations. + *

+ * 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 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 static void remove(String key) { * 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 static void clear() { * @return found value bound to key */ public static Optional 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> suppliers() { + return new HashMap<>(SUPPLIERS.get()); + } + + static void suppliers(Map> 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 00000000000..7c322de8cd0 --- /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>> { + + /** + * For service loading. + */ + public MdcSupplierPropagator() { + } + + @Override + public Map> data() { + return HelidonMdc.suppliers(); + } + + @Override + public void propagateData(Map> data) { + HelidonMdc.suppliers(data); + } + + @Override + public void clearData(Map> data) { + HelidonMdc.clear(); + } +} diff --git a/logging/common/src/main/java/module-info.java b/logging/common/src/main/java/module-info.java index 54f7ecf2985..d6dfd846345 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/logging/jul/src/test/java/io/helidon/logging/jul/JulMdcTest.java b/logging/jul/src/test/java/io/helidon/logging/jul/JulMdcTest.java index ead7958b1f3..e2e4b26afb1 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 void testMdc() { 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 @@ private String logMessage(String message) { @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 void testThreadPropagationWithContext() { @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 @@ private static final class TestCallable implements Callable { @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 c5cbd7ed373..22346db7012 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 1cf63faea3c..9428c8c3962 100644 --- a/tracing/provider-tests/pom.xml +++ b/tracing/provider-tests/pom.xml @@ -50,6 +50,10 @@ io.helidon.common.testing helidon-common-testing-junit5 + + io.helidon.logging + helidon-logging-jul + org.junit.jupiter junit-jupiter-api 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 00000000000..673d0d33781 --- /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 dd4c2b97018..6b03f34efd3 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; diff --git a/tracing/providers/opentelemetry/pom.xml b/tracing/providers/opentelemetry/pom.xml index ec0cee1d1a0..7ac5d563123 100644 --- a/tracing/providers/opentelemetry/pom.xml +++ b/tracing/providers/opentelemetry/pom.xml @@ -117,6 +117,7 @@ **/TestGlobalTracerAssignment.java + **/TestMdc.java @@ -131,6 +132,17 @@ + + mdc-test + + test + + + + **/TestMdc.java + + + diff --git a/tracing/providers/opentracing/pom.xml b/tracing/providers/opentracing/pom.xml index 44c85bb077d..61b32337ffa 100644 --- a/tracing/providers/opentracing/pom.xml +++ b/tracing/providers/opentracing/pom.xml @@ -141,6 +141,19 @@ + + + mdc-test + + test + + + + **/TestMdc.java + + + + diff --git a/tracing/providers/zipkin/pom.xml b/tracing/providers/zipkin/pom.xml index c94c3f65a51..e492d3cc2e0 100644 --- a/tracing/providers/zipkin/pom.xml +++ b/tracing/providers/zipkin/pom.xml @@ -154,6 +154,7 @@ io.helidon.tracing.providers.tests.TestTracerAndSpanPropagation.java + **/TestMdc.java @@ -168,6 +169,17 @@ + + mdc-test + + test + + + + **/TestMdc.java + + + 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 f15ef614c8c..23ae34727b9 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.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 b6c754b654d..d9bca2951b2 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 @@ ) 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;