Skip to content

Commit

Permalink
Implement MDC auto-instrumentation for log4j1 (#1211)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mateusz Rzeszutek authored Sep 17, 2020
1 parent 9635a5e commit bc8224f
Show file tree
Hide file tree
Showing 7 changed files with 374 additions and 0 deletions.
24 changes: 24 additions & 0 deletions instrumentation/log4j/log4j-1.2/log4j-1.2.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
apply from: "$rootDir/gradle/instrumentation.gradle"

muzzle {
pass {
group = "log4j"
module = "log4j"
versions = "[1.2,)"
}
}

dependencies {
// 1.2 introduces MDC and there's no version earlier than 1.2.4 available
library group: 'log4j', name: 'log4j', version: '1.2.4'
}

configurations {
// In order to test the real log4j library we need to remove the log4j transitive
// dependency 'log4j-over-slf4j' brought in by :testing-common which would shadow
// the log4j module under test using a proxy to slf4j instead.
testImplementation.exclude group: 'org.slf4j', module: 'log4j-over-slf4j'

// See: https://stackoverflow.com/a/9047963/2749853
testImplementation.exclude group: 'javax.jms', module: 'jms'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright The OpenTelemetry 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.opentelemetry.instrumentation.auto.log4j.v1_2;

import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;

import com.google.auto.service.AutoService;
import io.opentelemetry.instrumentation.auto.api.InstrumentationContext;
import io.opentelemetry.javaagent.tooling.Instrumenter;
import io.opentelemetry.trace.Span;
import io.opentelemetry.trace.TracingContextUtils;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import org.apache.log4j.spi.LoggingEvent;

@AutoService(Instrumenter.class)
public class Log4j1Instrumentation extends Instrumenter.Default {
public Log4j1Instrumentation() {
super("log4j1", "log4j");
}

@Override
public ElementMatcher<? super TypeDescription> typeMatcher() {
return named("org.apache.log4j.Category");
}

@Override
public Map<String, String> contextStore() {
return singletonMap("org.apache.log4j.spi.LoggingEvent", Span.class.getName());
}

@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
isMethod()
.and(isPublic())
.and(named("callAppenders"))
.and(takesArguments(1))
.and(takesArgument(0, named("org.apache.log4j.spi.LoggingEvent"))),
Log4j1Instrumentation.class.getName() + "$CallAppendersAdvice");
}

public static class CallAppendersAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(@Advice.Argument(0) LoggingEvent event) {
InstrumentationContext.get(LoggingEvent.class, Span.class)
.put(event, TracingContextUtils.getCurrentSpan());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* Copyright The OpenTelemetry 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.opentelemetry.instrumentation.auto.log4j.v1_2;

import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;

import com.google.auto.service.AutoService;
import io.opentelemetry.instrumentation.auto.api.InstrumentationContext;
import io.opentelemetry.javaagent.tooling.Instrumenter;
import io.opentelemetry.trace.Span;
import io.opentelemetry.trace.SpanContext;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import org.apache.log4j.MDC;
import org.apache.log4j.spi.LoggingEvent;

@AutoService(Instrumenter.class)
public class Log4j1LoggingEventInstrumentation extends Instrumenter.Default {
public Log4j1LoggingEventInstrumentation() {
super("log4j1", "log4j");
}

@Override
public ElementMatcher<? super TypeDescription> typeMatcher() {
return named("org.apache.log4j.spi.LoggingEvent");
}

@Override
public Map<String, String> contextStore() {
return singletonMap("org.apache.log4j.spi.LoggingEvent", Span.class.getName());
}

@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
Map<ElementMatcher.Junction<MethodDescription>, String> transformers = new HashMap<>();

transformers.put(
isMethod()
.and(isPublic())
.and(named("getMDC"))
.and(takesArguments(1))
.and(takesArgument(0, String.class)),
Log4j1LoggingEventInstrumentation.class.getName() + "$GetMdcAdvice");

transformers.put(
isMethod().and(isPublic()).and(named("getMDCCopy")).and(takesArguments(0)),
Log4j1LoggingEventInstrumentation.class.getName() + "$GetMdcCopyAdvice");

return transformers;
}

public static class GetMdcAdvice {
@Advice.OnMethodExit(suppress = Throwable.class)
public static void onExit(
@Advice.This LoggingEvent event,
@Advice.Argument(0) String key,
@Advice.Return(readOnly = false) Object value) {
if ("traceId".equals(key) || "spanId".equals(key) || "sampled".equals(key)) {
if (value != null) {
// Assume already instrumented event if traceId/spanId/sampled is present.
return;
}

Span span = InstrumentationContext.get(LoggingEvent.class, Span.class).get(event);
if (span == null || !span.getContext().isValid()) {
return;
}

SpanContext spanContext = span.getContext();
switch (key) {
case "traceId":
value = spanContext.getTraceIdAsHexString();
break;
case "spanId":
value = spanContext.getSpanIdAsHexString();
break;
case "sampled":
if (spanContext.isSampled()) {
value = "true";
}
break;
}
}
}
}

public static class GetMdcCopyAdvice {
@SuppressWarnings({"unchecked", "rawtypes"})
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.This LoggingEvent event,
@Advice.FieldValue(value = "mdcCopyLookupRequired", readOnly = false) boolean copyRequired,
@Advice.FieldValue(value = "mdcCopy", readOnly = false) Hashtable mdcCopy) {
// this advice basically replaces the original method

if (copyRequired) {
copyRequired = false;

Hashtable mdc = new Hashtable();

Hashtable originalMdc = MDC.getContext();
if (originalMdc != null) {
mdc.putAll(originalMdc);
}

// Assume already instrumented event if traceId is present.
if (!mdc.contains("traceId")) {
Span span = InstrumentationContext.get(LoggingEvent.class, Span.class).get(event);
if (span != null && span.getContext().isValid()) {
SpanContext spanContext = span.getContext();
mdc.put("traceId", spanContext.getTraceIdAsHexString());
mdc.put("spanId", spanContext.getSpanIdAsHexString());
if (spanContext.isSampled()) {
mdc.put("sampled", "true");
}
}
}

mdcCopy = mdc;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright The OpenTelemetry 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.
*/

import org.apache.log4j.AppenderSkeleton
import org.apache.log4j.spi.LoggingEvent

class ListAppender extends AppenderSkeleton {
static events = new ArrayList<LoggingEvent>()

@Override
protected void append(LoggingEvent loggingEvent) {
events.add(loggingEvent)
}

@Override
boolean requiresLayout() {
return false
}

@Override
void close() {
}

static clearEvents() {
events.clear()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright The OpenTelemetry 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.
*/

import io.opentelemetry.auto.test.AgentTestRunner
import io.opentelemetry.auto.test.utils.TraceUtils
import io.opentelemetry.trace.TracingContextUtils
import org.apache.log4j.LogManager

class Log4j1MdcTest extends AgentTestRunner {
def cleanup() {
ListAppender.clearEvents()
}

def "no ids when no span"() {
given:
def logger = LogManager.getLogger('TestLogger')

when:
logger.info("log message 1")
logger.info("log message 2")

then:
def events = ListAppender.events

events.size() == 2
events[0].message == "log message 1"
events[0].getMDC("traceId") == null
events[0].getMDC("spanId") == null
events[0].getMDC("traceFlags") == null

events[1].message == "log message 2"
events[1].getMDC("traceId") == null
events[1].getMDC("spanId") == null
events[1].getMDC("traceFlags") == null
}

def "ids when span"() {
given:
def logger = LogManager.getLogger('TestLogger')

when:
def span1 = TraceUtils.runUnderTrace("test") {
logger.info("log message 1")
TracingContextUtils.currentSpan
}

logger.info("log message 2")

def span2 = TraceUtils.runUnderTrace("test 2") {
logger.info("log message 3")
TracingContextUtils.currentSpan
}

then:
def events = ListAppender.events

events.size() == 3
events[0].message == "log message 1"
events[0].getMDC("traceId") == span1.context.traceIdAsHexString
events[0].getMDC("spanId") == span1.context.spanIdAsHexString
events[0].getMDC("sampled") == "true"

events[1].message == "log message 2"
events[1].getMDC("traceId") == null
events[1].getMDC("spanId") == null
events[1].getMDC("sampled") == null

events[2].message == "log message 3"
// this explicit getMDCCopy() call here is to make sure that whole instrumentation is tested
events[2].getMDCCopy()
events[2].getMDC("traceId") == span2.context.traceIdAsHexString
events[2].getMDC("spanId") == span2.context.spanIdAsHexString
events[2].getMDC("sampled") == "true"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
log4j.rootLogger=INFO, LIST
log4j.appender.LIST=ListAppender
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ include ':instrumentation:kubernetes-client-7.0'
include ':instrumentation:lettuce:lettuce-4.0'
include ':instrumentation:lettuce:lettuce-5.0'
include ':instrumentation:lettuce:lettuce-5.1'
include ':instrumentation:log4j:log4j-1.2'
include ':instrumentation:log4j:log4j-2.13.2:library'
include ':instrumentation:logback:logback-1.0.0:auto'
include ':instrumentation:logback:logback-1.0.0:library'
Expand Down

0 comments on commit bc8224f

Please sign in to comment.