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
26 changes: 26 additions & 0 deletions api/all/src/main/java/io/opentelemetry/api/logs/Loopback.java
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.api.logs;

import io.opentelemetry.context.Context;
import io.opentelemetry.context.ContextKey;

public final class Loopback {

private Loopback() {}

public static final ContextKey<Boolean> loopbackContextKey =
ContextKey.named("otel.loopback");

public static Context withLoopback(Context context) {
return context.with(loopbackContextKey, true);
}

public static boolean isLoopback(Context context) {
Boolean loopback = context.get(loopbackContextKey);
return loopback != null && loopback;
}
}
3 changes: 3 additions & 0 deletions sdk-extensions/incubator/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ dependencies {
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
implementation(project(":sdk-extensions:autoconfigure"))

// io.opentelemetry.sdk.extension.incubator.slf4j
implementation("org.slf4j:slf4j-api:2.0.17")

testImplementation(project(":sdk:testing"))
testImplementation(project(":sdk-extensions:autoconfigure"))
testImplementation(project(":exporters:logging"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.sdk.extension.incubator.slf4j;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.Value;
import io.opentelemetry.api.logs.Loopback;
import io.opentelemetry.api.logs.Severity;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.sdk.logs.LogRecordProcessor;
import io.opentelemetry.sdk.logs.ReadWriteLogRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
import org.slf4j.spi.LoggingEventBuilder;
import javax.annotation.Nullable;

public final class Slf4jBridgeProcessor implements LogRecordProcessor {
private Slf4jBridgeProcessor() {}

public static Slf4jBridgeProcessor create() {
return new Slf4jBridgeProcessor();
}

@Override
public void onEmit(Context context, ReadWriteLogRecord logRecord) {
if (Loopback.isLoopback(context)) {
Copy link
Member

Choose a reason for hiding this comment

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

We also have to check in instrumentation for this I suppose?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes exactly, for example: https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/15572/files#diff-19adc31cb9699cb43b2c7701d9838a060a4ae299710b1e0b382493cfd1cc382f

There's another version of this we can consider as well:

  • Update the internals of SdkLoggerProvider such every call to LogRecordProcessor#onEmit(Context, ReadWriteLogRecord) has Context w/ otel.loopback=true (implicit and explicit context)
  • Update the internals of SdkLoggerProvider such that calls to SdkLogRecordBuilder#emit() short circuit if otel.loopback=true

I went with the approach you see here because modifying the internals of the SDK is arguably spec territory and this approach allows solving without changing internals.

Also, even if SdkLogRecordBuilder#emit() is short circuiting if otel.loopback=true, you'd still want instrumentation to short circuit to avoid the unnecessary mapping work which involves memory allocation.

But it might be worth to do both things. Or to start with this type of solution which doesn't rely on modying SDK internal, and later evolve if something lands in the spec.

return;
}

try (Scope scope = Loopback.withLoopback(context).makeCurrent()) {
recordToSlf4j(
logRecord.getInstrumentationScopeInfo().getName(),
logRecord.getEventName(),
logRecord.getBodyValue(),
logRecord.getAttributes(),
logRecord.getSeverity());
}
}

@SuppressWarnings("CheckReturnValue")
private 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);
}
}
Loading