-
Notifications
You must be signed in to change notification settings - Fork 872
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Prototype Log4j2 Appender #4375
Changes from all commits
275f467
c99b249
7111f9a
ecd382d
45210d2
18a433a
819b806
e0f65f6
d3d512b
79cb6d9
db82669
d730612
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.log4j.v2_13_2; | ||
|
||
import io.opentelemetry.api.common.AttributeKey; | ||
import io.opentelemetry.api.common.Attributes; | ||
import io.opentelemetry.api.common.AttributesBuilder; | ||
import io.opentelemetry.context.Context; | ||
import io.opentelemetry.sdk.logs.LogBuilder; | ||
import io.opentelemetry.sdk.logs.data.Severity; | ||
import java.util.concurrent.TimeUnit; | ||
import org.apache.logging.log4j.Level; | ||
import org.apache.logging.log4j.core.LogEvent; | ||
import org.apache.logging.log4j.core.time.Instant; | ||
import org.apache.logging.log4j.message.Message; | ||
|
||
final class LogEventMapper { | ||
|
||
// Visible for testing | ||
static final AttributeKey<String> ATTR_THROWABLE_MESSAGE = | ||
AttributeKey.stringKey("throwable.message"); | ||
|
||
/** | ||
* Map the {@link LogEvent} data model onto the {@link LogBuilder}. Unmapped fields include: | ||
* | ||
* <ul> | ||
* <li>Fully qualified class name - {@link LogEvent#getLoggerFqcn()} | ||
* <li>Thread name - {@link LogEvent#getThreadName()} | ||
* <li>Thread id - {@link LogEvent#getThreadId()} | ||
* <li>Thread priority - {@link LogEvent#getThreadPriority()} | ||
* <li>Thread priority - {@link LogEvent#getThreadPriority()} | ||
* <li>Thrown details (stack trace, class name) - {@link LogEvent#getThrown()} | ||
* <li>Marker - {@link LogEvent#getMarker()} | ||
* <li>Nested diagnostic context - {@link LogEvent#getContextStack()} | ||
* <li>Mapped diagnostic context - {@link LogEvent#getContextData()} | ||
* </ul> | ||
*/ | ||
static void mapLogEvent(LogBuilder builder, LogEvent logEvent) { | ||
// TODO: map the LogEvent more completely when semantic conventions allow it | ||
AttributesBuilder attributes = Attributes.builder(); | ||
|
||
// message | ||
Message message = logEvent.getMessage(); | ||
if (message != null) { | ||
builder.setBody(message.getFormattedMessage()); | ||
} | ||
|
||
// time | ||
Instant instant = logEvent.getInstant(); | ||
if (instant != null) { | ||
builder.setEpoch( | ||
TimeUnit.MILLISECONDS.toNanos(instant.getEpochMillisecond()) | ||
+ instant.getNanoOfMillisecond(), | ||
TimeUnit.NANOSECONDS); | ||
} | ||
|
||
// level | ||
Level level = logEvent.getLevel(); | ||
if (level != null) { | ||
builder.setSeverity(levelToSeverity(level)); | ||
builder.setSeverityText(logEvent.getLevel().name()); | ||
} | ||
|
||
// throwable | ||
Throwable throwable = logEvent.getThrown(); | ||
if (throwable != null) { | ||
attributes.put(ATTR_THROWABLE_MESSAGE, throwable.getMessage()); | ||
} | ||
|
||
// span context | ||
builder.setContext(Context.current()); | ||
|
||
builder.setAttributes(attributes.build()); | ||
} | ||
|
||
private static Severity levelToSeverity(Level level) { | ||
switch (level.getStandardLevel()) { | ||
case ALL: | ||
return Severity.TRACE; | ||
case TRACE: | ||
return Severity.TRACE2; | ||
case DEBUG: | ||
return Severity.DEBUG; | ||
case INFO: | ||
return Severity.INFO; | ||
case WARN: | ||
return Severity.WARN; | ||
case ERROR: | ||
return Severity.ERROR; | ||
case FATAL: | ||
return Severity.FATAL; | ||
case OFF: | ||
return Severity.UNDEFINED_SEVERITY_NUMBER; | ||
} | ||
return Severity.UNDEFINED_SEVERITY_NUMBER; | ||
} | ||
|
||
private LogEventMapper() {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.log4j.v2_13_2; | ||
|
||
import io.opentelemetry.sdk.logs.LogBuilder; | ||
import io.opentelemetry.sdk.logs.SdkLogEmitterProvider; | ||
import java.io.Serializable; | ||
import java.util.concurrent.atomic.AtomicReference; | ||
import org.apache.logging.log4j.core.Appender; | ||
import org.apache.logging.log4j.core.Core; | ||
import org.apache.logging.log4j.core.Filter; | ||
import org.apache.logging.log4j.core.Layout; | ||
import org.apache.logging.log4j.core.LogEvent; | ||
import org.apache.logging.log4j.core.appender.AbstractAppender; | ||
import org.apache.logging.log4j.core.config.Property; | ||
import org.apache.logging.log4j.core.config.plugins.Plugin; | ||
import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; | ||
|
||
@Plugin( | ||
name = OpenTelemetryAppender.PLUGIN_NAME, | ||
category = Core.CATEGORY_NAME, | ||
elementType = Appender.ELEMENT_TYPE) | ||
public class OpenTelemetryAppender extends AbstractAppender { | ||
|
||
static final String PLUGIN_NAME = "OpenTelemetry"; | ||
|
||
@PluginBuilderFactory | ||
public static <B extends Builder<B>> B builder() { | ||
return new Builder<B>().asBuilder(); | ||
} | ||
|
||
static class Builder<B extends Builder<B>> extends AbstractAppender.Builder<B> | ||
implements org.apache.logging.log4j.core.util.Builder<OpenTelemetryAppender> { | ||
|
||
@Override | ||
public OpenTelemetryAppender build() { | ||
OpenTelemetryAppender appender = | ||
new OpenTelemetryAppender( | ||
getName(), getLayout(), getFilter(), isIgnoreExceptions(), getPropertyArray()); | ||
OpenTelemetryLog4j.registerInstance(appender); | ||
return appender; | ||
} | ||
} | ||
|
||
private final AtomicReference<SdkLogEmitterProvider> sdkLogEmitterProviderRef = | ||
new AtomicReference<>(); | ||
|
||
private OpenTelemetryAppender( | ||
String name, | ||
Layout<? extends Serializable> layout, | ||
Filter filter, | ||
boolean ignoreExceptions, | ||
Property[] properties) { | ||
super(name, filter, layout, ignoreExceptions, properties); | ||
} | ||
|
||
@Override | ||
public void append(LogEvent event) { | ||
SdkLogEmitterProvider logEmitterProvider = sdkLogEmitterProviderRef.get(); | ||
if (logEmitterProvider == null) { | ||
// appender hasn't been initialized | ||
return; | ||
} | ||
LogBuilder builder = | ||
logEmitterProvider.logEmitterBuilder(event.getLoggerName()).build().logBuilder(); | ||
LogEventMapper.mapLogEvent(builder, event); | ||
builder.emit(); | ||
} | ||
|
||
void initialize(SdkLogEmitterProvider sdkLogEmitterProvider) { | ||
if (!sdkLogEmitterProviderRef.compareAndSet(null, sdkLogEmitterProvider)) { | ||
throw new IllegalStateException("OpenTelemetryAppender has already been initialized."); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.log4j.v2_13_2; | ||
|
||
import io.opentelemetry.sdk.logs.SdkLogEmitterProvider; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import javax.annotation.Nullable; | ||
import javax.annotation.concurrent.GuardedBy; | ||
|
||
public final class OpenTelemetryLog4j { | ||
|
||
private static final Object lock = new Object(); | ||
|
||
@GuardedBy("lock") | ||
private static SdkLogEmitterProvider sdkLogEmitterProvider; | ||
|
||
@GuardedBy("lock") | ||
@Nullable | ||
private static Throwable initializeCaller; | ||
|
||
@GuardedBy("lock") | ||
private static final List<OpenTelemetryAppender> APPENDERS = new ArrayList<>(); | ||
|
||
public static void initialize(SdkLogEmitterProvider sdkLogEmitterProvider) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How is this going to work as a javaagent instrumentation? In the agent we keep all the SDK classes hidden in the agent classloader, they're basically unavailable from the application classloader. The agent won't be able to register its emitterProvider in the application's There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll need it - open-telemetry/opentelemetry-java#3807 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Took a stab at extracting out the appender API here. |
||
List<OpenTelemetryAppender> instances; | ||
synchronized (lock) { | ||
if (OpenTelemetryLog4j.sdkLogEmitterProvider != null) { | ||
throw new IllegalStateException( | ||
"OpenTelemetryLog4j.initialize has already been called. OpenTelemetryLog4j.initialize " | ||
+ "must be called only once. Previous invocation set to cause of this exception.", | ||
initializeCaller); | ||
} | ||
OpenTelemetryLog4j.sdkLogEmitterProvider = sdkLogEmitterProvider; | ||
instances = new ArrayList<>(APPENDERS); | ||
initializeCaller = new Throwable(); | ||
} | ||
for (OpenTelemetryAppender instance : instances) { | ||
instance.initialize(sdkLogEmitterProvider); | ||
} | ||
} | ||
|
||
static void registerInstance(OpenTelemetryAppender appender) { | ||
synchronized (lock) { | ||
if (sdkLogEmitterProvider != null) { | ||
appender.initialize(sdkLogEmitterProvider); | ||
} | ||
APPENDERS.add(appender); | ||
} | ||
} | ||
|
||
// Visible for testing | ||
static void resetForTest() { | ||
synchronized (lock) { | ||
sdkLogEmitterProvider = null; | ||
APPENDERS.clear(); | ||
} | ||
} | ||
|
||
private OpenTelemetryLog4j() {} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And shouldn't this text come from our severity and not logging framework level?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the spec, the severity text field "is the original string representation of the severity as it is known at the source".
But this does bring up the point that I have made a number of subjective decisions in mapping between the Log4j2 LogEvent data model and the otel log data model. Please take a close look at the attribute name's I've chosen for various things and let me know if you agree / disagree.