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/ 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)