Add logback appender (#4984)
This commit is contained in:
parent
1a9b37525c
commit
935cb88ff4
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.opentelemetry.instrumentation</groupId>
|
||||
<artifactId>opentelemetry-logback-appender-1.0</artifactId>
|
||||
<version>OPENTELEMETRY_VERSION</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!-- The SDK appender is required to configure the appender with the OpenTelemetry Log SDK -->
|
||||
<groupId>io.opentelemetry.instrumentation</groupId>
|
||||
<artifactId>opentelemetry-instrumentation-sdk-appender</artifactId>
|
||||
<version>OPENTELEMETRY_VERSION</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
**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
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
|
||||
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>
|
||||
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
|
||||
</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="OpenTelemetry" class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="console"/>
|
||||
<appender-ref ref="OpenTelemetry" />
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
```
|
||||
|
||||
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`.
|
|
@ -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<ILoggingEvent> {
|
||||
|
||||
public OpenTelemetryAppender() {}
|
||||
|
||||
@Override
|
||||
protected void append(ILoggingEvent event) {
|
||||
LoggingEventMapper.INSTANCE.capture(event);
|
||||
}
|
||||
}
|
|
@ -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<LogData> 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<LogData> 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<LogData> 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<LogData> 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");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
|
||||
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>
|
||||
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
|
||||
</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="OpenTelemetry" class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="console"/>
|
||||
<appender-ref ref="OpenTelemetry" />
|
||||
</root>
|
||||
|
||||
</configuration>
|
Loading…
Reference in New Issue