diff --git a/api/all/src/main/java/io/opentelemetry/api/GlobalOpenTelemetry.java b/api/all/src/main/java/io/opentelemetry/api/GlobalOpenTelemetry.java index 5e3a1e0ecdb..af69201f484 100644 --- a/api/all/src/main/java/io/opentelemetry/api/GlobalOpenTelemetry.java +++ b/api/all/src/main/java/io/opentelemetry/api/GlobalOpenTelemetry.java @@ -18,6 +18,7 @@ import io.opentelemetry.context.propagation.ContextPropagators; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Objects; import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; @@ -25,26 +26,40 @@ import javax.annotation.concurrent.ThreadSafe; /** - * A global singleton for the entrypoint to telemetry functionality for tracing, metrics and - * baggage. + * Provides access to a global singleton {@link OpenTelemetry} instance. * - *

If using the OpenTelemetry SDK, you may want to instantiate the {@link OpenTelemetry} to - * provide configuration, for example of {@code Resource} or {@code Sampler}. See {@code - * OpenTelemetrySdk} and {@code OpenTelemetrySdk.builder} for information on how to construct the - * SDK's {@link OpenTelemetry} implementation. + *

WARNING: To avoid inherent complications around initialization ordering, it's best-practice to + * pass around instances of {@link OpenTelemetry} rather than using {@link GlobalOpenTelemetry}. + * However, the OpenTelemetry javagent makes the {@link OpenTelemetry} instance it installs + * available via {@link GlobalOpenTelemetry}. As a result, {@link GlobalOpenTelemetry} plays an + * important role in native instrumentation and application custom instrumentation. * - *

WARNING: Due to the inherent complications around initialization order involving this class - * and its single global instance, we strongly recommend *not* using GlobalOpenTelemetry unless you - * have a use-case that absolutely requires it. Please favor using instances of OpenTelemetry - * wherever possible. + *

Native instrumentation should use {@link #getOrNoop()} as the default {@link OpenTelemetry} + * instance, and expose APIs for setting a custom instance. This results in the following behavior: * - *

If you are using the OpenTelemetry javaagent, it is generally best to only call - * GlobalOpenTelemetry.get() once, and then pass the resulting reference where you need to use it. + *

* - * @see TracerProvider - * @see ContextPropagators + *

Applications with custom instrumentation should call {@link #isSet()} once during + * initialization to access the javaagent instance or initialize (e.g. {@code isSet() ? + * GlobalOpenTelemetry.get() : initializeSdk()}), and pass the resulting instance around manually + * (or with dependency injection) to install custom instrumentation. This results in the following + * behavior: + * + *

*/ -// We intentionally assign to be use for error reporting. +// We intentionally assign for error reporting. @SuppressWarnings("StaticAssignmentOfThrowable") public final class GlobalOpenTelemetry { @@ -67,10 +82,56 @@ public final class GlobalOpenTelemetry { private GlobalOpenTelemetry() {} /** - * Returns the registered global {@link OpenTelemetry}. + * Returns the registered global {@link OpenTelemetry} if set, or else {@link + * OpenTelemetry#noop()}. + * + *

NOTE: if the global instance is set, the response is obfuscated to prevent callers from + * casting to SDK implementation instances and inappropriately accessing non-instrumentation APIs. + * + *

NOTE: This does not result in the {@link #set(OpenTelemetry)} side effects of {@link + * #get()}. + * + *

Native instrumentation should use this method to initialize their default {@link + * OpenTelemetry} instance. See class javadoc for more details. + */ + public static OpenTelemetry getOrNoop() { + synchronized (mutex) { + return globalOpenTelemetry != null ? globalOpenTelemetry : OpenTelemetry.noop(); + } + } + + /** + * Returns {@code true} if {@link GlobalOpenTelemetry} is set, otherwise {@code false}. + * + *

Application custom instrumentation should use this method during initialization. See class + * javadoc for more details. + */ + public static boolean isSet() { + synchronized (mutex) { + return globalOpenTelemetry != null; + } + } + + /** + * Returns the registered global {@link OpenTelemetry} if set, else calls {@link + * GlobalOpenTelemetry#set(OpenTelemetry)} with a no-op {@link OpenTelemetry} instance and returns + * that. + * + *

NOTE: all returned instanced are obfuscated to prevent callers from casting to SDK + * implementation instances and inappropriately accessing non-instrumentation APIs. + * + *

Native instrumentations should use {@link #getOrNoop()} instead. See class javadoc for more + * details. + * + *

Application custom instrumentation should use {@link #isSet()} and only call this if the + * response is {@code true}. See class javadoc for more details. + * + *

If the global instance has not been set, and {@code + * io.opentelemetry:opentelemetry-sdk-extension-autoconfigure} is present, and {@value + * GLOBAL_AUTOCONFIGURE_ENABLED_PROPERTY} is {@code true}, the global instance will be set to an + * autoconfigured instance instead of {@link OpenTelemetry#noop()}. * - * @throws IllegalStateException if a provider has been specified by system property using the - * interface FQCN but the specified provider cannot be found. + * @throws IllegalStateException if autoconfigure initialization is triggered and fails. */ public static OpenTelemetry get() { OpenTelemetry openTelemetry = globalOpenTelemetry; @@ -85,7 +146,7 @@ public static OpenTelemetry get() { } set(OpenTelemetry.noop()); - return OpenTelemetry.noop(); + openTelemetry = Objects.requireNonNull(globalOpenTelemetry); } } } diff --git a/api/testing-internal/src/main/java/io/opentelemetry/api/testing/internal/AbstractOpenTelemetryTest.java b/api/testing-internal/src/main/java/io/opentelemetry/api/testing/internal/AbstractOpenTelemetryTest.java index 4223d853661..da8bf1d465a 100644 --- a/api/testing-internal/src/main/java/io/opentelemetry/api/testing/internal/AbstractOpenTelemetryTest.java +++ b/api/testing-internal/src/main/java/io/opentelemetry/api/testing/internal/AbstractOpenTelemetryTest.java @@ -31,10 +31,6 @@ private void setOpenTelemetry() { GlobalOpenTelemetry.set(getOpenTelemetry()); } - private static OpenTelemetry getGlobalOpenTelemetry() { - return GlobalOpenTelemetry.get(); - } - @AfterEach public void after() { GlobalOpenTelemetry.resetForTest(); @@ -90,7 +86,9 @@ void independentNonGlobalPropagators() { @Test void setThenSet() { + assertThat(GlobalOpenTelemetry.isSet()).isFalse(); setOpenTelemetry(); + assertThat(GlobalOpenTelemetry.isSet()).isTrue(); assertThatThrownBy(() -> GlobalOpenTelemetry.set(getOpenTelemetry())) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("GlobalOpenTelemetry.set has already been called") @@ -99,14 +97,43 @@ void setThenSet() { @Test void getThenSet() { - assertThat(getGlobalOpenTelemetry().getClass().getName()) - .isEqualTo("io.opentelemetry.api.DefaultOpenTelemetry"); + assertThat(GlobalOpenTelemetry.isSet()).isFalse(); + assertThat(GlobalOpenTelemetry.getOrNoop()).isSameAs(OpenTelemetry.noop()); + + // Calling GlobalOpenTelemetry.get() has the side affect of setting GlobalOpenTelemetry to an + // (obfuscated) noop. + // Call GlobalOpenTelemetry.get() using a utility method so we can later assert it was + // responsible for setting GlobalOpenTelemetry. + assertThat(getGlobalOpenTelemetry()) + .satisfies( + instance -> + assertThat(instance.getClass().getName()) + .isEqualTo("io.opentelemetry.api.GlobalOpenTelemetry$ObfuscatedOpenTelemetry")) + .extracting("delegate") + .isSameAs(OpenTelemetry.noop()); + + assertThat(GlobalOpenTelemetry.isSet()).isTrue(); + assertThat(GlobalOpenTelemetry.getOrNoop()).isNotSameAs(OpenTelemetry.noop()); + assertThatThrownBy(() -> GlobalOpenTelemetry.set(getOpenTelemetry())) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("GlobalOpenTelemetry.set has already been called") .hasStackTraceContaining("getGlobalOpenTelemetry"); } + private static OpenTelemetry getGlobalOpenTelemetry() { + return GlobalOpenTelemetry.get(); + } + + @Test + void getOrNoop() { + // Should be able to call getOrNoop multiple times without side effect and later call set. + assertThat(GlobalOpenTelemetry.getOrNoop()).isSameAs(OpenTelemetry.noop()); + assertThat(GlobalOpenTelemetry.getOrNoop()).isSameAs(OpenTelemetry.noop()); + setOpenTelemetry(); + assertThat(GlobalOpenTelemetry.getOrNoop()).isNotSameAs(OpenTelemetry.noop()); + } + @Test void toString_noop_Valid() { assertThat(getOpenTelemetry().toString()) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt index 0e998b34399..e1abf63b48c 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt @@ -1,2 +1,5 @@ Comparing source compatibility of opentelemetry-api-1.57.0-SNAPSHOT.jar against opentelemetry-api-1.56.0.jar -No changes. \ No newline at end of file +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.api.GlobalOpenTelemetry (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.OpenTelemetry getOrNoop() + +++ NEW METHOD: PUBLIC(+) STATIC(+) boolean isSet() diff --git a/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdkTest.java b/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdkTest.java index f630b814cb7..6feacb1a889 100644 --- a/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdkTest.java +++ b/sdk-extensions/autoconfigure/src/testFullConfig/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdkTest.java @@ -73,7 +73,7 @@ void initializeAndGet_noGlobal() { @Test void globalOpenTelemetry_AutoConfigureDisabled() { // Autoconfigure is disabled by default and enabled via otel.java.global-autoconfigure.enabled - assertThat(GlobalOpenTelemetry.get()).isSameAs(OpenTelemetry.noop()); + assertThat(GlobalOpenTelemetry.get()).extracting("delegate").isSameAs(OpenTelemetry.noop()); logs.assertContains( "AutoConfiguredOpenTelemetrySdk found on classpath but automatic configuration is disabled." diff --git a/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/junit5/OpenTelemetryExtensionTest.java b/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/junit5/OpenTelemetryExtensionTest.java index f13c41e9878..2feb2d4cbde 100644 --- a/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/junit5/OpenTelemetryExtensionTest.java +++ b/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/junit5/OpenTelemetryExtensionTest.java @@ -230,7 +230,7 @@ void afterAll() { assertThat(extension.getSpans()).isNotEmpty(); extension.afterAll(null); - assertThat(GlobalOpenTelemetry.get()).isSameAs(OpenTelemetry.noop()); + assertThat(GlobalOpenTelemetry.get()).extracting("delegate").isSameAs(OpenTelemetry.noop()); meter.counterBuilder("counter").build().add(10); tracer.spanBuilder("span").startSpan().end();