This is an automated email from the ASF dual-hosted git repository. ramanathan1504 pushed a commit to branch fix-trace-metadata-1976 in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit bed2dbe142bfda14988f0c73f71c31edb342e186 Author: Ramanathan <[email protected]> AuthorDate: Fri Jul 3 19:07:52 2026 +0530 Add W3C trace context support with pattern converters for trace ID, span ID, and trace flags --- .../log4j/core/impl/TestTraceContextProvider.java | 57 +++++++++ .../core/impl/TraceContextIntegrationTest.java | 127 +++++++++++++++++++++ ...g.apache.logging.log4j.spi.TraceContextProvider | 1 + .../logging/log4j/core/impl/Log4jLogEvent.java | 18 +-- .../log4j/core/pattern/SpanIdPatternConverter.java | 46 ++++++++ .../core/pattern/TraceFlagsPatternConverter.java | 46 ++++++++ .../core/pattern/TraceIdPatternConverter.java | 46 ++++++++ .../logging/log4j/core/pattern/package-info.java | 2 +- .../core/util/TraceContextProviderService.java | 21 +++- 9 files changed, 350 insertions(+), 14 deletions(-) diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/TestTraceContextProvider.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/TestTraceContextProvider.java new file mode 100644 index 0000000000..8bb0248e1f --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/TestTraceContextProvider.java @@ -0,0 +1,57 @@ +/* + * 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.logging.log4j.core.impl; + +import org.apache.logging.log4j.spi.TraceContextProvider; + +/** + * A test-only TraceContextProvider designed to simulate thread-local trace contexts. + * Mimics active span context lookups in systems like OpenTelemetry or Micrometer. + */ +public class TestTraceContextProvider implements TraceContextProvider { + + private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>(); + private static final ThreadLocal<String> SPAN_ID = new ThreadLocal<>(); + private static final ThreadLocal<String> TRACE_FLAGS = new ThreadLocal<>(); + + public static void setContext(final String traceId, final String spanId, final String traceFlags) { + TRACE_ID.set(traceId); + SPAN_ID.set(spanId); + TRACE_FLAGS.set(traceFlags); + } + + public static void clearContext() { + TRACE_ID.remove(); + SPAN_ID.remove(); + TRACE_FLAGS.remove(); + } + + @Override + public String getTraceId() { + return TRACE_ID.get(); + } + + @Override + public String getSpanId() { + return SPAN_ID.get(); + } + + @Override + public String getTraceFlags() { + return TRACE_FLAGS.get(); + } +} diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/TraceContextIntegrationTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/TraceContextIntegrationTest.java new file mode 100644 index 0000000000..e63da79666 --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/TraceContextIntegrationTest.java @@ -0,0 +1,127 @@ +/* + * 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.logging.log4j.core.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.async.RingBufferLogEvent; +import org.apache.logging.log4j.core.async.RingBufferLogEventTranslator; +import org.apache.logging.log4j.core.util.ClockFactory; +import org.apache.logging.log4j.core.util.DummyNanoClock; +import org.apache.logging.log4j.message.SimpleMessage; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class TraceContextIntegrationTest { + + @BeforeEach + void setUp() { + // No programmatic bypass! Relying entirely on META-INF/services/ auto-discovery + TestTraceContextProvider.setContext("4bf92f3577b34da6a3ce929d0e0e4736", "00f067aa0ba902b7", "01"); + ThreadContext.clearMap(); + } + + @AfterEach + void tearDown() { + TestTraceContextProvider.clearContext(); + ThreadContext.clearMap(); + } + + @Test + public void printCompiledServiceFiles() { + // Look directly at the compiled test resources folder + java.io.File servicesDir = new java.io.File("target/test-classes/META-INF/services"); + if (servicesDir.exists() && servicesDir.isDirectory()) { + System.out.println(">>> FILES IN target/test-classes/META-INF/services/:"); + for (String file : servicesDir.list()) { + System.out.println(">>> - " + file); + } + } else { + System.out.println(">>> ERROR: target/test-classes/META-INF/services directory does not exist!"); + } + } + + @Test + public void testLog4jLogEventNativeCaptureWithEmptyThreadContext() { + assertThat(ThreadContext.getContext()).isEmpty(); + + final LogEvent event = Log4jLogEvent.newBuilder() + .setLoggerName("TestLogger") + .setLevel(Level.INFO) + .setMessage(new SimpleMessage("Standard event testing")) + .build(); + + // Must successfully resolve directly via ServiceLoader auto-discovery + assertThat(event.getTraceId()).isEqualTo("4bf92f3577b34da6a3ce929d0e0e4736"); + assertThat(event.getSpanId()).isEqualTo("00f067aa0ba902b7"); + assertThat(event.getTraceFlags()).isEqualTo("01"); + assertThat(event.getContextData().isEmpty()).isTrue(); + } + + @Test + public void testMutableLogEventNativeCapture() { + final MutableLogEvent event = new MutableLogEvent(); + event.setMessage(new SimpleMessage("Mutable event testing")); + event.initTime(ClockFactory.getClock(), new DummyNanoClock()); + + assertThat(event.getTraceId()).isEqualTo("4bf92f3577b34da6a3ce929d0e0e4736"); + assertThat(event.getSpanId()).isEqualTo("00f067aa0ba902b7"); + assertThat(event.getTraceFlags()).isEqualTo("01"); + assertThat(event.getContextData().isEmpty()).isTrue(); + } + + @Test + public void testRingBufferLogEventTranslatorNativeCapture() { + final RingBufferLogEvent event = new RingBufferLogEvent(); + final RingBufferLogEventTranslator translator = new RingBufferLogEventTranslator(); + + translator.setBasicValues( + null, + "Logger", + null, + "FQCN", + Level.INFO, + new SimpleMessage("Async event testing"), + null, + ThreadContext.getImmutableStack(), + null, + ClockFactory.getClock(), + new DummyNanoClock()); + + translator.translateTo(event, 0); + + assertThat(event.getTraceId()).isEqualTo("4bf92f3577b34da6a3ce929d0e0e4736"); + assertThat(event.getSpanId()).isEqualTo("00f067aa0ba902b7"); + assertThat(event.getTraceFlags()).isEqualTo("01"); + assertThat(event.getContextData().isEmpty()).isTrue(); + } + + @Test + public void testLog4jLogEventDirectConstructorNativeCapture() { + final LogEvent event = new Log4jLogEvent( + "TestLogger", null, "FQCN", Level.INFO, new SimpleMessage("Direct constructor testing"), null, null); + + assertThat(event.getTraceId()).isEqualTo("4bf92f3577b34da6a3ce929d0e0e4736"); + assertThat(event.getSpanId()).isEqualTo("00f067aa0ba902b7"); + assertThat(event.getTraceFlags()).isEqualTo("01"); + assertThat(event.getContextData().isEmpty()).isTrue(); + } +} diff --git a/log4j-core-test/src/test/resources/META-INF/services/org.apache.logging.log4j.spi.TraceContextProvider b/log4j-core-test/src/test/resources/META-INF/services/org.apache.logging.log4j.spi.TraceContextProvider new file mode 100644 index 0000000000..0e5fd8db11 --- /dev/null +++ b/log4j-core-test/src/test/resources/META-INF/services/org.apache.logging.log4j.spi.TraceContextProvider @@ -0,0 +1 @@ +org.apache.logging.log4j.core.impl.TestTraceContextProvider \ No newline at end of file diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java index b7b99bd60e..02ea4e374d 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java @@ -474,9 +474,9 @@ public class Log4jLogEvent implements LogEvent { null, // StackTraceElement source CLOCK, // nanoClock.nanoTime(), - Strings.EMPTY, - Strings.EMPTY, - Strings.EMPTY); + TraceContextProviderService.getActiveProvider().getTraceId(), + TraceContextProviderService.getActiveProvider().getSpanId(), + TraceContextProviderService.getActiveProvider().getTraceFlags()); } /** @@ -514,9 +514,9 @@ public class Log4jLogEvent implements LogEvent { source, // StackTraceElement source CLOCK, // nanoClock.nanoTime(), - Strings.EMPTY, - Strings.EMPTY, - Strings.EMPTY); + TraceContextProviderService.getActiveProvider().getTraceId(), + TraceContextProviderService.getActiveProvider().getSpanId(), + TraceContextProviderService.getActiveProvider().getTraceFlags()); } /** @@ -756,9 +756,9 @@ public class Log4jLogEvent implements LogEvent { ((LoggerNameAwareMessage) message).setLoggerName(loggerName); } this.nanoTime = nanoTime; - this.traceId = traceId; - this.spanId = spanId; - this.traceFlags = traceFlags; + this.traceId = traceId != null ? traceId : Strings.EMPTY; + this.spanId = spanId != null ? spanId : Strings.EMPTY; + this.traceFlags = traceFlags != null ? traceFlags : Strings.EMPTY; } private static StringMap createContextData(final Map<String, String> contextMap) { diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/SpanIdPatternConverter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/SpanIdPatternConverter.java new file mode 100644 index 0000000000..7dd7c3217f --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/SpanIdPatternConverter.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.logging.log4j.core.pattern; + +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.config.plugins.Plugin; + +/** + * converter to format W3C standard span ID natively. + */ +@Plugin(name = "SpanIdPatternConverter", category = PatternConverter.CATEGORY) +@ConverterKeys({"spanId"}) +public final class SpanIdPatternConverter extends LogEventPatternConverter { + + private static final SpanIdPatternConverter INSTANCE = new SpanIdPatternConverter(); + + private SpanIdPatternConverter() { + super("SpanId", "spanId"); + } + + public static SpanIdPatternConverter newInstance(final String[] options) { + return INSTANCE; + } + + @Override + public void format(final LogEvent event, final StringBuilder toAppendTo) { + final String spanId = event.getSpanId(); + if (spanId != null) { + toAppendTo.append(spanId); + } + } +} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/TraceFlagsPatternConverter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/TraceFlagsPatternConverter.java new file mode 100644 index 0000000000..3970d00463 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/TraceFlagsPatternConverter.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.logging.log4j.core.pattern; + +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.config.plugins.Plugin; + +/** + * Built-in converter to format W3C standard trace flags natively. + */ +@Plugin(name = "TraceFlagsPatternConverter", category = PatternConverter.CATEGORY) +@ConverterKeys({"traceFlags"}) +public final class TraceFlagsPatternConverter extends LogEventPatternConverter { + + private static final TraceFlagsPatternConverter INSTANCE = new TraceFlagsPatternConverter(); + + private TraceFlagsPatternConverter() { + super("TraceFlags", "traceFlags"); + } + + public static TraceFlagsPatternConverter newInstance(final String[] options) { + return INSTANCE; + } + + @Override + public void format(final LogEvent event, final StringBuilder toAppendTo) { + final String traceFlags = event.getTraceFlags(); + if (traceFlags != null) { + toAppendTo.append(traceFlags); + } + } +} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/TraceIdPatternConverter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/TraceIdPatternConverter.java new file mode 100644 index 0000000000..c7e84f2252 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/TraceIdPatternConverter.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.logging.log4j.core.pattern; + +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.config.plugins.Plugin; + +/** + * Built-in converter to format W3C standard trace ID natively. + */ +@Plugin(name = "TraceIdPatternConverter", category = PatternConverter.CATEGORY) +@ConverterKeys({"traceId"}) +public final class TraceIdPatternConverter extends LogEventPatternConverter { + + private static final TraceIdPatternConverter INSTANCE = new TraceIdPatternConverter(); + + private TraceIdPatternConverter() { + super("TraceId", "traceId"); + } + + public static TraceIdPatternConverter newInstance(final String[] options) { + return INSTANCE; + } + + @Override + public void format(final LogEvent event, final StringBuilder toAppendTo) { + final String traceId = event.getTraceId(); + if (traceId != null) { + toAppendTo.append(traceId); + } + } +} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/package-info.java index df5bc576a2..9a21c95c79 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/package-info.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/package-info.java @@ -18,7 +18,7 @@ * Provides classes implementing format specifiers in conversion patterns. */ @Export -@Version("2.26.0") +@Version("2.27.0") package org.apache.logging.log4j.core.pattern; import org.osgi.annotation.bundle.Export; diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/TraceContextProviderService.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/TraceContextProviderService.java index 3440e5e4a9..82c06bc27a 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/TraceContextProviderService.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/TraceContextProviderService.java @@ -22,7 +22,8 @@ import org.apache.logging.log4j.spi.TraceContextProvider; import org.apache.logging.log4j.status.StatusLogger; /** - * Service registry designed to discover and cache the active {@link TraceContextProvider}. + * Service registry designed to discover and cache the active {@link TraceContextProvider} + * natively using the standard Java ServiceLoader. */ public final class TraceContextProviderService { @@ -31,9 +32,21 @@ public final class TraceContextProviderService { static { TraceContextProvider found = null; try { - final ServiceLoader<TraceContextProvider> loader = - ServiceLoader.load(TraceContextProvider.class, TraceContextProviderService.class.getClassLoader()); - final Iterator<TraceContextProvider> iterator = loader.iterator(); + // Standard ServiceLoader lookup using the Thread Context ClassLoader (TCCL) + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if (cl == null) { + cl = TraceContextProviderService.class.getClassLoader(); + } + + ServiceLoader<TraceContextProvider> loader = ServiceLoader.load(TraceContextProvider.class, cl); + Iterator<TraceContextProvider> iterator = loader.iterator(); + + if (!iterator.hasNext() && cl != TraceContextProviderService.class.getClassLoader()) { + loader = ServiceLoader.load( + TraceContextProvider.class, TraceContextProviderService.class.getClassLoader()); + iterator = loader.iterator(); + } + if (iterator.hasNext()) { found = iterator.next(); StatusLogger.getLogger()
