From 687d67e4a213cab7dba24e4535e44b5fab6b8e6c Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Wed, 22 Apr 2026 15:28:46 -0500 Subject: [PATCH 1/7] Add guidance for null checking, promote ApiUsageLogger to opentelemetry-common public API --- .../api/baggage/ImmutableEntryMetadata.java | 4 +- .../api/internal/ApiUsageLogger.java | 42 -------------- .../api/trace/DefaultTracer.java | 4 +- .../java/io/opentelemetry/api/trace/Span.java | 8 +-- .../io/opentelemetry/api/trace/SpanId.java | 4 +- .../io/opentelemetry/api/trace/TraceId.java | 4 +- .../trace/ExtendedDefaultTracer.java | 5 +- .../opentelemetry/common/ApiUsageLogger.java | 52 +++++++++++++++++ .../common}/ApiUsageLoggerTest.java | 13 ++--- .../opentelemetry-common.txt | 5 +- docs/knowledge/README.md | 2 +- .../{api-stability.md => api-design.md} | 56 ++++++++++++++++++- docs/knowledge/general-patterns.md | 2 + 13 files changed, 135 insertions(+), 66 deletions(-) delete mode 100644 api/all/src/main/java/io/opentelemetry/api/internal/ApiUsageLogger.java create mode 100644 common/src/main/java/io/opentelemetry/common/ApiUsageLogger.java rename {api/all/src/test/java/io/opentelemetry/api/internal => common/src/test/java/io/opentelemetry/common}/ApiUsageLoggerTest.java (57%) rename docs/knowledge/{api-stability.md => api-design.md} (57%) diff --git a/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableEntryMetadata.java b/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableEntryMetadata.java index 2a2e1a3cc6c..75d0de51ae0 100644 --- a/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableEntryMetadata.java +++ b/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableEntryMetadata.java @@ -6,7 +6,7 @@ package io.opentelemetry.api.baggage; import com.google.auto.value.AutoValue; -import io.opentelemetry.api.internal.ApiUsageLogger; +import io.opentelemetry.common.ApiUsageLogger; import javax.annotation.concurrent.Immutable; @Immutable @@ -25,7 +25,7 @@ abstract class ImmutableEntryMetadata implements BaggageEntryMetadata { */ static ImmutableEntryMetadata create(String metadata) { if (metadata == null) { - ApiUsageLogger.log("metadata is null"); + ApiUsageLogger.log(BaggageEntryMetadata.class, "create", "metadata is null"); return EMPTY; } return new AutoValue_ImmutableEntryMetadata(metadata); diff --git a/api/all/src/main/java/io/opentelemetry/api/internal/ApiUsageLogger.java b/api/all/src/main/java/io/opentelemetry/api/internal/ApiUsageLogger.java deleted file mode 100644 index 9253afa10a0..00000000000 --- a/api/all/src/main/java/io/opentelemetry/api/internal/ApiUsageLogger.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.api.internal; - -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Helper for API misuse logging. - * - *

This class is internal and is hence not for public use. Its APIs are unstable and can change - * at any time. - */ -public final class ApiUsageLogger { - - private static final Logger API_USAGE_LOGGER = Logger.getLogger(ApiUsageLogger.class.getName()); - - /** - * Log the {@code message} to the {@link #API_USAGE_LOGGER API Usage Logger}. - * - *

Log at {@link Level#FINEST} and include a stack trace. - */ - public static void log(String message) { - log(message, Level.FINEST); - } - - /** - * Log the {@code message} to the {@link #API_USAGE_LOGGER API Usage Logger}. - * - *

Log includes a stack trace. - */ - public static void log(String message, Level level) { - if (API_USAGE_LOGGER.isLoggable(level)) { - API_USAGE_LOGGER.log(level, message, new AssertionError()); - } - } - - private ApiUsageLogger() {} -} diff --git a/api/all/src/main/java/io/opentelemetry/api/trace/DefaultTracer.java b/api/all/src/main/java/io/opentelemetry/api/trace/DefaultTracer.java index 808df33e15e..6b75f1a2bfa 100644 --- a/api/all/src/main/java/io/opentelemetry/api/trace/DefaultTracer.java +++ b/api/all/src/main/java/io/opentelemetry/api/trace/DefaultTracer.java @@ -7,7 +7,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.internal.ApiUsageLogger; +import io.opentelemetry.common.ApiUsageLogger; import io.opentelemetry.context.Context; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; @@ -55,7 +55,7 @@ public Span startSpan() { @Override public NoopSpanBuilder setParent(Context context) { if (context == null) { - ApiUsageLogger.log("context is null"); + ApiUsageLogger.log(SpanBuilder.class, "setParent", "context is null"); return this; } spanContext = Span.fromContext(context).getSpanContext(); diff --git a/api/all/src/main/java/io/opentelemetry/api/trace/Span.java b/api/all/src/main/java/io/opentelemetry/api/trace/Span.java index 708bb1de077..6f2f4a1d71f 100644 --- a/api/all/src/main/java/io/opentelemetry/api/trace/Span.java +++ b/api/all/src/main/java/io/opentelemetry/api/trace/Span.java @@ -10,7 +10,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.internal.ApiUsageLogger; +import io.opentelemetry.common.ApiUsageLogger; import io.opentelemetry.context.Context; import io.opentelemetry.context.ImplicitContextKeyed; import java.time.Instant; @@ -43,7 +43,7 @@ static Span current() { */ static Span fromContext(Context context) { if (context == null) { - ApiUsageLogger.log("context is null"); + ApiUsageLogger.log(Span.class, "fromContext", "context is null"); return Span.getInvalid(); } Span span = context.get(SpanContextKey.KEY); @@ -57,7 +57,7 @@ static Span fromContext(Context context) { @Nullable static Span fromContextOrNull(Context context) { if (context == null) { - ApiUsageLogger.log("context is null"); + ApiUsageLogger.log(Span.class, "fromContextOrNull", "context is null"); return null; } return context.get(SpanContextKey.KEY); @@ -78,7 +78,7 @@ static Span getInvalid() { */ static Span wrap(SpanContext spanContext) { if (spanContext == null) { - ApiUsageLogger.log("context is null"); + ApiUsageLogger.log(Span.class, "wrap", "spanContext is null"); return getInvalid(); } return PropagatedSpan.create(spanContext); diff --git a/api/all/src/main/java/io/opentelemetry/api/trace/SpanId.java b/api/all/src/main/java/io/opentelemetry/api/trace/SpanId.java index 0076974bb1c..f202418ccdb 100644 --- a/api/all/src/main/java/io/opentelemetry/api/trace/SpanId.java +++ b/api/all/src/main/java/io/opentelemetry/api/trace/SpanId.java @@ -5,9 +5,9 @@ package io.opentelemetry.api.trace; -import io.opentelemetry.api.internal.ApiUsageLogger; import io.opentelemetry.api.internal.OtelEncodingUtils; import io.opentelemetry.api.internal.TemporaryBuffers; +import io.opentelemetry.common.ApiUsageLogger; import javax.annotation.concurrent.Immutable; /** @@ -73,7 +73,7 @@ public static boolean isValid(CharSequence spanId) { */ public static String fromBytes(byte[] spanIdBytes) { if (spanIdBytes == null || spanIdBytes.length < BYTES_LENGTH) { - ApiUsageLogger.log("spanIdBytes is null or too short"); + ApiUsageLogger.log(SpanId.class, "fromBytes", "spanIdBytes is null or too short"); return INVALID; } char[] result = TemporaryBuffers.chars(HEX_LENGTH); diff --git a/api/all/src/main/java/io/opentelemetry/api/trace/TraceId.java b/api/all/src/main/java/io/opentelemetry/api/trace/TraceId.java index 5be08017f7b..d79d15a194f 100644 --- a/api/all/src/main/java/io/opentelemetry/api/trace/TraceId.java +++ b/api/all/src/main/java/io/opentelemetry/api/trace/TraceId.java @@ -5,9 +5,9 @@ package io.opentelemetry.api.trace; -import io.opentelemetry.api.internal.ApiUsageLogger; import io.opentelemetry.api.internal.OtelEncodingUtils; import io.opentelemetry.api.internal.TemporaryBuffers; +import io.opentelemetry.common.ApiUsageLogger; import javax.annotation.concurrent.Immutable; /** @@ -77,7 +77,7 @@ public static boolean isValid(CharSequence traceId) { */ public static String fromBytes(byte[] traceIdBytes) { if (traceIdBytes == null || traceIdBytes.length < BYTES_LENGTH) { - ApiUsageLogger.log("traceIdBytes is null or too short"); + ApiUsageLogger.log(TraceId.class, "fromBytes", "traceIdBytes is null or too short"); return INVALID; } char[] result = TemporaryBuffers.chars(HEX_LENGTH); diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedDefaultTracer.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedDefaultTracer.java index 79aaac8bed8..d117f5b6eeb 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedDefaultTracer.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedDefaultTracer.java @@ -8,11 +8,12 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.incubator.propagation.ExtendedContextPropagators; -import io.opentelemetry.api.internal.ApiUsageLogger; import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.common.ApiUsageLogger; import io.opentelemetry.context.Context; import io.opentelemetry.context.propagation.ContextPropagators; import java.util.Map; @@ -63,7 +64,7 @@ public Span startSpan() { @Override public NoopSpanBuilder setParent(Context context) { if (context == null) { - ApiUsageLogger.log("context is null"); + ApiUsageLogger.log(SpanBuilder.class, "setParent", "context is null"); return this; } spanContext = Span.fromContext(context).getSpanContext(); diff --git a/common/src/main/java/io/opentelemetry/common/ApiUsageLogger.java b/common/src/main/java/io/opentelemetry/common/ApiUsageLogger.java new file mode 100644 index 00000000000..ad6ba158398 --- /dev/null +++ b/common/src/main/java/io/opentelemetry/common/ApiUsageLogger.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.common; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A utility for logging API misuse, allowing operators to diagnose invalid usage with a single + * logging configuration entry. + * + *

Logs at {@link Level#FINEST} by default, so messages are silent in production unless + * explicitly enabled. Each log record includes a {@link Throwable} to make the offending call site + * visible in the stack trace without requiring the exception to be thrown. + * + *

To investigate API misuse, configure the logger named {@code io.opentelemetry.usage} at {@link + * Level#FINEST} in development, or periodically in staging/production. + * + *

This class is public for use by OpenTelemetry component authors. It is not intended for use by + * application developers. + */ +public final class ApiUsageLogger { + + /** The logger name used for all API-misuse diagnostics. */ + private static final Logger LOGGER = Logger.getLogger("io.opentelemetry.usage"); + + /** + * Log a misuse of {@code apiClass#methodName} with the given {@code message}. + * + *

Logs at {@link Level#FINEST} and includes a stack trace. + * + * @param apiClass the public API class where the misuse occurred + * @param methodName the name of the method where the misuse occurred + * @param message a brief description of the problem + */ + public static void log(Class apiClass, String methodName, String message) { + log(apiClass, methodName, message, Level.FINEST); + } + + // Visible for testing + static void log(Class apiClass, String methodName, String message, Level level) { + if (LOGGER.isLoggable(level)) { + LOGGER.log( + level, apiClass.getSimpleName() + "." + methodName + "(): " + message, new Throwable()); + } + } + + private ApiUsageLogger() {} +} diff --git a/api/all/src/test/java/io/opentelemetry/api/internal/ApiUsageLoggerTest.java b/common/src/test/java/io/opentelemetry/common/ApiUsageLoggerTest.java similarity index 57% rename from api/all/src/test/java/io/opentelemetry/api/internal/ApiUsageLoggerTest.java rename to common/src/test/java/io/opentelemetry/common/ApiUsageLoggerTest.java index ee284f13406..dd463d48aaf 100644 --- a/api/all/src/test/java/io/opentelemetry/api/internal/ApiUsageLoggerTest.java +++ b/common/src/test/java/io/opentelemetry/common/ApiUsageLoggerTest.java @@ -3,24 +3,23 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.api.internal; - -import static java.util.logging.Level.WARNING; +package io.opentelemetry.common; import io.github.netmikey.logunit.api.LogCapturer; import io.opentelemetry.internal.testing.slf4j.SuppressLogger; +import java.util.logging.Level; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -@SuppressLogger(ApiUsageLogger.class) +@SuppressLogger(loggerName = "io.opentelemetry.usage") class ApiUsageLoggerTest { @RegisterExtension - LogCapturer apiUsageLogs = LogCapturer.create().captureForLogger(ApiUsageLogger.class.getName()); + LogCapturer apiUsageLogs = LogCapturer.create().captureForLogger("io.opentelemetry.usage"); @Test void log() { - ApiUsageLogger.log("thing", WARNING); - apiUsageLogs.assertContains("thing"); + ApiUsageLogger.log(ApiUsageLoggerTest.class, "log", "thing went wrong", Level.WARNING); + apiUsageLogs.assertContains("ApiUsageLoggerTest.log(): thing went wrong"); } } diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-common.txt b/docs/apidiffs/current_vs_latest/opentelemetry-common.txt index 8bc557e479f..4367422e5eb 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-common.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-common.txt @@ -1,2 +1,5 @@ Comparing source compatibility of opentelemetry-common-1.62.0-SNAPSHOT.jar against opentelemetry-common-1.61.0.jar -No changes. \ No newline at end of file ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.common.ApiUsageLogger (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) STATIC(+) void log(java.lang.Class, java.lang.String, java.lang.String) diff --git a/docs/knowledge/README.md b/docs/knowledge/README.md index 0d5cbb0ef4c..d1e23f4e8e1 100644 --- a/docs/knowledge/README.md +++ b/docs/knowledge/README.md @@ -15,7 +15,7 @@ is the signal. | --- | --- | | [build.md](build.md) | Always — build requirements and common tasks | | [general-patterns.md](general-patterns.md) | Always — style, nullability, visibility, AutoValue, locking, logging | -| [api-stability.md](api-stability.md) | Public API additions, removals, renames, or deprecations; stable vs alpha compatibility | +| [api-design.md](api-design.md) | Public API additions, removals, renames, deprecations, or implementations; null guards; stable vs alpha compatibility | | [gradle-conventions.md](gradle-conventions.md) | `build.gradle.kts` or `settings.gradle.kts` changes; new modules | | [testing-patterns.md](testing-patterns.md) | Test files in scope — assertions, test utilities, test suites | | [other-tasks.md](other-tasks.md) | Dev environment setup, benchmarks, composite builds, OTLP protobuf updates | diff --git a/docs/knowledge/api-stability.md b/docs/knowledge/api-design.md similarity index 57% rename from docs/knowledge/api-stability.md rename to docs/knowledge/api-design.md index f8c3f8bcd10..a610c474065 100644 --- a/docs/knowledge/api-stability.md +++ b/docs/knowledge/api-design.md @@ -1,4 +1,4 @@ -# API Stability and Breaking Changes +# API Design See [VERSIONING.md](../../VERSIONING.md) for the full versioning and compatibility policy. @@ -54,6 +54,60 @@ a human-readable diff to `docs/apidiffs/current_vs_latest/.txt`. - The diff files are committed to the repo. Include any changes to them in your PR. - Run `./gradlew jApiCmp` to regenerate diffs after API changes. +## Null guards + +`@Nullable` annotations (see [general-patterns.md](general-patterns.md)) and +[NullAway](https://github.com/uber/NullAway) enforce null contracts at build time within this +repo. At runtime there is no such guarantee — callers in other +codebases can pass `null` regardless of annotations. Add null guards only at **public API entry +points**; once inside the implementation, trust NullAway. + +### Configuration-time boundaries (SDK builders, provider factories) + +Fail fast with `Objects.requireNonNull`: + +```java +public SdkTracerProviderBuilder setResource(Resource resource) { + Objects.requireNonNull(resource, "resource"); + this.resource = resource; + return this; +} +``` + +These APIs are called once during startup, so a hard failure surfaces the bug immediately and +unambiguously. + +### Runtime / instrumentation-time boundaries (Span methods, metric recordings, log builders) + +Do **not** throw. Log the violation via +[`ApiUsageLogger`](../../common/src/main/java/io/opentelemetry/common/ApiUsageLogger.java) — +which logs at `FINEST` with a stack trace so the offending call site is visible — then degrade +gracefully (return `this`, an empty/noop result, or substitute a safe default such as +`Attributes.empty()` or `Context.current()`): + +```java +@Override +public Span addEvent(String name) { + if (name == null) { + ApiUsageLogger.log(Span.class, "addEvent", "name is null"); + return this; + } + // ... normal implementation +} +``` + +The class and method arguments identify the problem immediately in the log message without +requiring stack trace analysis. `FINEST` is silent by default, so there is no production noise. +To investigate misuse, enable the logger named `io.opentelemetry.usage` at `FINEST` in +development, or periodically in staging/production. Check each argument once, at the first +public entry point — internal methods called by that entry point do not need to re-validate. + +### Where to implement guards + +Add guards in the concrete implementation class, or in an existing `default` interface method +that would otherwise NPE. Do **not** add new `default` methods to interfaces solely for null +safety — that expands the interface surface without a functional benefit. + ## Stable vs alpha modules Artifacts without an `-alpha` version suffix are **stable**. Artifacts with `-alpha` have no diff --git a/docs/knowledge/general-patterns.md b/docs/knowledge/general-patterns.md index 92f6ff0688f..51a29a1e20b 100644 --- a/docs/knowledge/general-patterns.md +++ b/docs/knowledge/general-patterns.md @@ -10,6 +10,8 @@ All arguments and members are treated as non-null by default. Annotate with `@Nu - **Return types**: annotate only if the method actually returns `null`. A non-null implementation of a `@Nullable`-declared interface method should omit the annotation — it is more precise. +For null guard behavior at public API boundaries, see [api-design.md](api-design.md). + ## API consistency The project aims to provide a consistent experience across all public APIs. When designing new From 922d71efa8bc74f6dc6501bb1cf82641c36e2438 Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:23:44 -0500 Subject: [PATCH 2/7] add logNullParam --- .../io/opentelemetry/common/ApiUsageLogger.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/common/src/main/java/io/opentelemetry/common/ApiUsageLogger.java b/common/src/main/java/io/opentelemetry/common/ApiUsageLogger.java index ad6ba158398..f191c2da0e0 100644 --- a/common/src/main/java/io/opentelemetry/common/ApiUsageLogger.java +++ b/common/src/main/java/io/opentelemetry/common/ApiUsageLogger.java @@ -27,6 +27,20 @@ public final class ApiUsageLogger { /** The logger name used for all API-misuse diagnostics. */ private static final Logger LOGGER = Logger.getLogger("io.opentelemetry.usage"); + /** + * Log that {@code paramName} was null in {@code apiClass#methodName}. + * + *

Convenience overload of {@link #log(Class, String, String)} for the common case of a null + * parameter that should not be null. + * + * @param apiClass the public API class where the misuse occurred + * @param methodName the name of the method where the misuse occurred + * @param paramName the name of the parameter that was null + */ + public static void logNullParam(Class apiClass, String methodName, String paramName) { + log(apiClass, methodName, paramName + " is null"); + } + /** * Log a misuse of {@code apiClass#methodName} with the given {@code message}. * From eef3bf32b5675fc9cedb936096927ab868e1432f Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:25:58 -0500 Subject: [PATCH 3/7] Add logNullParam helper method --- .../opentelemetry/api/baggage/ImmutableEntryMetadata.java | 2 +- .../main/java/io/opentelemetry/api/trace/DefaultTracer.java | 2 +- api/all/src/main/java/io/opentelemetry/api/trace/Span.java | 6 +++--- .../api/incubator/trace/ExtendedDefaultTracer.java | 2 +- docs/knowledge/api-design.md | 5 +++-- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableEntryMetadata.java b/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableEntryMetadata.java index 75d0de51ae0..57b2514d553 100644 --- a/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableEntryMetadata.java +++ b/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableEntryMetadata.java @@ -25,7 +25,7 @@ abstract class ImmutableEntryMetadata implements BaggageEntryMetadata { */ static ImmutableEntryMetadata create(String metadata) { if (metadata == null) { - ApiUsageLogger.log(BaggageEntryMetadata.class, "create", "metadata is null"); + ApiUsageLogger.logNullParam(BaggageEntryMetadata.class, "create", "metadata"); return EMPTY; } return new AutoValue_ImmutableEntryMetadata(metadata); diff --git a/api/all/src/main/java/io/opentelemetry/api/trace/DefaultTracer.java b/api/all/src/main/java/io/opentelemetry/api/trace/DefaultTracer.java index 6b75f1a2bfa..c5fa3390bb0 100644 --- a/api/all/src/main/java/io/opentelemetry/api/trace/DefaultTracer.java +++ b/api/all/src/main/java/io/opentelemetry/api/trace/DefaultTracer.java @@ -55,7 +55,7 @@ public Span startSpan() { @Override public NoopSpanBuilder setParent(Context context) { if (context == null) { - ApiUsageLogger.log(SpanBuilder.class, "setParent", "context is null"); + ApiUsageLogger.logNullParam(SpanBuilder.class, "setParent", "context"); return this; } spanContext = Span.fromContext(context).getSpanContext(); diff --git a/api/all/src/main/java/io/opentelemetry/api/trace/Span.java b/api/all/src/main/java/io/opentelemetry/api/trace/Span.java index 6f2f4a1d71f..42828494181 100644 --- a/api/all/src/main/java/io/opentelemetry/api/trace/Span.java +++ b/api/all/src/main/java/io/opentelemetry/api/trace/Span.java @@ -43,7 +43,7 @@ static Span current() { */ static Span fromContext(Context context) { if (context == null) { - ApiUsageLogger.log(Span.class, "fromContext", "context is null"); + ApiUsageLogger.logNullParam(Span.class, "fromContext", "context"); return Span.getInvalid(); } Span span = context.get(SpanContextKey.KEY); @@ -57,7 +57,7 @@ static Span fromContext(Context context) { @Nullable static Span fromContextOrNull(Context context) { if (context == null) { - ApiUsageLogger.log(Span.class, "fromContextOrNull", "context is null"); + ApiUsageLogger.logNullParam(Span.class, "fromContextOrNull", "context"); return null; } return context.get(SpanContextKey.KEY); @@ -78,7 +78,7 @@ static Span getInvalid() { */ static Span wrap(SpanContext spanContext) { if (spanContext == null) { - ApiUsageLogger.log(Span.class, "wrap", "spanContext is null"); + ApiUsageLogger.logNullParam(Span.class, "wrap", "spanContext"); return getInvalid(); } return PropagatedSpan.create(spanContext); diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedDefaultTracer.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedDefaultTracer.java index d117f5b6eeb..b5841d9911b 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedDefaultTracer.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedDefaultTracer.java @@ -64,7 +64,7 @@ public Span startSpan() { @Override public NoopSpanBuilder setParent(Context context) { if (context == null) { - ApiUsageLogger.log(SpanBuilder.class, "setParent", "context is null"); + ApiUsageLogger.logNullParam(SpanBuilder.class, "setParent", "context"); return this; } spanContext = Span.fromContext(context).getSpanContext(); diff --git a/docs/knowledge/api-design.md b/docs/knowledge/api-design.md index a610c474065..c6552dcadbd 100644 --- a/docs/knowledge/api-design.md +++ b/docs/knowledge/api-design.md @@ -89,7 +89,7 @@ gracefully (return `this`, an empty/noop result, or substitute a safe default su @Override public Span addEvent(String name) { if (name == null) { - ApiUsageLogger.log(Span.class, "addEvent", "name is null"); + ApiUsageLogger.logNullParam(Span.class, "addEvent", "name"); return this; } // ... normal implementation @@ -97,7 +97,8 @@ public Span addEvent(String name) { ``` The class and method arguments identify the problem immediately in the log message without -requiring stack trace analysis. `FINEST` is silent by default, so there is no production noise. +requiring stack trace analysis. Use `ApiUsageLogger.log(...)` directly when the message is not +simply "X is null" (e.g. `"spanIdBytes is null or too short"`). `FINEST` is silent by default, so there is no production noise. To investigate misuse, enable the logger named `io.opentelemetry.usage` at `FINEST` in development, or periodically in staging/production. Check each argument once, at the first public entry point — internal methods called by that entry point do not need to re-validate. From 807b0b146f2546659598041ee983726b8b644959 Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Wed, 22 Apr 2026 20:02:27 -0500 Subject: [PATCH 4/7] guidance for sdk extension interfaces --- docs/knowledge/api-design.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/knowledge/api-design.md b/docs/knowledge/api-design.md index c6552dcadbd..ffe168089c2 100644 --- a/docs/knowledge/api-design.md +++ b/docs/knowledge/api-design.md @@ -103,6 +103,19 @@ To investigate misuse, enable the logger named `io.opentelemetry.usage` at `FINE development, or periodically in staging/production. Check each argument once, at the first public entry point — internal methods called by that entry point do not need to re-validate. +### SDK extension interfaces and SPIs + +These interfaces are called by the SDK, not directly by application developers. +Examples include `Sampler`, `SpanExporter`, `SpanProcessor`, `LogRecordExporter`, +`MetricExporter`, `MetricReader`, `ComponentProvider`, and SPI interfaces such as those in +`sdk-extensions/autoconfigure-spi` (`ResourceProvider`, `AutoConfigurationCustomizerProvider`, +etc.), `HttpSenderProvider`, and `ContextStorageProvider`. + +Because the SDK is NullAway-verified, a null argument here indicates a bug in the SDK itself, +not misuse by an application developer. Use `Objects.requireNonNull` — a hard failure surfaces +the bug immediately and unambiguously, which is preferable to silent degradation that would +mask the underlying SDK defect. + ### Where to implement guards Add guards in the concrete implementation class, or in an existing `default` interface method From a89f825f4f3ba215b38eff91fd37a850c81884d1 Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Wed, 22 Apr 2026 20:22:05 -0500 Subject: [PATCH 5/7] japicmp --- docs/apidiffs/current_vs_latest/opentelemetry-common.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-common.txt b/docs/apidiffs/current_vs_latest/opentelemetry-common.txt index 4367422e5eb..c987ac19a2b 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-common.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-common.txt @@ -3,3 +3,4 @@ Comparing source compatibility of opentelemetry-common-1.62.0-SNAPSHOT.jar again +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. +++ NEW SUPERCLASS: java.lang.Object +++ NEW METHOD: PUBLIC(+) STATIC(+) void log(java.lang.Class, java.lang.String, java.lang.String) + +++ NEW METHOD: PUBLIC(+) STATIC(+) void logNullParam(java.lang.Class, java.lang.String, java.lang.String) From a90a68266d36ae1f86c1ccdc50efb2e51e326932 Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:47:42 -0500 Subject: [PATCH 6/7] Move to *.impl.* package, add warning first time usage issue occurs --- .../api/baggage/ImmutableEntryMetadata.java | 2 +- .../api/trace/DefaultTracer.java | 2 +- .../java/io/opentelemetry/api/trace/Span.java | 2 +- .../io/opentelemetry/api/trace/SpanId.java | 4 +- .../io/opentelemetry/api/trace/TraceId.java | 4 +- .../trace/ExtendedDefaultTracer.java | 2 +- .../common/{ => impl}/ApiUsageLogger.java | 39 +++++++------ .../common/ApiUsageLoggerTest.java | 25 --------- .../common/impl/ApiUsageLoggerTest.java | 55 +++++++++++++++++++ .../opentelemetry-common.txt | 2 +- 10 files changed, 87 insertions(+), 50 deletions(-) rename common/src/main/java/io/opentelemetry/common/{ => impl}/ApiUsageLogger.java (51%) delete mode 100644 common/src/test/java/io/opentelemetry/common/ApiUsageLoggerTest.java create mode 100644 common/src/test/java/io/opentelemetry/common/impl/ApiUsageLoggerTest.java diff --git a/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableEntryMetadata.java b/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableEntryMetadata.java index 57b2514d553..8225ff87dc9 100644 --- a/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableEntryMetadata.java +++ b/api/all/src/main/java/io/opentelemetry/api/baggage/ImmutableEntryMetadata.java @@ -6,7 +6,7 @@ package io.opentelemetry.api.baggage; import com.google.auto.value.AutoValue; -import io.opentelemetry.common.ApiUsageLogger; +import io.opentelemetry.common.impl.ApiUsageLogger; import javax.annotation.concurrent.Immutable; @Immutable diff --git a/api/all/src/main/java/io/opentelemetry/api/trace/DefaultTracer.java b/api/all/src/main/java/io/opentelemetry/api/trace/DefaultTracer.java index c5fa3390bb0..6b9cff09e7b 100644 --- a/api/all/src/main/java/io/opentelemetry/api/trace/DefaultTracer.java +++ b/api/all/src/main/java/io/opentelemetry/api/trace/DefaultTracer.java @@ -7,7 +7,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.common.ApiUsageLogger; +import io.opentelemetry.common.impl.ApiUsageLogger; import io.opentelemetry.context.Context; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; diff --git a/api/all/src/main/java/io/opentelemetry/api/trace/Span.java b/api/all/src/main/java/io/opentelemetry/api/trace/Span.java index 42828494181..c60f7225af0 100644 --- a/api/all/src/main/java/io/opentelemetry/api/trace/Span.java +++ b/api/all/src/main/java/io/opentelemetry/api/trace/Span.java @@ -10,7 +10,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.common.ApiUsageLogger; +import io.opentelemetry.common.impl.ApiUsageLogger; import io.opentelemetry.context.Context; import io.opentelemetry.context.ImplicitContextKeyed; import java.time.Instant; diff --git a/api/all/src/main/java/io/opentelemetry/api/trace/SpanId.java b/api/all/src/main/java/io/opentelemetry/api/trace/SpanId.java index f202418ccdb..5fca80f2403 100644 --- a/api/all/src/main/java/io/opentelemetry/api/trace/SpanId.java +++ b/api/all/src/main/java/io/opentelemetry/api/trace/SpanId.java @@ -7,7 +7,7 @@ import io.opentelemetry.api.internal.OtelEncodingUtils; import io.opentelemetry.api.internal.TemporaryBuffers; -import io.opentelemetry.common.ApiUsageLogger; +import io.opentelemetry.common.impl.ApiUsageLogger; import javax.annotation.concurrent.Immutable; /** @@ -73,7 +73,7 @@ public static boolean isValid(CharSequence spanId) { */ public static String fromBytes(byte[] spanIdBytes) { if (spanIdBytes == null || spanIdBytes.length < BYTES_LENGTH) { - ApiUsageLogger.log(SpanId.class, "fromBytes", "spanIdBytes is null or too short"); + ApiUsageLogger.logUsageIssue(SpanId.class, "fromBytes", "spanIdBytes is null or too short"); return INVALID; } char[] result = TemporaryBuffers.chars(HEX_LENGTH); diff --git a/api/all/src/main/java/io/opentelemetry/api/trace/TraceId.java b/api/all/src/main/java/io/opentelemetry/api/trace/TraceId.java index d79d15a194f..3ee77848d75 100644 --- a/api/all/src/main/java/io/opentelemetry/api/trace/TraceId.java +++ b/api/all/src/main/java/io/opentelemetry/api/trace/TraceId.java @@ -7,7 +7,7 @@ import io.opentelemetry.api.internal.OtelEncodingUtils; import io.opentelemetry.api.internal.TemporaryBuffers; -import io.opentelemetry.common.ApiUsageLogger; +import io.opentelemetry.common.impl.ApiUsageLogger; import javax.annotation.concurrent.Immutable; /** @@ -77,7 +77,7 @@ public static boolean isValid(CharSequence traceId) { */ public static String fromBytes(byte[] traceIdBytes) { if (traceIdBytes == null || traceIdBytes.length < BYTES_LENGTH) { - ApiUsageLogger.log(TraceId.class, "fromBytes", "traceIdBytes is null or too short"); + ApiUsageLogger.logUsageIssue(TraceId.class, "fromBytes", "traceIdBytes is null or too short"); return INVALID; } char[] result = TemporaryBuffers.chars(HEX_LENGTH); diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedDefaultTracer.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedDefaultTracer.java index b5841d9911b..40d72caba86 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedDefaultTracer.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedDefaultTracer.java @@ -13,7 +13,7 @@ import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.Tracer; -import io.opentelemetry.common.ApiUsageLogger; +import io.opentelemetry.common.impl.ApiUsageLogger; import io.opentelemetry.context.Context; import io.opentelemetry.context.propagation.ContextPropagators; import java.util.Map; diff --git a/common/src/main/java/io/opentelemetry/common/ApiUsageLogger.java b/common/src/main/java/io/opentelemetry/common/impl/ApiUsageLogger.java similarity index 51% rename from common/src/main/java/io/opentelemetry/common/ApiUsageLogger.java rename to common/src/main/java/io/opentelemetry/common/impl/ApiUsageLogger.java index f191c2da0e0..740c4d850f8 100644 --- a/common/src/main/java/io/opentelemetry/common/ApiUsageLogger.java +++ b/common/src/main/java/io/opentelemetry/common/impl/ApiUsageLogger.java @@ -3,8 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.common; +package io.opentelemetry.common.impl; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; @@ -16,29 +17,34 @@ * explicitly enabled. Each log record includes a {@link Throwable} to make the offending call site * visible in the stack trace without requiring the exception to be thrown. * - *

To investigate API misuse, configure the logger named {@code io.opentelemetry.usage} at {@link - * Level#FINEST} in development, or periodically in staging/production. + *

The first time any API misuse is detected, a one-time {@link Level#WARNING} is emitted to the + * {@code io.opentelemetry.usage} logger. This warning is visible under default logging + * configuration and signals that API misuse is occurring. To see the full details of every misuse + * event (message and stack trace), configure the {@code io.opentelemetry.usage} logger at {@link + * Level#FINEST}. * - *

This class is public for use by OpenTelemetry component authors. It is not intended for use by - * application developers. + *

This class is not intended for use by application developers. Its API is stable and will not + * be changed or removed in a backwards-incompatible manner. */ public final class ApiUsageLogger { /** The logger name used for all API-misuse diagnostics. */ private static final Logger LOGGER = Logger.getLogger("io.opentelemetry.usage"); + private static final AtomicBoolean WARN_ONCE = new AtomicBoolean(); + /** * Log that {@code paramName} was null in {@code apiClass#methodName}. * - *

Convenience overload of {@link #log(Class, String, String)} for the common case of a null - * parameter that should not be null. + *

Convenience overload of {@link #logUsageIssue(Class, String, String)} for the common case of + * a null parameter that should not be null. * * @param apiClass the public API class where the misuse occurred * @param methodName the name of the method where the misuse occurred * @param paramName the name of the parameter that was null */ public static void logNullParam(Class apiClass, String methodName, String paramName) { - log(apiClass, methodName, paramName + " is null"); + logUsageIssue(apiClass, methodName, paramName + " is null"); } /** @@ -50,15 +56,16 @@ public static void logNullParam(Class apiClass, String methodName, String par * @param methodName the name of the method where the misuse occurred * @param message a brief description of the problem */ - public static void log(Class apiClass, String methodName, String message) { - log(apiClass, methodName, message, Level.FINEST); - } - - // Visible for testing - static void log(Class apiClass, String methodName, String message, Level level) { - if (LOGGER.isLoggable(level)) { + public static void logUsageIssue(Class apiClass, String methodName, String message) { + if (WARN_ONCE.compareAndSet(false, true)) { + LOGGER.warning( + "OpenTelemetry API usage issue detected. To see more details, enable FINEST logging for io.opentelemetry.usage. Stacktraces are includes to identify the offending call site."); + } + if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.log( - level, apiClass.getSimpleName() + "." + methodName + "(): " + message, new Throwable()); + Level.FINEST, + apiClass.getSimpleName() + "." + methodName + "(): " + message, + new Throwable()); } } diff --git a/common/src/test/java/io/opentelemetry/common/ApiUsageLoggerTest.java b/common/src/test/java/io/opentelemetry/common/ApiUsageLoggerTest.java deleted file mode 100644 index dd463d48aaf..00000000000 --- a/common/src/test/java/io/opentelemetry/common/ApiUsageLoggerTest.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.common; - -import io.github.netmikey.logunit.api.LogCapturer; -import io.opentelemetry.internal.testing.slf4j.SuppressLogger; -import java.util.logging.Level; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -@SuppressLogger(loggerName = "io.opentelemetry.usage") -class ApiUsageLoggerTest { - - @RegisterExtension - LogCapturer apiUsageLogs = LogCapturer.create().captureForLogger("io.opentelemetry.usage"); - - @Test - void log() { - ApiUsageLogger.log(ApiUsageLoggerTest.class, "log", "thing went wrong", Level.WARNING); - apiUsageLogs.assertContains("ApiUsageLoggerTest.log(): thing went wrong"); - } -} diff --git a/common/src/test/java/io/opentelemetry/common/impl/ApiUsageLoggerTest.java b/common/src/test/java/io/opentelemetry/common/impl/ApiUsageLoggerTest.java new file mode 100644 index 00000000000..932f3ca0b18 --- /dev/null +++ b/common/src/test/java/io/opentelemetry/common/impl/ApiUsageLoggerTest.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.common.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.github.netmikey.logunit.api.LogCapturer; +import io.opentelemetry.internal.testing.slf4j.SuppressLogger; +import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.event.Level; + +@SuppressLogger(loggerName = "io.opentelemetry.usage") +class ApiUsageLoggerTest { + + @RegisterExtension + LogCapturer apiUsageLogs = + LogCapturer.create().captureForLogger("io.opentelemetry.usage", Level.TRACE); + + @BeforeEach + void resetWarnOnce() throws Exception { + Field warnOnce = ApiUsageLogger.class.getDeclaredField("WARN_ONCE"); + warnOnce.setAccessible(true); + ((AtomicBoolean) warnOnce.get(null)).set(false); + } + + @Test + void log() { + ApiUsageLogger.logUsageIssue(ApiUsageLoggerTest.class, "log", "thing went wrong"); + apiUsageLogs.assertContains("ApiUsageLoggerTest.log(): thing went wrong"); + } + + @Test + void logNullParam() { + ApiUsageLogger.logNullParam(ApiUsageLoggerTest.class, "logNullParam", "myParam"); + apiUsageLogs.assertContains("ApiUsageLoggerTest.logNullParam(): myParam is null"); + } + + @Test + void warnOnce() { + ApiUsageLogger.logUsageIssue(ApiUsageLoggerTest.class, "warnOnce", "first"); + ApiUsageLogger.logUsageIssue(ApiUsageLoggerTest.class, "warnOnce", "second"); + long count = + apiUsageLogs.getEvents().stream() + .filter(e -> e.getMessage().contains("OpenTelemetry API usage issue detected")) + .count(); + assertEquals(1, count); + } +} diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-common.txt b/docs/apidiffs/current_vs_latest/opentelemetry-common.txt index c987ac19a2b..0317fc04d22 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-common.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-common.txt @@ -1,5 +1,5 @@ Comparing source compatibility of opentelemetry-common-1.62.0-SNAPSHOT.jar against opentelemetry-common-1.61.0.jar -+++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.common.ApiUsageLogger (not serializable) ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.common.impl.ApiUsageLogger (not serializable) +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. +++ NEW SUPERCLASS: java.lang.Object +++ NEW METHOD: PUBLIC(+) STATIC(+) void log(java.lang.Class, java.lang.String, java.lang.String) From ae7548aa945469a5fc5c564770a5cf33c97ad2bd Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:55:12 -0500 Subject: [PATCH 7/7] update japicmp --- docs/apidiffs/current_vs_latest/opentelemetry-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-common.txt b/docs/apidiffs/current_vs_latest/opentelemetry-common.txt index 0317fc04d22..80201246f3b 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-common.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-common.txt @@ -2,5 +2,5 @@ Comparing source compatibility of opentelemetry-common-1.62.0-SNAPSHOT.jar again +++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.common.impl.ApiUsageLogger (not serializable) +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. +++ NEW SUPERCLASS: java.lang.Object - +++ NEW METHOD: PUBLIC(+) STATIC(+) void log(java.lang.Class, java.lang.String, java.lang.String) +++ NEW METHOD: PUBLIC(+) STATIC(+) void logNullParam(java.lang.Class, java.lang.String, java.lang.String) + +++ NEW METHOD: PUBLIC(+) STATIC(+) void logUsageIssue(java.lang.Class, java.lang.String, java.lang.String)