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()

Reply via email to