Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
8 changes: 4 additions & 4 deletions api/all/src/main/java/io/opentelemetry/api/trace/Span.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
52 changes: 52 additions & 0 deletions common/src/main/java/io/opentelemetry/common/ApiUsageLogger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.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.
*
* <p>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.
*
* <p>To investigate API misuse, configure the logger named {@code io.opentelemetry.usage} at {@link
* Level#FINEST} in development, or periodically in staging/production.
*
* <p>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}.
*
* <p>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());
Comment thread
jack-berg marked this conversation as resolved.
Outdated
}
}

private ApiUsageLogger() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
5 changes: 4 additions & 1 deletion docs/apidiffs/current_vs_latest/opentelemetry-common.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
Comparing source compatibility of opentelemetry-common-1.62.0-SNAPSHOT.jar against opentelemetry-common-1.61.0.jar
No changes.
+++ 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)
2 changes: 1 addition & 1 deletion docs/knowledge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# API Stability and Breaking Changes
# API Design

See [VERSIONING.md](../../VERSIONING.md) for the full versioning and compatibility policy.

Expand Down Expand Up @@ -54,6 +54,60 @@ a human-readable diff to `docs/apidiffs/current_vs_latest/<artifact>.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
Expand Down
2 changes: 2 additions & 0 deletions docs/knowledge/general-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading