Add event API (#4781)

* Add event API

* Log when emitting event without domain, add javadoc example to Logger
This commit is contained in:
jack-berg 2022-09-27 17:39:33 -05:00 committed by GitHub
parent 29fe7caffe
commit 01a07b51a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 300 additions and 28 deletions

View File

@ -6,20 +6,37 @@
package io.opentelemetry.api.logs;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.internal.ValidationUtil;
import io.opentelemetry.context.Context;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
class DefaultLogger implements Logger {
private static final Logger INSTANCE = new DefaultLogger();
private static final Logger INSTANCE_WITH_DOMAIN = new DefaultLogger(/* hasDomain= */ true);
private static final Logger INSTANCE_NO_DOMAIN = new DefaultLogger(/* hasDomain= */ false);
private static final LogRecordBuilder NOOP_LOG_RECORD_BUILDER = new NoopLogRecordBuilder();
private static final EventBuilder NOOP_LOG_RECORD_BUILDER = new NoopLogRecordBuilder();
private DefaultLogger() {}
private final boolean hasDomain;
static Logger getInstance() {
return INSTANCE;
private DefaultLogger(boolean hasDomain) {
this.hasDomain = hasDomain;
}
static Logger getInstance(boolean hasDomain) {
return hasDomain ? INSTANCE_WITH_DOMAIN : INSTANCE_NO_DOMAIN;
}
@Override
public EventBuilder eventBuilder(String eventName) {
if (!hasDomain) {
ValidationUtil.log(
"Cannot emit event from Logger without event domain. Please use LoggerBuilder#setEventDomain(String) when obtaining Logger.",
Level.WARNING);
}
return NOOP_LOG_RECORD_BUILDER;
}
@Override
@ -27,7 +44,7 @@ class DefaultLogger implements Logger {
return NOOP_LOG_RECORD_BUILDER;
}
private static final class NoopLogRecordBuilder implements LogRecordBuilder {
private static final class NoopLogRecordBuilder implements EventBuilder {
private NoopLogRecordBuilder() {}

View File

@ -8,7 +8,10 @@ package io.opentelemetry.api.logs;
class DefaultLoggerProvider implements LoggerProvider {
private static final LoggerProvider INSTANCE = new DefaultLoggerProvider();
private static final LoggerBuilder NOOP_BUILDER = new NoopLoggerBuilder();
private static final LoggerBuilder NOOP_BUILDER_WITH_DOMAIN =
new NoopLoggerBuilder(/* hasDomain= */ true);
private static final LoggerBuilder NOOP_BUILDER_NO_DOMAIN =
new NoopLoggerBuilder(/* hasDomain= */ false);
private DefaultLoggerProvider() {}
@ -18,12 +21,22 @@ class DefaultLoggerProvider implements LoggerProvider {
@Override
public LoggerBuilder loggerBuilder(String instrumentationScopeName) {
return NOOP_BUILDER;
return NOOP_BUILDER_NO_DOMAIN;
}
private static class NoopLoggerBuilder implements LoggerBuilder {
private NoopLoggerBuilder() {}
private final boolean hasDomain;
private NoopLoggerBuilder(boolean hasDomain) {
this.hasDomain = hasDomain;
}
@Override
@SuppressWarnings("BuilderReturnThis")
public LoggerBuilder setEventDomain(String eventDomain) {
return eventDomain == null ? NOOP_BUILDER_NO_DOMAIN : NOOP_BUILDER_WITH_DOMAIN;
}
@Override
public LoggerBuilder setSchemaUrl(String schemaUrl) {
@ -37,7 +50,7 @@ class DefaultLoggerProvider implements LoggerProvider {
@Override
public Logger build() {
return DefaultLogger.getInstance();
return DefaultLogger.getInstance(hasDomain);
}
}
}

View File

@ -0,0 +1,16 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.api.logs;
/**
* Used to construct an emit events from a {@link Logger}.
*
* <p>An event is a log record with attributes for {@code event.domain} and {@code event.name}.
*
* <p>Obtain a {@link Logger#eventBuilder(String)}, add properties using the setters, and emit the
* log record by calling {@link #emit()}.
*/
public interface EventBuilder extends LogRecordBuilder {}

View File

@ -10,12 +10,47 @@ import javax.annotation.concurrent.ThreadSafe;
/**
* A {@link Logger} is the entry point into a log pipeline.
*
* <p>Obtain a {@link #logRecordBuilder()}, add properties using the setters, and emit it via {@link
* LogRecordBuilder#emit()}.
* <p>Obtain a {@link EventBuilder} or {@link #logRecordBuilder()}, add properties using the
* setters, and emit it via {@link LogRecordBuilder#emit()}.
*
* <p>Example usage emitting events:
*
* <pre>{@code
* class MyClass {
* private final Logger eventLogger = openTelemetryLoggerProvider.loggerBuilder("instrumentation-library-name")
* .setInstrumentationVersion("1.0.0")
* .setEventDomain("acme.observability")
* .build();
*
* void doWork() {
* eventLogger.eventBuilder("my-event")
* .setAllAttributes(Attributes.builder()
* .put("key1", "value1")
* .put("key2", "value2")
* .build())
* .emit();
* // do work
* }
* }
* }</pre>
*/
@ThreadSafe
public interface Logger {
/**
* Return a {@link EventBuilder} to emit an event.
*
* <p><b>NOTE:</b> this API MUST only be called on {@link Logger}s which have been assigned an
* {@link LoggerBuilder#setEventDomain(String) event domain}.
*
* <p>Build the event using the {@link EventBuilder} setters, and emit via {@link
* EventBuilder#emit()}.
*
* @param eventName the event name, which acts as a classifier for events. Within a particular
* event domain, event name defines a particular class or type of event.
*/
EventBuilder eventBuilder(String eventName);
/**
* Return a {@link LogRecordBuilder} to emit a log record.
*

View File

@ -8,6 +8,20 @@ package io.opentelemetry.api.logs;
/** Builder class for creating {@link Logger} instances. */
public interface LoggerBuilder {
/**
* Set the event domain of the resulting {@link Logger}.
*
* <p><b>NOTE:</b> Event domain is required to use {@link Logger#eventBuilder(String)}.
*
* <p>The event domain will be included in the {@code event.domain} attribute of every event
* produced by the resulting {@link Logger}.
*
* @param eventDomain The event domain, which acts as a namespace for event names. Within a
* particular event domain, event name defines a particular class or type of event.
* @return this
*/
LoggerBuilder setEventDomain(String eventDomain);
/**
* Assign an OpenTelemetry schema URL to the resulting {@link Logger}.
*

View File

@ -14,12 +14,13 @@ import javax.annotation.concurrent.ThreadSafe;
* <p>The OpenTelemetry logging API exists to satisfy two use cases:
*
* <ol>
* <li>Enable emitting structured <a
* href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/semantic_conventions/events.md">events</a>
* via {@link Logger#eventBuilder(String)}. Requires assigning an {@link
* LoggerBuilder#setEventDomain(String) event domain} to the {@link Logger}.
* <li>Enable the creation of log appenders, which bridge logs from other log frameworks (e.g.
* SLF4J, Log4j, JUL, Logback, etc) into OpenTelemetry via {@link Logger#logRecordBuilder()}.
* It is <b>NOT</b> a replacement log framework.
* <li>Enable emitting structured <a
* href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/semantic_conventions/events.md">events</a>.
* TODO: add link when event API is added.
* </ol>
*
* @see Logger

View File

@ -26,6 +26,26 @@ class DefaultLoggerProviderTest {
.setSchemaUrl("http://schema.com")
.build())
.doesNotThrowAnyException();
;
assertThatCode(() -> provider.loggerBuilder("scope-name").build().logRecordBuilder())
.doesNotThrowAnyException();
assertThatCode(() -> provider.loggerBuilder("scope-name").build().eventBuilder("event-name"))
.doesNotThrowAnyException();
assertThatCode(
() ->
provider
.loggerBuilder("scope-name")
.setEventDomain("event-domain")
.build()
.logRecordBuilder())
.doesNotThrowAnyException();
assertThatCode(
() ->
provider
.loggerBuilder("scope-name")
.setEventDomain("event-domain")
.build()
.eventBuilder("event-name"))
.doesNotThrowAnyException();
}
}

View File

@ -5,24 +5,31 @@
package io.opentelemetry.api.logs;
import static io.opentelemetry.api.internal.ValidationUtil.API_USAGE_LOGGER_NAME;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import io.github.netmikey.logunit.api.LogCapturer;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.context.Context;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.slf4j.event.LoggingEvent;
class DefaultLoggerTest {
private static final Logger logger = DefaultLogger.getInstance();
@RegisterExtension
LogCapturer apiUsageLogs = LogCapturer.create().captureForLogger(API_USAGE_LOGGER_NAME);
@Test
void buildAndEmit() {
// Logger with no event.domain
assertThatCode(
() ->
logger
DefaultLogger.getInstance(true)
.logRecordBuilder()
.setEpoch(100, TimeUnit.SECONDS)
.setEpoch(Instant.now())
@ -34,5 +41,57 @@ class DefaultLoggerTest {
.setAllAttributes(Attributes.builder().put("key2", "value2").build())
.emit())
.doesNotThrowAnyException();
assertThatCode(
() ->
DefaultLogger.getInstance(true)
.eventBuilder("event-name")
.setEpoch(100, TimeUnit.SECONDS)
.setEpoch(Instant.now())
.setContext(Context.root())
.setSeverity(Severity.DEBUG)
.setSeverityText("debug")
.setBody("body")
.setAttribute(AttributeKey.stringKey("key1"), "value1")
.setAllAttributes(Attributes.builder().put("key2", "value2").build())
.emit())
.doesNotThrowAnyException();
assertThat(apiUsageLogs.getEvents()).isEmpty();
// Logger with event.domain
assertThatCode(
() ->
DefaultLogger.getInstance(false)
.logRecordBuilder()
.setEpoch(100, TimeUnit.SECONDS)
.setEpoch(Instant.now())
.setContext(Context.root())
.setSeverity(Severity.DEBUG)
.setSeverityText("debug")
.setBody("body")
.setAttribute(AttributeKey.stringKey("key1"), "value1")
.setAllAttributes(Attributes.builder().put("key2", "value2").build())
.emit())
.doesNotThrowAnyException();
assertThatCode(
() ->
DefaultLogger.getInstance(false)
.eventBuilder("event-name")
.setEpoch(100, TimeUnit.SECONDS)
.setEpoch(Instant.now())
.setContext(Context.root())
.setSeverity(Severity.DEBUG)
.setSeverityText("debug")
.setBody("body")
.setAttribute(AttributeKey.stringKey("key1"), "value1")
.setAllAttributes(Attributes.builder().put("key2", "value2").build())
.emit())
.doesNotThrowAnyException();
assertThat(apiUsageLogs.getEvents())
.hasSize(1)
.extracting(LoggingEvent::getMessage)
.allMatch(
log ->
log.equals(
"Cannot emit event from Logger without event domain. Please use LoggerBuilder#setEventDomain(String) when obtaining Logger."));
}
}

View File

@ -6,6 +6,7 @@
package io.opentelemetry.sdk.logs;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.logs.EventBuilder;
import io.opentelemetry.api.logs.LogRecordBuilder;
import io.opentelemetry.api.logs.Severity;
import io.opentelemetry.api.trace.Span;
@ -18,8 +19,8 @@ import java.time.Instant;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
/** SDK implementation of {@link LogRecordBuilder}. */
final class SdkLogRecordBuilder implements LogRecordBuilder {
/** SDK implementation of {@link EventBuilder} and {@link LogRecordBuilder}. */
final class SdkLogRecordBuilder implements EventBuilder {
private final LoggerSharedState loggerSharedState;
private final LogLimits logLimits;
@ -40,43 +41,43 @@ final class SdkLogRecordBuilder implements LogRecordBuilder {
}
@Override
public LogRecordBuilder setEpoch(long timestamp, TimeUnit unit) {
public SdkLogRecordBuilder setEpoch(long timestamp, TimeUnit unit) {
this.epochNanos = unit.toNanos(timestamp);
return this;
}
@Override
public LogRecordBuilder setEpoch(Instant instant) {
public SdkLogRecordBuilder setEpoch(Instant instant) {
this.epochNanos = TimeUnit.SECONDS.toNanos(instant.getEpochSecond()) + instant.getNano();
return this;
}
@Override
public LogRecordBuilder setContext(Context context) {
public SdkLogRecordBuilder setContext(Context context) {
this.spanContext = Span.fromContext(context).getSpanContext();
return this;
}
@Override
public LogRecordBuilder setSeverity(Severity severity) {
public SdkLogRecordBuilder setSeverity(Severity severity) {
this.severity = severity;
return this;
}
@Override
public LogRecordBuilder setSeverityText(String severityText) {
public SdkLogRecordBuilder setSeverityText(String severityText) {
this.severityText = severityText;
return this;
}
@Override
public LogRecordBuilder setBody(String body) {
public SdkLogRecordBuilder setBody(String body) {
this.body = Body.string(body);
return this;
}
@Override
public <T> LogRecordBuilder setAttribute(AttributeKey<T> key, T value) {
public <T> SdkLogRecordBuilder setAttribute(AttributeKey<T> key, T value) {
if (key == null || key.getKey().isEmpty() || value == null) {
return this;
}

View File

@ -5,20 +5,64 @@
package io.opentelemetry.sdk.logs;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.internal.ValidationUtil;
import io.opentelemetry.api.logs.EventBuilder;
import io.opentelemetry.api.logs.LogRecordBuilder;
import io.opentelemetry.api.logs.Logger;
import io.opentelemetry.api.logs.LoggerProvider;
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
import java.util.logging.Level;
import javax.annotation.Nullable;
/** SDK implementation of {@link Logger}. */
final class SdkLogger implements Logger {
// Obtain a noop logger with the domain set so that we can obtain noop EventBuilder without
// generating additional warning logs
private static final Logger NOOP_LOGGER_WITH_DOMAIN =
LoggerProvider.noop().loggerBuilder("unused").setEventDomain("unused").build();
private final LoggerSharedState loggerSharedState;
private final InstrumentationScopeInfo instrumentationScopeInfo;
@Nullable private final String eventDomain;
SdkLogger(
LoggerSharedState loggerSharedState, InstrumentationScopeInfo instrumentationScopeInfo) {
this(loggerSharedState, instrumentationScopeInfo, null);
}
SdkLogger(
LoggerSharedState loggerSharedState,
InstrumentationScopeInfo instrumentationScopeInfo,
@Nullable String eventDomain) {
this.loggerSharedState = loggerSharedState;
this.instrumentationScopeInfo = instrumentationScopeInfo;
this.eventDomain = eventDomain;
}
/**
* Return a logger identical to {@code this} ensuring the {@link #eventDomain} is equal to {@code
* eventDomain}. If {@link #eventDomain} is not equal, creates a new instance.
*/
SdkLogger withEventDomain(String eventDomain) {
if (!eventDomain.equals(this.eventDomain)) {
return new SdkLogger(loggerSharedState, instrumentationScopeInfo, eventDomain);
}
return this;
}
@Override
public EventBuilder eventBuilder(String eventName) {
if (eventDomain == null) {
ValidationUtil.log(
"Cannot emit event from Logger without event domain. Please use LoggerBuilder#setEventDomain(String) when obtaining Logger.",
Level.WARNING);
return NOOP_LOGGER_WITH_DOMAIN.eventBuilder(eventName);
}
return new SdkLogRecordBuilder(loggerSharedState, instrumentationScopeInfo)
.setAttribute(AttributeKey.stringKey("event.domain"), eventDomain)
.setAttribute(AttributeKey.stringKey("event.name"), eventName);
}
@Override

View File

@ -9,17 +9,25 @@ import io.opentelemetry.api.logs.LoggerBuilder;
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
import io.opentelemetry.sdk.common.InstrumentationScopeInfoBuilder;
import io.opentelemetry.sdk.internal.ComponentRegistry;
import javax.annotation.Nullable;
final class SdkLoggerBuilder implements LoggerBuilder {
private final ComponentRegistry<SdkLogger> registry;
private final InstrumentationScopeInfoBuilder scopeBuilder;
@Nullable private String eventDomain;
SdkLoggerBuilder(ComponentRegistry<SdkLogger> registry, String instrumentationScopeName) {
this.registry = registry;
this.scopeBuilder = InstrumentationScopeInfo.builder(instrumentationScopeName);
}
@Override
public LoggerBuilder setEventDomain(String eventDomain) {
this.eventDomain = eventDomain;
return this;
}
@Override
public SdkLoggerBuilder setSchemaUrl(String schemaUrl) {
scopeBuilder.setSchemaUrl(schemaUrl);
@ -34,6 +42,7 @@ final class SdkLoggerBuilder implements LoggerBuilder {
@Override
public SdkLogger build() {
return registry.get(scopeBuilder.build());
SdkLogger logger = registry.get(scopeBuilder.build());
return eventDomain == null ? logger : logger.withEventDomain(eventDomain);
}
}

View File

@ -9,6 +9,7 @@ import static io.opentelemetry.api.common.AttributeKey.booleanArrayKey;
import static io.opentelemetry.api.common.AttributeKey.doubleArrayKey;
import static io.opentelemetry.api.common.AttributeKey.longArrayKey;
import static io.opentelemetry.api.common.AttributeKey.stringArrayKey;
import static io.opentelemetry.api.internal.ValidationUtil.API_USAGE_LOGGER_NAME;
import static io.opentelemetry.sdk.testing.assertj.LogAssertions.assertThat;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
@ -17,6 +18,7 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import io.github.netmikey.logunit.api.LogCapturer;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
@ -30,9 +32,14 @@ import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.slf4j.event.LoggingEvent;
class SdkLoggerTest {
@RegisterExtension
LogCapturer apiUsageLogs = LogCapturer.create().captureForLogger(API_USAGE_LOGGER_NAME);
@Test
void logRecordBuilder() {
LoggerSharedState state = mock(LoggerSharedState.class);
@ -134,4 +141,40 @@ class SdkLoggerTest {
verify(logRecordProcessor, never()).onEmit(any());
}
@Test
void eventBuilder() {
AtomicReference<ReadWriteLogRecord> seenLog = new AtomicReference<>();
SdkLoggerProvider loggerProvider =
SdkLoggerProvider.builder().addLogRecordProcessor(seenLog::set).build();
// Emit event from logger with name and add event domain
loggerProvider
.loggerBuilder("test")
.setEventDomain("event-domain")
.build()
.eventBuilder("event-name")
.emit();
assertThat(seenLog.get().toLogRecordData())
.hasAttributes(
Attributes.builder()
.put("event.domain", "event-domain")
.put("event.name", "event-name")
.build());
assertThat(apiUsageLogs.getEvents()).isEmpty();
seenLog.set(null);
// Emit event from logger with name and no event domain
loggerProvider.get("test").eventBuilder("event-name");
assertThat(apiUsageLogs.getEvents())
.hasSize(1)
.extracting(LoggingEvent::getMessage)
.allMatch(
log ->
log.equals(
"Cannot emit event from Logger without event domain. Please use LoggerBuilder#setEventDomain(String) when obtaining Logger."));
}
}