Repository navigation
Description
Describe the bug
When the tracer provider is not set in the OpenTelemetrySdk builder, spans claim they are recorded, but there is no exporter defined for them.
Therefore, despite appropriately guarding the attribute creation code with if (!span.isRecording()), an application can waste resources generating spans with attributes that will never be exported anyway.
Steps to reproduce
This reduced test case shows the problem:
@Test
public void shouldNotEmitSpansWhenTracingDisabledButLoggingEnabled2() {
// Given
OpenTelemetry openTelemetry = OpenTelemetrySdk.builder().setTracerProvider(null).build();
// Same as OpenTelemetry openTelemetry = OpenTelemetrySdk.builder().build();
// When
Span span = openTelemetry.getTracer("test").spanBuilder("test-span").startSpan();
// Then
assertThat(span.isRecording()).isFalse(); // Fails!
}Of course our setup code is a bit more complicated: we have a real logger provider, but no tracer provider, and we ended up passing null for the tracer provider.
What did you expect to see?
In the test above, the assertion assertThat(span.isRecording()).isFalse(); should pass. As a result, all the application span code would be a noop.
What did you see instead?
In the test above, the assertion assertThat(span.isRecording()).isFalse(); does not pass. As a result, all the application span code does a lot of useless work.
Workaround
First attempt:
Do the following instead:
SdkTracerProvider noopTracer = SdkTracerProvider.builder().setSampler(alwaysOff()).build();
OpenTelemetry openTelemetry = OpenTelemetrySdk.builder().setTracerProvider(noopTracer).build();But even that is not optimal, it will run through a lot of code on this line:
openTelemetry.getTracer("test").spanBuilder("test-span").startSpan();
I was hoping I could find a way to setSdkTracer#tracerEnabled to false, but I could not.
I wanted to override SdkTracerProviderBuilder#tracerConfiguratorBuilder, but there is no public API to do so either.
Second attempt:
In the end, the best workaround I found is the following:
SdkTracerProvider noopTracer = SdkTracerProvider.builder().build();
noopTracer.close();
OpenTelemetry openTelemetry = OpenTelemetrySdk.builder().setTracerProvider(noopTracer).build();This ensures we take one of the fast paths in io.opentelemetry.sdk.trace.SdkTracer#spanBuilder, which returns a NoopSpanBuilder, and then the call to io.opentelemetry.api.trace.SpanBuilder.startSpan does as little as possible, and the returned io.opentelemetry.api.trace.PropagatedSpan isRecording() method returns false.
Which what we want.
Conclusion:
To cut a long story short, the default is surprising, and making it to behave as noop required a surprising amount of work, and it is a bit "heavy". i.e. doing the "right thing" in this case is not exactly trivial.
What version and what artifacts are you using?
io.opentelemetry:opentelemetry-sdk-trace:1.62.0
Environment
Compiler: Amazon Correto 17.0.19
OS: Ubuntu 24.04.4 LTS
Additional context
n/a
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Activity
jkwatson commentedon Aug 24, 2026
how about just using
TracerProvider.noop()?JnRouvignac commentedon Aug 25, 2026
I tried that, but it does not work because these are different types:
TracerProvider.noop()returns aio.opentelemetry.api.trace.TracerProviderio.opentelemetry.sdk.OpenTelemetrySdkBuilder#setTracerProviderexpects aio.opentelemetry.sdk.SdkTracerProviderjack-berg commentedon Aug 25, 2026
Yeah I agree we can and should do better than this.
You can get the behavior your desire using this experimental feature called scope config, which allows you to select/disable specific tracers:
But an OpenTelemetrySdk instance with no SdkTracerProvider configured should have the behavior you discuss out of the box.
JnRouvignac commentedon Aug 25, 2026
Thanks I will try it out and report back.
The only thing I was wondering is: without tracing provider, the log lines won't get any traceId and spanId.
Is there a use case for having traceId and spanId on log lines but without exporting traces? In theory one could correlate log lines through distributed applications without incurring the higher cost of storing traces, and in effect I know some companies who did this successfully for years.
WDYT?
It's at least one use case worth keeping maybe.
jkwatson commentedon Aug 25, 2026
A tracer provider with a no-op exporter will still generate spans, whose ids will get injected into logs, as appropriate.
JnRouvignac commentedon Aug 26, 2026
Yes, in that case one should probably use an always-on sampler to get traceId / spanId on all the logs.
Then for that case, one should have:
JnRouvignac commentedon Aug 26, 2026
Duh! I should have read the javadoc 🤦
It solves the problem reported in the issue description indeed, but it's still quite a bit of code though.
Since this is an incubating API, we won't use it in for the moment.
SdkTracerProvider.close()is good enough for us right now.Also, I am not sure how to achieve #8740 (comment) though? But this is not really a use case I need right now, so I don't really need an answer here. It's more like somebody else's use case at the minute. 😄
Thank you!
Debashismitra01 commentedon Sep 3, 2026
@jack-berg @JnRouvignac Hi! I’d like to work on this issue if it’s still available. I’ve gone through the discussion and would be happy to investigate the current default tracer-provider behavior, add a regression test, and propose a minimal fix. Please let me know if there’s anything I should keep in mind before starting.
Update SdkTracerProviderTest.java
brunobat commentedon Nov 3, 2026
@jack-berg, I think the fix for this issue might have introduced a breaking change.
When we now set:
otel.traces.exporter=noneThere will be:
I think we can achieve the same behavior by setting:
otel.traces.sampler=always_offHowever, this will produce non recording spans.
noneshould be different from no-op and probably have different configs.jack-berg commentedon Nov 3, 2026
Hmm I knew that this would change behavior, but considered it a bug fix.
How should you specify no op vs. non-span context generating? As far as I can tell, the spec is ambiguous about:
Is there a workaround? Are non recording spans a problem? It seems accurate in this case.
brunobat commentedon Nov 3, 2026
The way we can do it in Quarkus is through
*.trace.enabledand*.exporter.*.enabledconfigs but that doesn't exist here in upstream.I think the spec says a disabled tracer should be no-op but I think we have no config to express that... This what the current main branch behavior achieves when setting
otel.traces.exporter=none.otel.traces.exporter=noneshould only apply to exporters, leaving the remaining tracer functionality enabled.#8815 effectively collapsed the tracer and exporter configs.
Either we add a new tracer config or we can chose a less intrusive option like use the
otel.traces.exporterand keep thenoneas before but introduce anoop(or some other name) that would not generate any spans, like the behavior in current main.jack-berg commentedon Nov 3, 2026
Problem is:
Attaching a noop behavior to
otel.traces.exporter=noneactually seems appropriate to me. If there is no exporter, then there is no processor and if there's no processor then there's no point in processing the data. Hence noop. But I understand the desire to have an escape hatch and keep producing trace context. Idea: We have a package private noop exporters built in like NoopSpanExporter. I wonder if we should attach a special behavior tootel.traces.exporter=noop. This would instruct configuration to wire in a processor configured with a noop exporter, and trace context is still created. So:brunobat commentedon Nov 4, 2026
A tracer has a second output channel that isn't the exporter. The
SpanContextinjected into outbound requests and into log records. This is what we just missed.Yes, but I'm not sure which behavior...
I think if we just set in
opentelemetry-java/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java
Line 59 in d9751e6
the code to
We end up with the following workflow:
instanceof)isEmpty())exporter=none[]exporter=otlp[BatchSpanProcessor]exporter=noop[the noop processor]Where
pentelemetry-sdk-extension-autoconfigure-1.67.0-sources.jar!/io/opentelemetry/sdk/autoconfigure/TracerProviderConfiguration.java:110should have something like:I think this is simple enough and covers all cases.
brunobat commentedon Nov 4, 2026
@jack-berg created this PR #9004 to illustrate the solution proposed in the previous comment.