Skip to content

Allow customizing OTel span status on error via a callback#1443

Open
philsttr wants to merge 1 commit into
micrometer-metrics:mainfrom
philsttr:error_status
Open

Allow customizing OTel span status on error via a callback#1443
philsttr wants to merge 1 commit into
micrometer-metrics:mainfrom
philsttr:error_status

Conversation

@philsttr

@philsttr philsttr commented Jun 15, 2026

Copy link
Copy Markdown

Problem

In the OpenTelemetry bridge, reporting an error on a span always sets the delegate span's status to ERROR. This happens in three places:

There is no hook to influence this. In practice, some exceptions are expected (e.g. a NotFoundException that maps to a 404) and I'd like for these to not cause the span to be marked as errored.

Change

Introduce SpanErrorStatusHandler, a @FunctionalInterface that decides the status to set on the OTel span for a given throwable:

@FunctionalInterface
public interface SpanErrorStatusHandler {

	SpanErrorStatusHandler DEFAULT = (error, span) -> {
		if (error.getMessage() == null) {
			span.setStatus(StatusCode.ERROR);
		}
		else {
			span.setStatus(StatusCode.ERROR, error.getMessage());
		}
	};

	void handle(Throwable error, io.opentelemetry.api.trace.Span span);
}

The DEFAULT implementation is the same as the behavior prior to this change.

The exception is still always recorded via Span.recordException(...); the handler is responsible only for the status. Leaving the status unset means the span is marked OK on end, which is how a handler treats an exception as expected.

The handler is configured on OtelTracer through a new constructor overload and threaded into every span the tracer creates (nextSpan, currentSpan, startScopedSpan, spanBuilder). For example, using a custom handler would look something like this:

OtelTracer tracer = new OtelTracer(otelTracer, currentTraceContext, publisher,
		baggageManager, (error, span) -> {
	if (error instanceof ExpectedException) {
		return; // leave status unset -> span ends OK
	}
	span.setStatus(StatusCode.ERROR, error.getMessage());
});

Backward compatibility

Fully backward compatible. OtelSpan, OtelScopedSpan and OtelSpanBuilder now carry the handler via new constructor/factory variants, but all existing constructors and factories default to SpanErrorStatusHandler.DEFAULT, which reproduces today's behavior exactly. No public API is removed or changed.

Routing all three call sites through the default also unifies a small existing inconsistency: OtelScopedSpan and OtelSpanBuilder previously passed a possibly-null message straight to setStatus(ERROR, message), while OtelSpan null-checked it. Both are equivalent in the OTel SDK, so this is a harmless consolidation.

Tests

Added SpanErrorStatusHandlerTests covering:

  • Default behavior unchanged: status ERROR with the throwable message, and the exception event still recorded.
  • A custom "expected exception" handler that leaves the status unset, asserting the span ends OK while the exception event is still recorded, verified across Span.error, ScopedSpan.error and Span.Builder.error.
  • A custom handler setting a different status description.

Out of scope

  • The generic Span.Builder API in micrometer-tracing-api is untouched; this is OTel-specific configuration only (no per-builder override).
  • The Brave bridge is untouched; it delegates status handling to Brave and does not set OTel status codes.
  • Downstream wiring (e.g. Spring Boot auto-configuration) is not part of this change; consumers can opt in by using the new OtelTracer constructor. Assuming this PR is merged, I might submit a follow-up Spring Boot PR to allow a custom SpanErrorStatusHandler to be auto-configured if a bean of that type is found.

@philsttr

philsttr commented Jun 15, 2026

Copy link
Copy Markdown
Author

circleci: build-jdk11 build error looks unrelated

Cannot find a Java installation on your machine (Linux 6.17.0-1013-aws amd64) matching: {languageVersion=11, vendor=any vendor, implementation=vendor-specific, nativeImageCapable=false}

Previously the OpenTelemetry bridge unconditionally set the delegate span's
status to ERROR whenever an error was reported, in three places:
OtelSpan.error(), OtelScopedSpan.error() and OtelSpanBuilder.start(). This left
no way to treat certain exceptions as expected and avoid marking the span as
errored.

Introduce SpanErrorStatusHandler, a functional interface that decides the
status to set on the OTel span for a given throwable. The exception is still
always recorded via Span.recordException(); the handler is responsible only for
the status. Its DEFAULT implementation reproduces the historical behavior
(always set ERROR, using the throwable message as the description when present),
so existing users see no change. A custom handler may, for example, leave the
status unset for expected exceptions, in which case the span is marked OK on
end, or set a custom description.

The handler is configured on OtelTracer via a new constructor overload and is
threaded into every span the tracer creates (nextSpan, currentSpan,
startScopedSpan, spanBuilder). OtelSpan, OtelScopedSpan and OtelSpanBuilder each
carry the handler through new constructor/factory variants; all existing
constructors and factories default to SpanErrorStatusHandler.DEFAULT, keeping
the change backward compatible. Routing all three call sites through the default
also unifies a minor inconsistency where the scoped span and builder passed a
possibly-null message directly to setStatus.

Add SpanErrorStatusHandlerTests covering the unchanged default behavior and a
custom "expected exception" handler across Span.error, ScopedSpan.error and
Span.Builder.error, plus a custom-description case.

Signed-off-by: Phil Clay <philsttr@users.noreply.github.com>
@philsttr

Copy link
Copy Markdown
Author

New circleci: build-jdk11 build error still looks unrelated

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant