Capture log4j 1.x events
MODIFY
agent-tooling/src/main/java/io/opentelemetry/auto/tooling/GlobalIgnoresMatcher.java
agent-tooling/src/main/java/io/opentelemetry/auto/tooling/GlobalIgnoresMatcher.java
@@ -93,14 +93,6 @@ class GlobalIgnoresMatcher<T extends TypeDescription>
return true;
}
- if (name.startsWith("org.apache.log4j.")) {
- if (name.equals("org.apache.log4j.MDC")) {
- return false;
- }
-
- return true;
- }
-
if (name.startsWith("org.slf4j.")) {
if (name.equals("org.slf4j.MDC")) {
return false;

ADD
instrumentation/log4j-events-1.1/log4j-events-1.1.gradle
+muzzle {
+ pass {
+ group = 'log4j'
+ module = 'log4j'
+ versions = '(,)'
+ }
+}
+
+apply from: "${rootDir}/gradle/java.gradle"
+
+apply plugin: 'org.unbroken-dome.test-sets'
+
+testSets {
+ latestDepTest {
+ dirName = 'test'
+ }
+}
+
+configurations {
+ // In order to test the real log4j library we need to remove the log4j transitive
+ // dependency 'log4j-over-slf4j' brought in by :testing which would shadow
+ // the log4j module under test using a proxy to slf4j instead.
+ testCompile.exclude group: 'org.slf4j', module: 'log4j-over-slf4j'
+
+ // See: https://stackoverflow.com/a/9047963/2749853
+ testCompile.exclude group: 'javax.jms', module: 'jms'
+}
+
+dependencies {
+ compileOnly group: 'log4j', name: 'log4j', version: '1.2.16'
+
+ testCompile group: 'log4j', name: 'log4j', version: '1.2.16'
+
+ latestDepTestCompile group: 'log4j', name: 'log4j', version: '+'
+}

ADD
instrumentation/log4j-events-1.1/src/main/java/io/opentelemetry/auto/instrumentation/log4jevents/Log4jEventInstrumentation.java
+package io.opentelemetry.auto.instrumentation.log4jevents;
+
+import static net.bytebuddy.matcher.ElementMatchers.isMethod;
+import static net.bytebuddy.matcher.ElementMatchers.isProtected;
+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.auto.tooling.Instrumenter;
+import java.util.HashMap;
+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.Category;
+import org.apache.log4j.Priority;
+
+@AutoService(Instrumenter.class)
+public class Log4jEventInstrumentation extends Instrumenter.Default {
+ public Log4jEventInstrumentation() {
+ super("log4j-events");
+ }
+
+ @Override
+ public ElementMatcher<? super TypeDescription> typeMatcher() {
+ return named("org.apache.log4j.Category");
+ }
+
+ @Override
+ public String[] helperClassNames() {
+ return new String[] {packageName + ".Log4jEvents"};
+ }
+
+ @Override
+ public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
+ final Map<ElementMatcher<? super MethodDescription>, String> transformers = new HashMap<>();
+ transformers.put(
+ isMethod()
+ .and(isProtected())
+ .and(named("forcedLog"))
+ .and(takesArguments(4))
+ .and(takesArgument(0, named("java.lang.String")))
+ .and(takesArgument(1, named("org.apache.log4j.Priority")))
+ .and(takesArgument(2, named("java.lang.Object")))
+ .and(takesArgument(3, named("java.lang.Throwable"))),
+ Log4jEventInstrumentation.class.getName() + "$ForcedLogAdvice");
+ return transformers;
+ }
+
+ public static class ForcedLogAdvice {
+
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void methodEnter(
+ @Advice.This final Category logger,
+ @Advice.Argument(1) final Priority level,
+ @Advice.Argument(2) final Object message,
+ @Advice.Argument(3) final Throwable t) {
+ Log4jEvents.capture(logger, level, message, t);
+ }
+ }
+}

ADD
instrumentation/log4j-events-1.1/src/main/java/io/opentelemetry/auto/instrumentation/log4jevents/Log4jEvents.java
+package io.opentelemetry.auto.instrumentation.log4jevents;
+
+import io.opentelemetry.OpenTelemetry;
+import io.opentelemetry.auto.config.Config;
+import io.opentelemetry.trace.AttributeValue;
+import io.opentelemetry.trace.Span;
+import io.opentelemetry.trace.Tracer;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.Map;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.log4j.Category;
+import org.apache.log4j.Priority;
+
+@Slf4j
+public class Log4jEvents {
+
+ private static final Tracer TRACER =
+ OpenTelemetry.getTracerFactory().get("io.opentelemetry.auto.log4j-events-1.1");
+
+ // these constants are copied from org.apache.log4j.Priority and org.apache.log4j.Level because
+ // Level was only introduced in 1.2, and then Level.TRACE was only introduced in 1.2.12
+ private static final int OFF_INT = Integer.MAX_VALUE;
+ private static final int FATAL_INT = 50000;
+ private static final int ERROR_INT = 40000;
+ private static final int WARN_INT = 30000;
+ private static final int INFO_INT = 20000;
+ private static final int DEBUG_INT = 10000;
+ private static final int TRACE_INT = 5000;
+ private static final int ALL_INT = Integer.MIN_VALUE;
+
+ public static void capture(
+ final Category logger, final Priority level, final Object message, final Throwable t) {
+
+ if (level.toInt() < getThreshold()) {
+ return;
+ }
+ final Span currentSpan = TRACER.getCurrentSpan();
+ if (!currentSpan.getContext().isValid()) {
+ return;
+ }
+
+ final Map<String, AttributeValue> attributes = new HashMap<>(t == null ? 2 : 3);
+ attributes.put("level", newAttributeValue(level.toString()));
+ attributes.put("loggerName", newAttributeValue(logger.getName()));
+ if (t != null) {
+ attributes.put("error.stack", newAttributeValue(toString(t)));
+ }
+ currentSpan.addEvent(String.valueOf(message), attributes);
+ }
+
+ private static AttributeValue newAttributeValue(final String stringValue) {
+ return AttributeValue.stringAttributeValue(stringValue);
+ }
+
+ private static String toString(final Throwable t) {
+ final StringWriter out = new StringWriter();
+ t.printStackTrace(new PrintWriter(out));
+ return out.toString();
+ }
+
+ private static int getThreshold() {
+ final String level = Config.get().getLogsEventsThreshold();
+ if (level == null) {
+ return OFF_INT;
+ }
+ switch (level) {
+ case "OFF":
+ return OFF_INT;
+ case "FATAL":
+ return FATAL_INT;
+ case "ERROR":
+ case "SEVERE":
+ return ERROR_INT;
+ case "WARN":
+ case "WARNING":
+ return WARN_INT;
+ case "INFO":
+ return INFO_INT;
+ case "CONFIG":
+ case "DEBUG":
+ case "FINE":
+ case "FINER":
+ return DEBUG_INT;
+ case "TRACE":
+ case "FINEST":
+ return TRACE_INT;
+ case "ALL":
+ return ALL_INT;
+ default:
+ log.error("unexpected value for {}: {}", Config.LOGS_EVENTS_THRESHOLD, level);
+ return OFF_INT;
+ }
+ }
+}

ADD
instrumentation/log4j-events-1.1/src/test/groovy/Log4jEventTest.groovy
+import io.opentelemetry.auto.test.log.events.LogEventsTestBase
+import org.apache.log4j.Logger
+
+class Log4jEventTest extends LogEventsTestBase {
+
+ @Override
+ Object createLogger(String name) {
+ Logger.getLogger(name)
+ }
+}

ADD
instrumentation/log4j-events-1.1/src/test/resources/log4j.properties
+log4j.rootLogger=WARN, CONSOLE
+log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
+log4j.appender.CONSOLE.layout=org.apache.log4j.SimpleLayout

MODIFY
settings.gradle
settings.gradle
@@ -99,6 +99,7 @@ include ':instrumentation:jsp-2.3'
include ':instrumentation:kafka-clients-0.11'
include ':instrumentation:kafka-streams-0.11'
include ':instrumentation:lettuce-5.0'
+include ':instrumentation:log4j-events-1.1'
// FIXME this instrumentation relied on scope listener
// include ':instrumentation:log4j1'
// FIXME this instrumentation relied on scope listener

Capture log4j 1.x events (#181)