Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions logging/common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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");
}
Expand All @@ -45,19 +57,45 @@ 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<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));
}

/**
* 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);
}

Expand All @@ -68,10 +106,29 @@ public static void clear() {
* @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();
}

}
Original file line number Diff line number Diff line change
@@ -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();
}
}
5 changes: 4 additions & 1 deletion logging/common/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;

}
21 changes: 14 additions & 7 deletions logging/jul/src/test/java/io/helidon/logging/jul/JulMdcTest.java
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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() {
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -114,7 +121,7 @@ private static final class TestCallable implements Callable<String> {

@Override
public String call() {
return JulMdc.get(TEST_KEY);
return JulMdc.get(TEST_KEY) + "/" + JulMdc.get(TEST_SUPPLIED_KEY);
}
}

Expand Down
4 changes: 2 additions & 2 deletions logging/jul/src/test/resources/logging.properties
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
4 changes: 4 additions & 0 deletions tracing/provider-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}

}
}
3 changes: 2 additions & 1 deletion tracing/provider-tests/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand Down
Loading
Loading