diff --git a/README.md b/README.md
index 59c0031f..14f2d70f 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ It contains auto-configurations that instrument and trace the following Spring B
- Reactor
- RxJava
- Redis
-- Standard logging - logs are added to active span
+- Standard logging - Logback and Log4j2 logs are added to the active span
- Spring Messaging - trace messages being sent through [Messaging Channels](https://docs.spring.io/spring-integration/reference/html/#messaging-channels-section)
- RabbitMQ
diff --git a/instrument-starters/opentracing-spring-cloud-core/pom.xml b/instrument-starters/opentracing-spring-cloud-core/pom.xml
index 45f333bf..b8257c82 100644
--- a/instrument-starters/opentracing-spring-cloud-core/pom.xml
+++ b/instrument-starters/opentracing-spring-cloud-core/pom.xml
@@ -47,6 +47,16 @@
spring-boot-configuration-processor
true
+
+ org.apache.logging.log4j
+ log4j-api
+ true
+
+
+ org.apache.logging.log4j
+ log4j-core
+ true
+
org.springframework.boot
spring-boot-starter-aop
diff --git a/instrument-starters/opentracing-spring-cloud-core/src/main/java/io/opentracing/contrib/spring/cloud/log/Log4j2LoggingAutoConfiguration.java b/instrument-starters/opentracing-spring-cloud-core/src/main/java/io/opentracing/contrib/spring/cloud/log/Log4j2LoggingAutoConfiguration.java
new file mode 100644
index 00000000..78340cac
--- /dev/null
+++ b/instrument-starters/opentracing-spring-cloud-core/src/main/java/io/opentracing/contrib/spring/cloud/log/Log4j2LoggingAutoConfiguration.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright © 2017-2026 The OpenTracing Authors
+ *
+ * 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.opentracing.contrib.spring.cloud.log;
+
+import io.opentracing.Tracer;
+import io.opentracing.contrib.spring.tracer.configuration.TracerAutoConfiguration;
+import javax.annotation.PostConstruct;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.config.LoggerConfig;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ConditionalOnBean(Tracer.class)
+@AutoConfigureAfter(TracerAutoConfiguration.class)
+@ConditionalOnClass(name = "org.apache.logging.log4j.core.LoggerContext")
+@ConditionalOnProperty(name = "opentracing.spring.cloud.log.enabled", havingValue = "true", matchIfMissing = true)
+public class Log4j2LoggingAutoConfiguration {
+
+ @Autowired
+ private Tracer tracer;
+
+ @PostConstruct
+ public void postConstruct() {
+ LoggerContext context = (LoggerContext) LogManager.getContext(false);
+ LoggerConfig rootLogger = context.getConfiguration().getRootLogger();
+ Log4j2SpanLogsAppender appender = new Log4j2SpanLogsAppender(tracer);
+ appender.start();
+ rootLogger.addAppender(appender, null, null);
+ context.updateLoggers();
+ }
+}
diff --git a/instrument-starters/opentracing-spring-cloud-core/src/main/java/io/opentracing/contrib/spring/cloud/log/Log4j2SpanLogsAppender.java b/instrument-starters/opentracing-spring-cloud-core/src/main/java/io/opentracing/contrib/spring/cloud/log/Log4j2SpanLogsAppender.java
new file mode 100644
index 00000000..ce16b4bb
--- /dev/null
+++ b/instrument-starters/opentracing-spring-cloud-core/src/main/java/io/opentracing/contrib/spring/cloud/log/Log4j2SpanLogsAppender.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright © 2017-2026 The OpenTracing Authors
+ *
+ * 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.opentracing.contrib.spring.cloud.log;
+
+import io.opentracing.Span;
+import io.opentracing.Tracer;
+import io.opentracing.tag.Tags;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.Property;
+
+public class Log4j2SpanLogsAppender extends AbstractAppender {
+
+ private final Tracer tracer;
+
+ public Log4j2SpanLogsAppender(Tracer tracer) {
+ super(Log4j2SpanLogsAppender.class.getSimpleName(), null, null, true, Property.EMPTY_ARRAY);
+ this.tracer = tracer;
+ }
+
+ @Override
+ public void append(LogEvent event) {
+ Span span = tracer.activeSpan();
+ if (span == null) {
+ return;
+ }
+
+ Map logs = new HashMap<>(6);
+ logs.put("logger", event.getLoggerName());
+ logs.put("level", event.getLevel().toString());
+ logs.put("thread", event.getThreadName());
+ logs.put("message", event.getMessage().getFormattedMessage());
+
+ if (Level.ERROR.equals(event.getLevel())) {
+ Tags.ERROR.set(span, Boolean.TRUE);
+ logs.put("event", Tags.ERROR.getKey());
+ }
+
+ if (event.getThrown() != null) {
+ logs.put("error.object", event.getThrown());
+ }
+ span.log(TimeUnit.MICROSECONDS.convert(event.getTimeMillis(), TimeUnit.MILLISECONDS), logs);
+ }
+}
diff --git a/instrument-starters/opentracing-spring-cloud-core/src/main/resources/META-INF/spring.factories b/instrument-starters/opentracing-spring-cloud-core/src/main/resources/META-INF/spring.factories
index 40123753..b13779b1 100644
--- a/instrument-starters/opentracing-spring-cloud-core/src/main/resources/META-INF/spring.factories
+++ b/instrument-starters/opentracing-spring-cloud-core/src/main/resources/META-INF/spring.factories
@@ -3,6 +3,7 @@ io.opentracing.contrib.spring.cloud.async.CustomAsyncConfigurerAutoConfiguration
io.opentracing.contrib.spring.cloud.async.DefaultAsyncAutoConfiguration,\
io.opentracing.contrib.spring.cloud.scheduled.ScheduledAutoConfiguration,\
io.opentracing.contrib.spring.cloud.log.LoggingAutoConfiguration,\
+io.opentracing.contrib.spring.cloud.log.Log4j2LoggingAutoConfiguration,\
io.opentracing.contrib.spring.cloud.traced.TracedAutoConfiguration
# Environment Post Processor
diff --git a/instrument-starters/opentracing-spring-cloud-core/src/test/java/io/opentracing/contrib/spring/cloud/log/Log4j2SpanLogsAppenderTest.java b/instrument-starters/opentracing-spring-cloud-core/src/test/java/io/opentracing/contrib/spring/cloud/log/Log4j2SpanLogsAppenderTest.java
new file mode 100644
index 00000000..b168f564
--- /dev/null
+++ b/instrument-starters/opentracing-spring-cloud-core/src/test/java/io/opentracing/contrib/spring/cloud/log/Log4j2SpanLogsAppenderTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright © 2017-2026 The OpenTracing Authors
+ *
+ * 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.opentracing.contrib.spring.cloud.log;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+import io.opentracing.mock.MockSpan;
+import io.opentracing.mock.MockTracer;
+import io.opentracing.tag.Tags;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.apache.logging.log4j.message.SimpleMessage;
+import org.junit.Test;
+
+public class Log4j2SpanLogsAppenderTest {
+
+ @Test
+ public void appendsLogEventToActiveSpan() {
+ MockTracer tracer = new MockTracer();
+ MockSpan span = tracer.buildSpan("request").start();
+ Log4j2SpanLogsAppender appender = new Log4j2SpanLogsAppender(tracer);
+ appender.start();
+
+ try (io.opentracing.Scope ignored = tracer.scopeManager().activate(span)) {
+ appender.append(Log4jLogEvent.newBuilder()
+ .setLoggerName("example.Controller")
+ .setLevel(Level.ERROR)
+ .setThreadName("request-thread")
+ .setMessage(new SimpleMessage("request failed"))
+ .setTimeMillis(1000L)
+ .setThrown(new IllegalStateException("failure"))
+ .build());
+ }
+
+ assertEquals(1, span.logEntries().size());
+ MockSpan.LogEntry entry = span.logEntries().get(0);
+ assertEquals("example.Controller", entry.fields().get("logger"));
+ assertEquals("ERROR", entry.fields().get("level"));
+ assertEquals("request-thread", entry.fields().get("thread"));
+ assertEquals("request failed", entry.fields().get("message"));
+ assertEquals(1000000L, entry.timestampMicros());
+ assertSame(IllegalStateException.class, ((Throwable) entry.fields().get("error.object")).getClass());
+ assertEquals(Tags.ERROR.getKey(), entry.fields().get("event"));
+ assertEquals(Boolean.TRUE, span.tags().get(Tags.ERROR.getKey()));
+ span.finish();
+ }
+}
diff --git a/instrument-starters/opentracing-spring-cloud-feign-starter/src/main/java/io/opentracing/contrib/spring/cloud/feign/FeignContextBeanPostProcessor.java b/instrument-starters/opentracing-spring-cloud-feign-starter/src/main/java/io/opentracing/contrib/spring/cloud/feign/FeignContextBeanPostProcessor.java
index 756d115e..13dabaf9 100644
--- a/instrument-starters/opentracing-spring-cloud-feign-starter/src/main/java/io/opentracing/contrib/spring/cloud/feign/FeignContextBeanPostProcessor.java
+++ b/instrument-starters/opentracing-spring-cloud-feign-starter/src/main/java/io/opentracing/contrib/spring/cloud/feign/FeignContextBeanPostProcessor.java
@@ -19,7 +19,6 @@
import io.opentracing.Tracer;
import java.util.List;
import org.springframework.beans.BeansException;
-import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.cloud.openfeign.FeignContext;
@@ -30,10 +29,12 @@ public class FeignContextBeanPostProcessor implements BeanPostProcessor {
private Tracer tracer;
private List spanDecorators;
+ private TracedFeignBeanFactory tracedFeignBeanFactory;
FeignContextBeanPostProcessor(Tracer tracer, List spanDecorators) {
this.tracer = tracer;
this.spanDecorators = spanDecorators;
+ this.tracedFeignBeanFactory = new TracedFeignBeanFactory(tracer, spanDecorators);
}
@Override
@@ -46,6 +47,6 @@ public Object postProcessBeforeInitialization(Object bean, String name) throws B
@Override
public Object postProcessAfterInitialization(Object bean, String name) throws BeansException {
- return bean;
+ return tracedFeignBeanFactory.from(bean);
}
}
diff --git a/instrument-starters/opentracing-spring-cloud-feign-starter/src/main/java/io/opentracing/contrib/spring/cloud/feign/FeignTracingAutoConfiguration.java b/instrument-starters/opentracing-spring-cloud-feign-starter/src/main/java/io/opentracing/contrib/spring/cloud/feign/FeignTracingAutoConfiguration.java
index 73d41b34..fe0a4594 100644
--- a/instrument-starters/opentracing-spring-cloud-feign-starter/src/main/java/io/opentracing/contrib/spring/cloud/feign/FeignTracingAutoConfiguration.java
+++ b/instrument-starters/opentracing-spring-cloud-feign-starter/src/main/java/io/opentracing/contrib/spring/cloud/feign/FeignTracingAutoConfiguration.java
@@ -16,17 +16,11 @@
package io.opentracing.contrib.spring.cloud.feign;
import feign.Client;
-import feign.Request;
import feign.opentracing.FeignSpanDecorator;
-import feign.opentracing.TracingClient;
import feign.opentracing.hystrix.TracingConcurrencyStrategy;
import io.opentracing.Tracer;
import io.opentracing.contrib.spring.tracer.configuration.TracerAutoConfiguration;
import java.util.List;
-import org.aspectj.lang.ProceedingJoinPoint;
-import org.aspectj.lang.annotation.Around;
-import org.aspectj.lang.annotation.Aspect;
-import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
@@ -74,29 +68,4 @@ public HystrixFeign(Tracer tracer) {
TracingConcurrencyStrategy.register(tracer);
}
}
-
- @Bean
- public TracingAspect tracingAspect() {
- return new TracingAspect();
- }
-
- /**
- * Trace feign clients created manually
- */
- @Aspect
- class TracingAspect {
-
- @Around("execution (* feign.Client.*(..)) && !within(is(FinalType))")
- public Object feignClientWasCalled(final ProceedingJoinPoint pjp) throws Throwable {
- Object bean = pjp.getTarget();
- if (!(bean instanceof TracingClient)) {
- Object[] args = pjp.getArgs();
- return new TracingClientBuilder((Client) bean, tracer)
- .withFeignSpanDecorators(spanDecorators)
- .build()
- .execute((Request) args[0], (Request.Options) args[1]);
- }
- return pjp.proceed();
- }
- }
}
diff --git a/instrument-starters/opentracing-spring-cloud-feign-starter/src/test/java/io/opentracing/contrib/spring/cloud/feign/FeignContextBeanPostProcessorTest.java b/instrument-starters/opentracing-spring-cloud-feign-starter/src/test/java/io/opentracing/contrib/spring/cloud/feign/FeignContextBeanPostProcessorTest.java
new file mode 100644
index 00000000..b50f506e
--- /dev/null
+++ b/instrument-starters/opentracing-spring-cloud-feign-starter/src/test/java/io/opentracing/contrib/spring/cloud/feign/FeignContextBeanPostProcessorTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright © 2017-2026 The OpenTracing Authors
+ *
+ * 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.opentracing.contrib.spring.cloud.feign;
+
+import static org.junit.Assert.assertTrue;
+
+import feign.Client;
+import feign.Request;
+import feign.Response;
+import feign.opentracing.TracingClient;
+import io.opentracing.mock.MockTracer;
+import java.io.IOException;
+import java.util.Collections;
+import org.junit.Test;
+
+public class FeignContextBeanPostProcessorTest {
+
+ @Test
+ public void wrapsFinalFeignClientWithoutSubclassProxy() throws IOException {
+ FeignContextBeanPostProcessor postProcessor = new FeignContextBeanPostProcessor(
+ new MockTracer(), Collections.emptyList());
+
+ Object client = postProcessor.postProcessAfterInitialization(new FinalClient(), "client");
+
+ assertTrue(client instanceof TracingClient);
+ }
+
+ private static final class FinalClient implements Client {
+ @Override
+ public Response execute(Request request, Request.Options options) {
+ return null;
+ }
+ }
+}
diff --git a/instrument-starters/opentracing-spring-cloud-feign-starter/src/test/java/io/opentracing/contrib/spring/cloud/feign/FeignManualOkhttpClientTest.java b/instrument-starters/opentracing-spring-cloud-feign-starter/src/test/java/io/opentracing/contrib/spring/cloud/feign/FeignManualOkhttpClientTest.java
new file mode 100644
index 00000000..f35f711f
--- /dev/null
+++ b/instrument-starters/opentracing-spring-cloud-feign-starter/src/test/java/io/opentracing/contrib/spring/cloud/feign/FeignManualOkhttpClientTest.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright © 2017-2026 The OpenTracing Authors
+ *
+ * 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.opentracing.contrib.spring.cloud.feign;
+
+import org.springframework.test.context.TestPropertySource;
+
+@TestPropertySource(properties = "feign.okhttp.enabled=true")
+public class FeignManualOkhttpClientTest extends FeignManualTest {
+}
diff --git a/instrument-starters/opentracing-spring-cloud-feign-starter/src/test/java/io/opentracing/contrib/spring/cloud/feign/FeignManualTest.java b/instrument-starters/opentracing-spring-cloud-feign-starter/src/test/java/io/opentracing/contrib/spring/cloud/feign/FeignManualTest.java
index e1ed406f..61f5ae3d 100644
--- a/instrument-starters/opentracing-spring-cloud-feign-starter/src/test/java/io/opentracing/contrib/spring/cloud/feign/FeignManualTest.java
+++ b/instrument-starters/opentracing-spring-cloud-feign-starter/src/test/java/io/opentracing/contrib/spring/cloud/feign/FeignManualTest.java
@@ -20,6 +20,7 @@
import feign.Client;
import feign.Feign;
import feign.RequestLine;
+import feign.opentracing.TracingClient;
import io.opentracing.contrib.spring.cloud.feign.FeignManualTest.ManualFeignConfiguration;
import io.opentracing.contrib.spring.cloud.feign.FeignTest.FeignRibbonLocalConfiguration;
import io.opentracing.mock.MockTracer;
@@ -51,6 +52,7 @@ static class ManualFeignConfiguration {
@Autowired
public ManualFeignConfiguration(Client client) {
+ manualClient = client;
feignInterface = Feign.builder().client(client)
.target(FeignInterface.class, "http://localService");
}
@@ -66,12 +68,14 @@ interface FeignInterface {
}
protected static FeignInterface feignInterface;
+ protected static Client manualClient;
@Autowired
protected MockTracer mockTracer;
@Test
public void testTracedRequestDefinedUrl() throws InterruptedException {
+ org.junit.Assert.assertTrue(manualClient instanceof TracingClient);
feignInterface.hello();
verify(mockTracer);
}