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

Event emitter provider #5049

Merged
merged 10 commits into from
Feb 3, 2023
13 changes: 13 additions & 0 deletions api/events/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
plugins {
id("otel.java-conventions")
id("otel.publish-conventions")

id("otel.animalsniffer-conventions")
}

description = "OpenTelemetry Events API"
otelJava.moduleName.set("io.opentelemetry.api.events")

dependencies {
api(project(":api:all"))
}
1 change: 1 addition & 0 deletions api/events/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
otel.release=alpha
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.events;

import io.opentelemetry.api.common.Attributes;

class DefaultEventEmitter implements EventEmitter {

private static final EventEmitter INSTANCE = new DefaultEventEmitter();

private DefaultEventEmitter() {}

static EventEmitter getInstance() {
return INSTANCE;
}

@Override
public void emit(String eventName, Attributes attributes) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.events;

class DefaultEventEmitterProvider implements EventEmitterProvider {

private static final EventEmitterProvider INSTANCE = new DefaultEventEmitterProvider();
private static final EventEmitterBuilder NOOP_EVENT_EMITTER_BUILDER =
new NoopEventEmitterBuilder();

private DefaultEventEmitterProvider() {}

static EventEmitterProvider getInstance() {
return INSTANCE;
}

@Override
public EventEmitterBuilder eventEmitterBuilder(
String instrumentationScopeName, String eventDomain) {
return NOOP_EVENT_EMITTER_BUILDER;
}

private static class NoopEventEmitterBuilder implements EventEmitterBuilder {

@Override
public EventEmitterBuilder setSchemaUrl(String schemaUrl) {
return this;
}

@Override
public EventEmitterBuilder setInstrumentationVersion(String instrumentationVersion) {
return this;
}

@Override
public EventEmitter build() {
return DefaultEventEmitter.getInstance();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.events;

import io.opentelemetry.api.common.Attributes;
import javax.annotation.concurrent.ThreadSafe;

/**
* A {@link EventEmitter} is the entry point into an event pipeline.
*
* <p>Example usage emitting events:
*
* <pre>{@code
* class MyClass {
* private final EventEmitter eventEmitter = openTelemetryEventEmitterProvider.loggerBuilder("instrumentation-library-name", "acme.observability")
jack-berg marked this conversation as resolved.
Show resolved Hide resolved
* .setInstrumentationVersion("1.0.0")
* .build();
*
* void doWork() {
* eventEmitter.emit("my-event", Attributes.builder()
* .put("key1", "value1")
* .put("key2", "value2")
* .build())
* // do work
* }
* }
* }</pre>
*/
@ThreadSafe
public interface EventEmitter {

/**
* Emit an event.
*
* @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.
* @param attributes attributes associated with the event
*/
void emit(String eventName, Attributes attributes);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.events;

/** Builder class for creating {@link EventEmitter} instances. */
public interface EventEmitterBuilder {

/**
* Assign an OpenTelemetry schema URL to the resulting {@link EventEmitter}.
*
* @param schemaUrl the URL of the OpenTelemetry schema being used by this instrumentation scope
* @return this
*/
EventEmitterBuilder setSchemaUrl(String schemaUrl);

/**
* Assign a version to the instrumentation scope that is using the resulting {@link EventEmitter}.
*
* @param instrumentationScopeVersion the version of the instrumentation scope
* @return this
*/
EventEmitterBuilder setInstrumentationVersion(String instrumentationScopeVersion);

/**
* Gets or creates a {@link EventEmitter} instance.
*
* @return a logger instance configured with the provided options
*/
EventEmitter build();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.events;

import javax.annotation.concurrent.ThreadSafe;

/**
* A registry for creating scoped {@link EventEmitter}s. The name <i>Provider</i> is for consistency
* with other languages and it is <b>NOT</b> loaded using reflection.
*
* @see EventEmitter
*/
@ThreadSafe
public interface EventEmitterProvider {

/**
* Gets or creates a named EventEmitter instance which emits events to the {@code eventDomain}.
*
* @param instrumentationScopeName A name uniquely identifying the instrumentation scope, such as
* the instrumentation library, package, or fully qualified class name. Must not be null.
* @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 a Logger instance.
*/
default EventEmitter get(String instrumentationScopeName, String eventDomain) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is eventDomain mandatory? I don't even know what sort of values I'd put in for this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As of now event.domain is required. The spec describes it as:

The event.domain attribute is used to logically separate events from different systems. For example, to record Events from browser apps, mobile apps and Kubernetes, we could use browser, device and k8s as the domain for their Events. This provides a clean separation of semantics for events in each of the domains. Within a particular domain, the event.name attribute identifies the event. Events with same domain and name are structurally similar to one another.

There's an open issue to discuss whether event.domain and event.name can be merged into a single field.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only sticking point I have with this API. I don't love having to explain to people what this is supposed to be used for. We have had enough trouble explaining what the "name" of a Tracer is...this adds an additional layer of having to explain this arcane idea that almost no one will care about. Can we make it optional? Or default to something like "none" or "unknown" or "who cares?"? :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not opposed to making it optional with a default given the experimental state of the spec. The spec could reaffirm the importance and requirement of this field, in which case we'd have to come back and adjust our API, but that's fine and won't be the last breaking change to the event API. Will push a commit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed a commit where event domain is now optional, set via eventEmitterProvider.eventEmitterBuilder("scope-name").setEventDomain("event-domain").build().

If unset, the event.domain attribute defaults to unknown.

return eventEmitterBuilder(instrumentationScopeName, eventDomain).build();
}

/**
* Creates a LoggerBuilder for a named EventEmitter instance.
*
* @param instrumentationScopeName A name uniquely identifying the instrumentation scope, such as
* the instrumentation library, package, or fully qualified class name. Must not be null.
* @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 a LoggerBuilder instance.
*/
EventEmitterBuilder eventEmitterBuilder(String instrumentationScopeName, String eventDomain);

/**
* Returns a no-op {@link EventEmitterProvider} which provides Loggers which do not record or
* emit.
*/
static EventEmitterProvider noop() {
return DefaultEventEmitterProvider.getInstance();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.events;

import io.opentelemetry.api.GlobalOpenTelemetry;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;

/**
* This class provides a temporary global accessor for {@link EventEmitterProvider} until the event
* API is marked stable. It will eventually be merged into {@link GlobalOpenTelemetry}.
*/
// We intentionally assign to be used for error reporting.
@SuppressWarnings("StaticAssignmentOfThrowable")
public final class GlobalEventEmitterProvider {

private static final AtomicReference<EventEmitterProvider> instance =
new AtomicReference<>(EventEmitterProvider.noop());

@Nullable private static volatile Throwable setInstanceCaller;

private GlobalEventEmitterProvider() {}

/** Returns the globally registered {@link EventEmitterProvider}. */
// instance cannot be set to null
@SuppressWarnings("NullAway")
public static EventEmitterProvider get() {
return instance.get();
}

/**
* Sets the global {@link EventEmitterProvider}. Future calls to {@link #get()} will return the
* provided {@link EventEmitterProvider} instance. This should be called once as early as possible
* in your application initialization logic.
*/
public static void set(EventEmitterProvider eventEmitterProvider) {
boolean changed = instance.compareAndSet(EventEmitterProvider.noop(), eventEmitterProvider);
if (!changed && (eventEmitterProvider != EventEmitterProvider.noop())) {
throw new IllegalStateException(
"GlobalEventEmitterProvider.set has already been called. GlobalEventEmitterProvider.set "
+ "must be called only once before any calls to GlobalEventEmitterProvider.get. "
+ "Previous invocation set to cause of this exception.",
setInstanceCaller);
}
setInstanceCaller = new Throwable();
}

/**
* Unsets the global {@link EventEmitterProvider}. This is only meant to be used from tests which
* need to reconfigure {@link EventEmitterProvider}.
*/
public static void resetForTest() {
instance.set(EventEmitterProvider.noop());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.events;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;

import io.opentelemetry.api.common.Attributes;
import org.junit.jupiter.api.Test;

class DefaultEventEmitterProviderTest {

@Test
void noopEventEmitterProvider_doesNotThrow() {
EventEmitterProvider provider = EventEmitterProvider.noop();

assertThat(provider).isSameAs(DefaultEventEmitterProvider.getInstance());
assertThatCode(() -> provider.get("scope-name", "event-domain")).doesNotThrowAnyException();
assertThatCode(
() ->
provider
.eventEmitterBuilder("scope-name", "event-domain")
.setInstrumentationVersion("1.0")
.setSchemaUrl("http://schema.com")
.build())
.doesNotThrowAnyException();

assertThatCode(
() ->
provider
.eventEmitterBuilder("scope-name", "event-domain")
.build()
.emit("event-name", Attributes.empty()))
.doesNotThrowAnyException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.events;

import static org.assertj.core.api.Assertions.assertThatCode;

import io.opentelemetry.api.common.Attributes;
import org.junit.jupiter.api.Test;

class DefaultEventEmitterTest {

@Test
void emit() {
assertThatCode(() -> DefaultEventEmitter.getInstance().emit("event-name", Attributes.empty()))
.doesNotThrowAnyException();
assertThatCode(
() ->
DefaultEventEmitter.getInstance()
.emit("event-name", Attributes.builder().put("key1", "value1").build()))
.doesNotThrowAnyException();
}
}
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.api.events;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

class GlobalEventEmitterProviderTest {

@BeforeAll
static void beforeClass() {
GlobalEventEmitterProvider.resetForTest();
}

@AfterEach
void after() {
GlobalEventEmitterProvider.resetForTest();
}

@Test
void setAndGet() {
assertThat(GlobalEventEmitterProvider.get()).isEqualTo(EventEmitterProvider.noop());
EventEmitterProvider eventEmitterProvider =
(instrumentationScopeName, eventDomain) ->
EventEmitterProvider.noop().eventEmitterBuilder(instrumentationScopeName, eventDomain);
GlobalEventEmitterProvider.set(eventEmitterProvider);
assertThat(GlobalEventEmitterProvider.get()).isEqualTo(eventEmitterProvider);
}

@Test
void setThenSet() {
GlobalEventEmitterProvider.set(
(instrumentationScopeName, eventDomain) ->
EventEmitterProvider.noop().eventEmitterBuilder(instrumentationScopeName, eventDomain));
assertThatThrownBy(
() ->
GlobalEventEmitterProvider.set(
(instrumentationScopeName, eventDomain) ->
EventEmitterProvider.noop()
.eventEmitterBuilder(instrumentationScopeName, eventDomain)))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("GlobalEventEmitterProvider.set has already been called")
.hasStackTraceContaining("setThenSet");
}
}
Loading