Skip to content
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 #4374

Closed
Closed
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
2 changes: 2 additions & 0 deletions instrumentation/log4j/log4j-2-testing/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ plugins {
dependencies {
api(project(":testing-common"))

api("io.opentelemetry:opentelemetry-sdk-extension-logging")

api("org.apache.logging.log4j:log4j-core:2.7")

implementation("com.google.guava:guava")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import io.opentelemetry.instrumentation.test.InstrumentationSpecification
import org.apache.logging.log4j.LogManager

abstract class Log4j2Test extends InstrumentationSpecification {
def cleanup() {
def setup() {
ListAppender.get().clearEvents()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" packages="com.example.appender">
<Configuration status="WARN" packages="com.example.appender,io.opentelemetry.instrumentation.log4j.v2_13_2">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} traceId: %X{trace_id} spanId: %X{span_id} flags: %X{trace_flags} - %msg%n" />
</Console>
<ListAppender name="ListAppender" />
<OpenTelemetry name="OpenTelemetryAppender" />
</Appenders>
<Loggers>
<Logger name="TestLogger" level="All">
<AppenderRef ref="OpenTelemetryAppender" leve="All" />
<AppenderRef ref="ListAppender" level="All" />
<AppenderRef ref="Console" level="All" />
</Logger>
Expand Down
2 changes: 2 additions & 0 deletions instrumentation/log4j/log4j-2.13.2/library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ plugins {
}

dependencies {
api("io.opentelemetry:opentelemetry-sdk-extension-logging")

library("org.apache.logging.log4j:log4j-core:2.13.2")

// Library instrumentation cannot be applied to 2.13.2 due to a bug in Log4J. The agent works
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.log4j.v2_13_2;

import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.logging.data.LogRecord;
import io.opentelemetry.sdk.logging.export.LogExporter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;

public final class InMemoryLogExporter implements LogExporter {

// using LinkedBlockingQueue to avoid manual locks for thread-safe operations
private final Queue<LogRecord> finishedLogItems = new LinkedBlockingQueue<>();
private boolean isStopped = false;

private InMemoryLogExporter() {}

/**
* Returns a new instance of the {@code InMemoryLogExporter}.
*
* @return a new instance of the {@code InMemoryLogExporter}.
*/
public static InMemoryLogExporter create() {
return new InMemoryLogExporter();
}

/**
* Returns a {@code List} of the finished {@code Log}s, represented by {@code LogRecord}.
*
* @return a {@code List} of the finished {@code Log}s.
*/
public List<LogRecord> getFinishedLogItems() {
return Collections.unmodifiableList(new ArrayList<>(finishedLogItems));
}

/**
* Clears the internal {@code List} of finished {@code Log}s.
*
* <p>Does not reset the state of this exporter if already shutdown.
*/
public void reset() {
finishedLogItems.clear();
}

/**
* Exports the collection of {@code Log}s into the inmemory queue.
*
* <p>If this is called after {@code shutdown}, this will return {@code ResultCode.FAILURE}.
*/
@Override
public CompletableResultCode export(Collection<LogRecord> logs) {
if (isStopped) {
return CompletableResultCode.ofFailure();
}
finishedLogItems.addAll(logs);
return CompletableResultCode.ofSuccess();
}

/**
* Clears the internal {@code List} of finished {@code Log}s.
*
* <p>Any subsequent call to export() function on this LogExporter, will return {@code
* CompletableResultCode.ofFailure()}
*/
@Override
public CompletableResultCode shutdown() {
isStopped = true;
finishedLogItems.clear();
return CompletableResultCode.ofSuccess();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.log4j.v2_13_2;

import static io.opentelemetry.instrumentation.api.log.LoggingContextConstants.SPAN_ID;
import static io.opentelemetry.instrumentation.api.log.LoggingContextConstants.TRACE_ID;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.sdk.common.InstrumentationLibraryInfo;
import io.opentelemetry.sdk.logging.data.Body;
import io.opentelemetry.sdk.logging.data.LogRecord;
import io.opentelemetry.sdk.logging.data.LogRecord.Severity;
import io.opentelemetry.sdk.logging.data.LogRecordBuilder;
import io.opentelemetry.sdk.resources.Resource;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.LogEvent;

final class LogEventMapper {

private static final Map<Level, Severity> LEVEL_SEVERITY_MAP;

static {
Map<Level, Severity> levelSeverityMap = new HashMap<>();
levelSeverityMap.put(Level.ALL, Severity.TRACE);
levelSeverityMap.put(Level.TRACE, Severity.TRACE2);
levelSeverityMap.put(Level.DEBUG, Severity.DEBUG);
levelSeverityMap.put(Level.INFO, Severity.INFO);
levelSeverityMap.put(Level.WARN, Severity.WARN);
levelSeverityMap.put(Level.ERROR, Severity.ERROR);
levelSeverityMap.put(Level.FATAL, Severity.FATAL);
LEVEL_SEVERITY_MAP = Collections.unmodifiableMap(levelSeverityMap);
}

static LogRecord toLogRecord(
LogEvent logEvent, Resource resource, InstrumentationLibraryInfo instrumentationLibraryInfo) {
LogRecordBuilder builder =
LogRecord.builder(resource, instrumentationLibraryInfo)
.setBody(Body.stringBody(logEvent.getMessage().getFormattedMessage()))
.setSeverity(
LEVEL_SEVERITY_MAP.getOrDefault(
logEvent.getLevel(), Severity.UNDEFINED_SEVERITY_NUMBER))
.setSeverityText(logEvent.getLevel().name())
.setUnixTimeNano(logEvent.getNanoTime());

AttributesBuilder attributes = Attributes.builder();
attributes.put("logger.name", logEvent.getLoggerName());
attributes.put("thread.name", logEvent.getThreadName());

Map<String, String> contextMap = logEvent.getContextData().toMap();
if (!contextMap.isEmpty()) {
if (contextMap.containsKey(TRACE_ID)) {
builder.setTraceId(contextMap.remove(TRACE_ID));
}
if (contextMap.containsKey(SPAN_ID)) {
builder.setSpanId(contextMap.remove(SPAN_ID));
}
contextMap.forEach(attributes::put);
}

builder.setAttributes(attributes.build());

return builder.build();
}

private LogEventMapper() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.log4j.v2_13_2;

import io.opentelemetry.sdk.common.InstrumentationLibraryInfo;
import io.opentelemetry.sdk.logging.LogSink;
import io.opentelemetry.sdk.logging.data.LogRecord;
import io.opentelemetry.sdk.resources.Resource;
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 newBuilder() {
return new Builder<B>().asBuilder();
}

public 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<SinkAndResource> sinkAndResourceRef = new AtomicReference<>();
private final InstrumentationLibraryInfo instrumentationLibraryInfo;

private OpenTelemetryAppender(
String name,
Layout<? extends Serializable> layout,
Filter filter,
boolean ignoreExceptions,
Property[] properties) {
super(name, filter, layout, ignoreExceptions, properties);
instrumentationLibraryInfo =
InstrumentationLibraryInfo.create(OpenTelemetryAppender.class.getName(), null);
}

@Override
public void append(LogEvent event) {
SinkAndResource sinkAndResource = sinkAndResourceRef.get();
if (sinkAndResource == null) {
// appender hasn't been initialized
return;
}
LogRecord logRecord =
LogEventMapper.toLogRecord(event, sinkAndResource.resource, instrumentationLibraryInfo);
sinkAndResource.logSink.offer(logRecord);
}

void initialize(LogSink logSink, Resource resource) {
if (!sinkAndResourceRef.compareAndSet(null, new SinkAndResource(logSink, resource))) {
throw new IllegalStateException("OpenTelemetryAppender has already been initialized.");
}
}

private static class SinkAndResource {
private final LogSink logSink;
private final Resource resource;

private SinkAndResource(LogSink logSink, Resource resource) {
this.logSink = logSink;
this.resource = resource;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.log4j.v2_13_2;

import io.opentelemetry.sdk.logging.LogSink;
import io.opentelemetry.sdk.resources.Resource;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.concurrent.GuardedBy;

public final class OpenTelemetryLog4j {

private static final Object LOCK = new Object();

@GuardedBy("LOCK")
private static LogSink logSink;

@GuardedBy("LOCK")
private static Resource resource;

@GuardedBy("LOCK")
private static final List<OpenTelemetryAppender> APPENDERS = new ArrayList<>();

public static void initialize(LogSink logSink, Resource resource) {
List<OpenTelemetryAppender> instances;
synchronized (LOCK) {
if (OpenTelemetryLog4j.logSink != null) {
throw new IllegalStateException("LogSinkSdkProvider has already been set.");
}
OpenTelemetryLog4j.logSink = logSink;
OpenTelemetryLog4j.resource = resource;
instances = new ArrayList<>(APPENDERS);
}
for (OpenTelemetryAppender instance : instances) {
instance.initialize(logSink, resource);
}
}

static void registerInstance(OpenTelemetryAppender appender) {
synchronized (LOCK) {
if (logSink != null) {
appender.initialize(logSink, resource);
}
APPENDERS.add(appender);
}
}

private OpenTelemetryLog4j() {}
}
Loading