diff --git a/api/all/build.gradle.kts b/api/all/build.gradle.kts index 5c003c4ef6b..4c461207911 100644 --- a/api/all/build.gradle.kts +++ b/api/all/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("maven-publish") id("me.champeau.jmh") + id("org.unbroken-dome.test-sets") id("ru.vyarus.animalsniffer") } @@ -10,6 +11,11 @@ description = "OpenTelemetry API" extra["moduleName"] = "io.opentelemetry.api" base.archivesBaseName = "opentelemetry-api" +testSets { + create("testLogsIfSdkFound") + create("testDoesNotLogIfSdkFoundAndSuppressed") +} + dependencies { api(project(":context")) @@ -17,4 +23,20 @@ dependencies { testImplementation("edu.berkeley.cs.jqf:jqf-fuzz") testImplementation("com.google.guava:guava-testlib") + + add("testLogsIfSdkFoundImplementation", project(":sdk:all")) + add("testDoesNotLogIfSdkFoundAndSuppressedImplementation", project(":sdk:all")) } + +tasks { + val testLogsIfSdkFound by existing(Test::class) { + } + + val testDoesNotLogIfSdkFoundAndSuppressed by existing(Test::class) { + jvmArgs("-Dotel.sdk.suppress-sdk-initialized-warning=true") + } + + named("check") { + dependsOn(testLogsIfSdkFound, testDoesNotLogIfSdkFoundAndSuppressed) + } +} \ No newline at end of file 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 9f91cf6a399..f3d8cecd260 100644 --- a/api/all/src/main/java/io/opentelemetry/api/GlobalOpenTelemetry.java +++ b/api/all/src/main/java/io/opentelemetry/api/GlobalOpenTelemetry.java @@ -32,6 +32,9 @@ public final class GlobalOpenTelemetry { private static final Logger logger = Logger.getLogger(GlobalOpenTelemetry.class.getName()); + private static final boolean suppressSdkCheck = + Boolean.getBoolean("otel.sdk.suppress-sdk-initialized-warning"); + private static final Object mutex = new Object(); @Nullable private static volatile ObfuscatedOpenTelemetry globalOpenTelemetry; @@ -58,6 +61,10 @@ public static OpenTelemetry get() { return autoConfigured; } + if (!suppressSdkCheck) { + SdkChecker.logIfSdkFound(); + } + set(OpenTelemetry.noop()); return OpenTelemetry.noop(); } @@ -168,6 +175,39 @@ private static OpenTelemetry maybeAutoConfigure() { } } + // Use an inner class that checks and logs in its static initializer to have log-once behavior + // using initial classloader lock and no further runtime locks or atomics. + private static class SdkChecker { + static { + boolean hasSdk = false; + try { + Class.forName("io.opentelemetry.sdk.OpenTelemetrySdk"); + hasSdk = true; + } catch (Throwable t) { + // Ignore + } + + if (hasSdk) { + logger.log( + Level.SEVERE, + "Attempt to access GlobalOpenTelemetry.get before OpenTelemetrySdk has been " + + "initialized. This generally means telemetry will not be recorded for parts of " + + "your application. Make sure to initialize OpenTelemetrySdk, using " + + "OpenTelemetrySdk.builder()...buildAndRegisterGlobal(), as early as possible in " + + "your application. If you do not need to use the OpenTelemetry SDK, either " + + "exclude it from your classpath or set the " + + "'otel.sdk.suppress-sdk-initialized-warning' system property to true.", + // Add stack trace to log to allow user to find the problematic invocation. + new Throwable()); + } + } + + // All the logic is in the static initializer, this method is called just to load the class and + // that's it. JVM will then optimize it away completely because it's empty so we have no + // overhead for a log-once pattern. + static void logIfSdkFound() {} + } + /** * Static global instances are obfuscated when they are returned from the API to prevent users * from casting them to their SDK-specific implementation. For example, we do not want users to diff --git a/api/all/src/testDoesNotLogIfSdkFoundAndSuppressed/java/io/opentelemetry/api/GlobalOpenTelemetryTest.java b/api/all/src/testDoesNotLogIfSdkFoundAndSuppressed/java/io/opentelemetry/api/GlobalOpenTelemetryTest.java new file mode 100644 index 00000000000..0e8ce156151 --- /dev/null +++ b/api/all/src/testDoesNotLogIfSdkFoundAndSuppressed/java/io/opentelemetry/api/GlobalOpenTelemetryTest.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api; + +import io.github.netmikey.logunit.api.LogCapturer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class GlobalOpenTelemetryTest { + + @RegisterExtension + LogCapturer logs = LogCapturer.create().captureForType(GlobalOpenTelemetry.class); + + @Test + void logIsSuppressed() { + GlobalOpenTelemetry.get(); + logs.assertDoesNotContain( + "Attempt to access GlobalOpenTelemetry.get before OpenTelemetrySdk has been " + + "initialized."); + } +} diff --git a/api/all/src/testLogsIfSdkFound/java/io/opentelemetry/api/GlobalOpenTelemetryTest.java b/api/all/src/testLogsIfSdkFound/java/io/opentelemetry/api/GlobalOpenTelemetryTest.java new file mode 100644 index 00000000000..9e9a1e203f5 --- /dev/null +++ b/api/all/src/testLogsIfSdkFound/java/io/opentelemetry/api/GlobalOpenTelemetryTest.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.netmikey.logunit.api.LogCapturer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.event.Level; +import org.slf4j.event.LoggingEvent; + +class GlobalOpenTelemetryTest { + + @RegisterExtension + LogCapturer logs = LogCapturer.create().captureForType(GlobalOpenTelemetry.class); + + @Test + void logsWarningOnAccessWithoutSdk() { + GlobalOpenTelemetry.get(); + LoggingEvent log = + logs.assertContains( + "Attempt to access GlobalOpenTelemetry.get before OpenTelemetrySdk has been " + + "initialized."); + assertThat(log.getLevel()).isEqualTo(Level.ERROR); + } +}