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 + + + + + + + + + + + +