From 9a380b0f1971a43f7ac9ec5400f4ef131092949b Mon Sep 17 00:00:00 2001
From: jack-berg <34418638+jack-berg@users.noreply.github.com>
Date: Mon, 3 Jan 2022 22:42:28 -0600
Subject: [PATCH] Add logback appender (#4984)
---
bom-alpha/build.gradle.kts | 3 +-
dependencyManagement/build.gradle.kts | 1 +
.../logback-appender-1.0/library/README.md | 80 +++++++++
.../appender/v1_0/OpenTelemetryAppender.java | 20 +++
.../v1_0/OpenTelemetryAppenderConfigTest.java | 153 ++++++++++++++++++
.../src/test/resources/logback-test.xml | 19 +++
6 files changed, 275 insertions(+), 1 deletion(-)
create mode 100644 instrumentation/logback/logback-appender-1.0/library/README.md
create mode 100644 instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java
create mode 100644 instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppenderConfigTest.java
create mode 100644 instrumentation/logback/logback-appender-1.0/library/src/test/resources/logback-test.xml
diff --git a/bom-alpha/build.gradle.kts b/bom-alpha/build.gradle.kts
index 4f1030cb8d1d..61fc1c6008bf 100644
--- a/bom-alpha/build.gradle.kts
+++ b/bom-alpha/build.gradle.kts
@@ -13,10 +13,11 @@ javaPlatform {
}
val otelVersion: String by project
+val otelAlphaVersion: String by project
dependencies {
api(platform("io.opentelemetry:opentelemetry-bom:${otelVersion}"))
- api(platform("io.opentelemetry:opentelemetry-bom-alpha:${otelVersion}-alpha"))
+ api(platform("io.opentelemetry:opentelemetry-bom-alpha:${otelAlphaVersion}"))
}
dependencies {
diff --git a/dependencyManagement/build.gradle.kts b/dependencyManagement/build.gradle.kts
index bb5e9b711cb9..efe7207b7a91 100644
--- a/dependencyManagement/build.gradle.kts
+++ b/dependencyManagement/build.gradle.kts
@@ -14,6 +14,7 @@ rootProject.extra["versions"] = dependencyVersions
val otelVersion = "1.10.0-rc.1"
val otelAlphaVersion = "1.10.0-alpha-rc.1"
rootProject.extra["otelVersion"] = otelVersion
+rootProject.extra["otelAlphaVersion"] = otelAlphaVersion
// Need both BOM and -all
val groovyVersion = "3.0.9"
diff --git a/instrumentation/logback/logback-appender-1.0/library/README.md b/instrumentation/logback/logback-appender-1.0/library/README.md
new file mode 100644
index 000000000000..f452e31b1892
--- /dev/null
+++ b/instrumentation/logback/logback-appender-1.0/library/README.md
@@ -0,0 +1,80 @@
+# Logback Appender
+
+This module provides a Logback [appender](https://logback.qos.ch/manual/appenders.html) which
+forwards Logback log events to
+the [OpenTelemetry Log SDK](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk/logs)
+
+To use it, add the following modules to your application's classpath.
+
+Replace `OPENTELEMETRY_VERSION` with the latest
+stable [release](https://search.maven.org/search?q=g:io.opentelemetry.instrumentation).
+
+**Maven**
+
+```xml
+
+
+ io.opentelemetry.instrumentation
+ opentelemetry-logback-appender-1.0
+ OPENTELEMETRY_VERSION
+ runtime
+
+
+
+ io.opentelemetry.instrumentation
+ opentelemetry-instrumentation-sdk-appender
+ OPENTELEMETRY_VERSION
+
+
+```
+
+**Gradle**
+
+```kotlin
+dependencies {
+ runtimeOnly("io.opentelemetry.instrumentation:opentelemetry-logback-appender-1.0:OPENTELEMETRY_VERSION")
+ // The SDK appender is required to configure the appender with the OpenTelemetry Log SDK
+ implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-sdk-appender:OPENTELEMETRY_VERSION")
+}
+```
+
+The following demonstrates how you might configure the appender in your `logback.xml` configuration:
+
+```xml
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Next, associate the `OpenTelemetry` configured via `logback.xml` with a `SdkLogEmitterProvider` in
+your application:
+
+```
+SdkLogEmitterProvider logEmitterProvider =
+ SdkLogEmitterProvider.builder()
+ .setResource(Resource.create(...))
+ .addLogProcessor(...)
+ .build();
+GlobalLogEmitterProvider.set(DelegatingLogEmitterProvider.from(logEmitterProvider));
+```
+
+In this example Logback log events will be sent to both the console appender and
+the `OpenTelemetryAppender`, which will drop the logs until `GlobalLogEmitterProvider.set(..)` is
+called. Once initialized, logs will be emitted to a `LogEmitter` obtained from
+the `SdkLogEmitterProvider`.
diff --git a/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java b/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java
new file mode 100644
index 000000000000..dbd7fe9dbb01
--- /dev/null
+++ b/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.logback.appender.v1_0;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.AppenderBase;
+import io.opentelemetry.instrumentation.logback.appender.v1_0.internal.LoggingEventMapper;
+
+public class OpenTelemetryAppender extends AppenderBase {
+
+ public OpenTelemetryAppender() {}
+
+ @Override
+ protected void append(ILoggingEvent event) {
+ LoggingEventMapper.INSTANCE.capture(event);
+ }
+}
diff --git a/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppenderConfigTest.java b/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppenderConfigTest.java
new file mode 100644
index 000000000000..b0c954641769
--- /dev/null
+++ b/instrumentation/logback/logback-appender-1.0/library/src/test/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppenderConfigTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.logback.appender.v1_0;
+
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanContext;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.instrumentation.api.appender.GlobalLogEmitterProvider;
+import io.opentelemetry.instrumentation.sdk.appender.DelegatingLogEmitterProvider;
+import io.opentelemetry.sdk.common.InstrumentationLibraryInfo;
+import io.opentelemetry.sdk.logs.SdkLogEmitterProvider;
+import io.opentelemetry.sdk.logs.data.LogData;
+import io.opentelemetry.sdk.logs.data.Severity;
+import io.opentelemetry.sdk.logs.export.InMemoryLogExporter;
+import io.opentelemetry.sdk.logs.export.SimpleLogProcessor;
+import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.sdk.trace.SdkTracerProvider;
+import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
+import java.time.Instant;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+
+class OpenTelemetryAppenderConfigTest {
+
+ private static final Logger logger = LoggerFactory.getLogger("TestLogger");
+
+ private static InMemoryLogExporter logExporter;
+ private static Resource resource;
+ private static InstrumentationLibraryInfo instrumentationLibraryInfo;
+
+ @BeforeAll
+ static void setupAll() {
+ logExporter = InMemoryLogExporter.create();
+ resource = Resource.getDefault();
+ instrumentationLibraryInfo = InstrumentationLibraryInfo.create("TestLogger", null);
+
+ SdkLogEmitterProvider logEmitterProvider =
+ SdkLogEmitterProvider.builder()
+ .setResource(resource)
+ .addLogProcessor(SimpleLogProcessor.create(logExporter))
+ .build();
+
+ GlobalLogEmitterProvider.resetForTest();
+ GlobalLogEmitterProvider.set(DelegatingLogEmitterProvider.from(logEmitterProvider));
+ }
+
+ @BeforeEach
+ void setup() {
+ logExporter.reset();
+ }
+
+ @Test
+ void logNoSpan() {
+ logger.info("log message 1");
+
+ List logDataList = logExporter.getFinishedLogItems();
+ assertThat(logDataList).hasSize(1);
+ LogData logData = logDataList.get(0);
+ assertThat(logData.getResource()).isEqualTo(resource);
+ assertThat(logData.getInstrumentationLibraryInfo()).isEqualTo(instrumentationLibraryInfo);
+ assertThat(logData.getBody().asString()).isEqualTo("log message 1");
+ assertThat(logData.getAttributes()).isEqualTo(Attributes.empty());
+ }
+
+ @Test
+ void logWithSpan() {
+ Span span1 = runWithSpan("span1", () -> logger.info("log message 1"));
+
+ logger.info("log message 2");
+
+ Span span2 = runWithSpan("span2", () -> logger.info("log message 3"));
+
+ List logDataList = logExporter.getFinishedLogItems();
+ assertThat(logDataList).hasSize(3);
+ assertThat(logDataList.get(0).getSpanContext()).isEqualTo(span1.getSpanContext());
+ assertThat(logDataList.get(1).getSpanContext()).isEqualTo(SpanContext.getInvalid());
+ assertThat(logDataList.get(2).getSpanContext()).isEqualTo(span2.getSpanContext());
+ }
+
+ private static Span runWithSpan(String spanName, Runnable runnable) {
+ Span span = SdkTracerProvider.builder().build().get("tracer").spanBuilder(spanName).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ runnable.run();
+ } finally {
+ span.end();
+ }
+ return span;
+ }
+
+ @Test
+ void logWithExtras() {
+ Instant start = Instant.now();
+ logger.info("log message 1", new IllegalStateException("Error!"));
+
+ List logDataList = logExporter.getFinishedLogItems();
+ assertThat(logDataList).hasSize(1);
+ LogData logData = logDataList.get(0);
+ assertThat(logData.getResource()).isEqualTo(resource);
+ assertThat(logData.getInstrumentationLibraryInfo()).isEqualTo(instrumentationLibraryInfo);
+ assertThat(logData.getBody().asString()).isEqualTo("log message 1");
+ assertThat(logData.getEpochNanos())
+ .isGreaterThan(TimeUnit.MILLISECONDS.toNanos(start.toEpochMilli()))
+ .isLessThan(TimeUnit.MILLISECONDS.toNanos(Instant.now().toEpochMilli()));
+ assertThat(logData.getSeverity()).isEqualTo(Severity.INFO);
+ assertThat(logData.getSeverityText()).isEqualTo("INFO");
+ assertThat(logData.getAttributes().size()).isEqualTo(3);
+ assertThat(logData.getAttributes().get(SemanticAttributes.EXCEPTION_TYPE))
+ .isEqualTo(IllegalStateException.class.getName());
+ assertThat(logData.getAttributes().get(SemanticAttributes.EXCEPTION_MESSAGE))
+ .isEqualTo("Error!");
+ assertThat(logData.getAttributes().get(SemanticAttributes.EXCEPTION_STACKTRACE))
+ .contains("logWithExtras");
+ }
+
+ @Test
+ void logContextData() {
+ MDC.put("key1", "val1");
+ MDC.put("key2", "val2");
+ try {
+ logger.info("log message 1");
+ } finally {
+ MDC.clear();
+ }
+
+ List logDataList = logExporter.getFinishedLogItems();
+ assertThat(logDataList).hasSize(1);
+ LogData logData = logDataList.get(0);
+ assertThat(logData.getResource()).isEqualTo(resource);
+ assertThat(logData.getInstrumentationLibraryInfo()).isEqualTo(instrumentationLibraryInfo);
+ assertThat(logData.getBody().asString()).isEqualTo("log message 1");
+ assertThat(logData.getAttributes().size()).isEqualTo(2);
+ AssertionsForClassTypes.assertThat(
+ logData.getAttributes().get(AttributeKey.stringKey("logback.mdc.key1")))
+ .isEqualTo("val1");
+ AssertionsForClassTypes.assertThat(
+ logData.getAttributes().get(AttributeKey.stringKey("logback.mdc.key2")))
+ .isEqualTo("val2");
+ }
+}
diff --git a/instrumentation/logback/logback-appender-1.0/library/src/test/resources/logback-test.xml b/instrumentation/logback/logback-appender-1.0/library/src/test/resources/logback-test.xml
new file mode 100644
index 000000000000..1255f7cfe3ec
--- /dev/null
+++ b/instrumentation/logback/logback-appender-1.0/library/src/test/resources/logback-test.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+