Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
plugins {
id("otel.javaagent-bootstrap")
}

dependencies {
compileOnly(project(":javaagent-bootstrap"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.bootstrap.logging;

import io.opentelemetry.javaagent.Slf4jLogRecorder;
import io.opentelemetry.javaagent.bootstrap.Slf4jBridgeLogRecorderHolder;

public class Slf4jBridgeInstaller {
private Slf4jBridgeInstaller() {}

public static void installSlf4jLogger(Slf4jLogRecorder slf4JLogRecorder) {
Slf4jBridgeLogRecorderHolder.initialize(slf4JLogRecorder);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.bootstrap.logging;

import java.util.concurrent.atomic.AtomicBoolean;

public final class Slf4jBridgeInstallerFlags {

private Slf4jBridgeInstallerFlags() {}

public static final AtomicBoolean IS_INSTALLED = new AtomicBoolean(false);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
plugins {
id("otel.javaagent-instrumentation")
}

muzzle {
pass {
group.set("org.slf4j")
module.set("slf4j-api")
versions.set("[2.0.0,)")
assertInverse.set(true)
}
}

val latestDepTest = findProperty("testLatestDeps") as Boolean
dependencies {
bootstrap(project(":instrumentation:internal:internal-slf4j-bridge:bootstrap"))

compileOnly(project(":javaagent-bootstrap"))

compileOnly("org.slf4j:slf4j-api") {
version {
// 2.0.0 introduced fluent API
strictly("2.0.0")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.internal.slf4jbridge;

import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;

import io.opentelemetry.javaagent.bootstrap.logging.Slf4jBridgeInstallerFlags;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

public class LoggerFactoryInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("org.slf4j.LoggerFactory");
}

@Override
public void transform(TypeTransformer transformer) {
// once a call to getILoggerFactory() exits we can be certain that slf4j is properly initialized
transformer.applyAdviceToMethod(
named("getILoggerFactory").and(takesArguments(0)),
this.getClass().getName() + "$GetLoggerFactoryAdvice");
}

@SuppressWarnings("unused")
public static class GetLoggerFactoryAdvice {

@Advice.OnMethodExit(suppress = Throwable.class)
public static void onExit() {
if (Slf4jBridgeInstallerFlags.IS_INSTALLED.compareAndSet(false, true)) {
Slf4jLogRecorderImpl.install();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's happening here:

  • This instrumentation has access to the slf4j-api from the application classpath
  • The OpenTelemetryInstaller from javaagent-tooling needs to register a LogRecordProcessor which calls routs to the application classpath slf4j-api
  • So we establish a common minimal interface Slf4jLogRecorder in javaagent-bootstrap which can be accessed from both the instrumentation and the OpenTelemetryInstaller
  • Each time the LogRecordProcessor#onEmit is called, it calls Slf4jLogRecorderHolder.get() to get a reference to a Slf4jLogRecorder and record the log.
  • This Slf4jLogRecorder instance starts as a noop, but this instrumentation here detects the first usage of slf4j-api in the application, and installs calls Slf4jLogRecordHolder.initialize(Slf4jLogRecordHolder) with a proper instance connected to the slf4j-api in the application classpath.

Voila!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If / when we agree on the basic concepts here, I can polish this up and it ready to merge:

  • Better testing
  • Find a way to have one definition mapping from otel to SLF4J. Currently, I maintain two copies of the same code, one here and one in opentelemetry-java for the non agent case.
  • Figure out the configuration story. Currently, the LogRecordProcessor is always added. Should be configurable and need to think about the defaults.
  • And what about declarative config? Declarative config makes it easy to add this processor, but:
    1. We want to this to be easy / discoverable and the starter templates won't include this java specific processor.
    2. I think this is too important to require adopting declarative config as a prerequisite.
  • Maybe the answer is to have a java agent / distribution option like:
    export OTEL_JAVAAGENT_SLF4J_BRIDGE=true
    
    or for delcarative config:
    distribution:
      javaaagent:
        slf4j_bridge_enabled: true
    

}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.internal.slf4jbridge;

import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesBuilder;
import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;

@AutoService(IgnoredTypesConfigurer.class)
public final class Slf4jBridgeIgnoredTypesConfigurer implements IgnoredTypesConfigurer {

@Override
public void configure(IgnoredTypesBuilder builder, ConfigProperties config) {
builder.allowClass("org.slf4j.LoggerFactory");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.internal.slf4jbridge;

import static java.util.Collections.singletonList;

import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import java.util.List;

@AutoService(InstrumentationModule.class)
public class Slf4jBridgeInstrumentationModule extends InstrumentationModule {

public Slf4jBridgeInstrumentationModule() {
super("internal-slf4j-bridge");
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new LoggerFactoryInstrumentation());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.internal.slf4jbridge;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.Value;
import io.opentelemetry.api.logs.LoggerProvider;
import io.opentelemetry.api.logs.Severity;
import io.opentelemetry.context.Context;
import io.opentelemetry.javaagent.Slf4jLogRecorder;
import io.opentelemetry.javaagent.bootstrap.CallDepth;
import io.opentelemetry.javaagent.bootstrap.logging.Slf4jBridgeInstaller;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
import org.slf4j.spi.LoggingEventBuilder;

public final class Slf4jLogRecorderImpl implements Slf4jLogRecorder {

private Slf4jLogRecorderImpl() {}

public static void install() {
Slf4jBridgeInstaller.installSlf4jLogger(new Slf4jLogRecorderImpl());
}

@Override
public void record(
Context context,
String scopeName,
@Nullable String eventName,
@Nullable Value<?> bodyValue,
Attributes attributes,
Severity severity) {
CallDepth callDepth = CallDepth.forClass(LoggerProvider.class);
try {
if (callDepth.getAndIncrement() > 0) {
return;
}
recordToSlf4j(scopeName, eventName, bodyValue, attributes, severity);
} finally {
callDepth.decrementAndGet();
}
}

@SuppressWarnings("CheckReturnValue")
public static void recordToSlf4j(
String scopeName,
@Nullable String eventName,
@Nullable Value<?> bodyValue,
Attributes attributes,
Severity severity) {
Logger logger = LoggerFactory.getLogger(scopeName);
Level level = toSlf4jLevel(severity);
if (!logger.isEnabledForLevel(level)) {
return;
}
LoggingEventBuilder builder = logger.atLevel(level);
if (bodyValue != null) {
builder.setMessage(bodyValue.asString());
}
attributes.forEach((key, value) -> builder.addKeyValue(key.getKey(), value));

// append event_name last to take priority over attributes
if (eventName != null) {
builder.addKeyValue("event_name", eventName);
}
builder.log();
}

private static Level toSlf4jLevel(Severity severity) {
switch (severity) {
case TRACE:
case TRACE2:
case TRACE3:
case TRACE4:
return Level.TRACE;
case DEBUG:
case DEBUG2:
case DEBUG3:
case DEBUG4:
return Level.DEBUG;
case INFO:
case INFO2:
case INFO3:
case INFO4:
return Level.INFO;
case WARN:
case WARN2:
case WARN3:
case WARN4:
return Level.WARN;
case ERROR:
case ERROR2:
case ERROR3:
case ERROR4:
case FATAL:
case FATAL2:
case FATAL3:
case FATAL4:
return Level.ERROR;
case UNDEFINED_SEVERITY_NUMBER:
return Level.INFO;
}
throw new IllegalArgumentException("Unknown severity: " + severity);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
classification: internal
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.logs.LogRecordBuilder;
import io.opentelemetry.api.logs.Loopback;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanContext;
import io.opentelemetry.api.trace.TraceFlags;
Expand Down Expand Up @@ -310,6 +311,12 @@ public void append(LogEvent event) {
}

private void emit(OpenTelemetry openTelemetry, LogEvent event) {
Context context = Context.current();
// This is an optimization. SDK will automatically filter these logs as well.
if (Loopback.isLoopback(context)) {
return;
}

String instrumentationName = event.getLoggerName();
if (instrumentationName == null || instrumentationName.isEmpty()) {
instrumentationName = "ROOT";
Expand All @@ -318,7 +325,6 @@ private void emit(OpenTelemetry openTelemetry, LogEvent event) {
LogRecordBuilder builder =
openTelemetry.getLogsBridge().loggerBuilder(instrumentationName).build().logRecordBuilder();
ReadOnlyStringMap contextData = event.getContextData();
Context context = Context.current();
// when using async logger we'll be executing on a different thread than what started logging
// reconstruct the context from context data
if (context == Context.root()) {
Expand All @@ -339,6 +345,7 @@ private void emit(OpenTelemetry openTelemetry, LogEvent event) {
TraceState.getDefault())));
}
}
context = Loopback.withLoopback(context);

mapper.mapLogEvent(
builder,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io.opentelemetry.api.incubator.logs.ExtendedLogRecordBuilder;
import io.opentelemetry.api.logs.LogRecordBuilder;
import io.opentelemetry.api.logs.LoggerProvider;
import io.opentelemetry.api.logs.Loopback;
import io.opentelemetry.api.logs.Severity;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.internal.SemconvStability;
Expand Down Expand Up @@ -111,19 +112,26 @@ public static Builder builder() {
}

public void emit(LoggerProvider loggerProvider, ILoggingEvent event, long threadId) {
Context context = Context.current();
// This is an optimization. SDK will automatically filter these logs as well.
if (Loopback.isLoopback(context)) {
return;
}
context = Loopback.withLoopback(context);

String instrumentationName = event.getLoggerName();
if (instrumentationName == null || instrumentationName.isEmpty()) {
instrumentationName = "ROOT";
}
LogRecordBuilder builder =
loggerProvider.loggerBuilder(instrumentationName).build().logRecordBuilder();
mapLoggingEvent(builder, event, threadId);
mapLoggingEvent(builder, event, threadId, context);
builder.emit();
}

/** Map the {@link ILoggingEvent} data model onto the {@link LogRecordBuilder}. */
private void mapLoggingEvent(
LogRecordBuilder builder, ILoggingEvent loggingEvent, long threadId) {
LogRecordBuilder builder, ILoggingEvent loggingEvent, long threadId, Context context) {
// message
String message = loggingEvent.getFormattedMessage();
if (message != null) {
Expand Down Expand Up @@ -233,7 +241,7 @@ private void mapLoggingEvent(
captureLogstashMarkerAttributes(builder, loggingEvent);
}
// span context
builder.setContext(Context.current());
builder.setContext(context);
}

// getInstant is available since Logback 1.3
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.Value;
import io.opentelemetry.api.logs.Severity;
import io.opentelemetry.context.Context;
import javax.annotation.Nullable;

@FunctionalInterface
public interface Slf4jLogRecorder {

void record(
Context context,
String scopeName,
@Nullable String eventName,
@Nullable Value<?> body,
Attributes attributes,
Severity severity);
}
Loading
Loading