Skip to content

No tracer provider in the OpenTelemetrySdk builder makes the application do a lot of work for spans that will be dropped #8740

Open

Description

@JnRouvignac

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

Activity

  1. jkwatson commented on Aug 24, 2026

    @jkwatson
    Contributor

    how about just using TracerProvider.noop()?

  2. JnRouvignac commented on Aug 25, 2026

    @JnRouvignac
    Author

    how about just using TracerProvider.noop()?

    I tried that, but it does not work because these are different types:

    • TracerProvider.noop() returns a io.opentelemetry.api.trace.TracerProvider
    • while io.opentelemetry.sdk.OpenTelemetrySdkBuilder#setTracerProvider expects a io.opentelemetry.sdk.SdkTracerProvider
  3. jack-berg commented on Aug 25, 2026

    @jack-berg
    Member

    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:

        SdkTracerProviderBuilder builder = SdkTracerProvider.builder();
        SdkTracerProviderUtil.addTracerConfiguratorCondition(builder, ScopeConfiguratorBuilder.nameMatchesGlob("*"), TracerConfig.disabled());
    
        // Given
        OpenTelemetry openTelemetry = OpenTelemetrySdk.builder().setTracerProvider(builder.build()).build();
        // Same as OpenTelemetry openTelemetry = OpenTelemetrySdk.builder().build();
    
        // When
        Span span = openTelemetry.getTracer("test").spanBuilder("test-span").startSpan();
    
        // Then
        assertThat(span.isRecording()).isFalse();
    

    But an OpenTelemetrySdk instance with no SdkTracerProvider configured should have the behavior you discuss out of the box.

  4. JnRouvignac commented on Aug 25, 2026

    @JnRouvignac
    Author

    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.

  5. jkwatson commented on Aug 25, 2026

    @jkwatson
    Contributor

    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.

    A tracer provider with a no-op exporter will still generate spans, whose ids will get injected into logs, as appropriate.

  6. JnRouvignac commented on Aug 26, 2026

    @JnRouvignac
    Author

    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.

    A tracer provider with a no-op exporter will still generate spans, whose ids will get injected into logs, as appropriate.

    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:

            assertThat(span.isRecording()).isFalse();
            assertThat(span.getSpanContext().isValid()).isTrue();
            assertThat(span.getSpanContext().isSampled()).isTrue();
  7. JnRouvignac commented on Aug 26, 2026

    @JnRouvignac
    Author

    You can get the behavior your desire using this experimental feature called scope config, which allows you to select/disable specific tracers:

        SdkTracerProviderBuilder builder = SdkTracerProvider.builder();
        SdkTracerProviderUtil.addTracerConfiguratorCondition(builder, ScopeConfiguratorBuilder.nameMatchesGlob("*"), TracerConfig.disabled());
    

    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!

  8. Debashismitra01 commented on Sep 3, 2026

    @Debashismitra01
    Contributor

    @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.

  9. added a commit that references this issue on Sep 21, 2026
  10. brunobat commented on Nov 3, 2026

    @brunobat
    Contributor

    @jack-berg, I think the fix for this issue might have introduced a breaking change.
    When we now set: otel.traces.exporter=none
    There will be:

    • No trace_id / span_id in logs
    • No Trace context propagated to downstream services
      I think we can achieve the same behavior by setting:
      otel.traces.sampler=always_off
      However, this will produce non recording spans.

    none should be different from no-op and probably have different configs.

  11. jack-berg commented on Nov 3, 2026

    @jack-berg
    Member

    Hmm I knew that this would change behavior, but considered it a bug fix.

    none should be different from no-op and probably have different configs.

    How should you specify no op vs. non-span context generating? As far as I can tell, the spec is ambiguous about:

    • How you indicate that a noop implementation should be used for TracerProvider, MeterProvider, LoggerProvider?OTEL_SDK_DISABLED=true disable all but how do you select for a specific signal?
    • What should happen if no processors (and therefore no exporters) are registered? Span creation spec doesn't mention this scenario. Tracer.enabled operation should return false when no processors are registered, which results in instrumentation skipping calling span start entirely in this case. So arguably, disabling span context creation when no span processors are registered is most consistent since if not, the existence of span context would depend on whether or not instrumentation calls Tracer.enabled.

    Is there a workaround? Are non recording spans a problem? It seems accurate in this case.

  12. brunobat commented on Nov 3, 2026

    @brunobat
    Contributor
     How should you specify no op vs. non-span context generating?
    

    The way we can do it in Quarkus is through *.trace.enabled and *.exporter.*.enabled configs 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=none should 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.exporter and keep the none as before but introduce a noop (or some other name) that would not generate any spans, like the behavior in current main.

  13. jack-berg commented on Nov 3, 2026

    @jack-berg
    Member

    otel.traces.exporter=none should only apply to exporters, leaving the remaining tracer functionality enabled.

    Problem is:

    1. SdkMeterProvider and SdkLoggerProvider already behaved like this, so SdkTracerProvider was the exception, which I why I treated it as a bug fix despite the behavior change.
    2. Not everyone agrees that a tracer provider should keep producing trace context when there is nothing configured to process the data.

    Either we add a new tracer config or we can chose a less intrusive option like use the otel.traces.exporter and keep the none as before but introduce a noop (or some other name) that would not generate any spans, like the behavior in current main.

    Attaching a noop behavior to otel.traces.exporter=none actually 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 to otel.traces.exporter=noop. This would instruct configuration to wire in a processor configured with a noop exporter, and trace context is still created. So:

    • Across all signals, if there is nothing configured to process the data, we configure a noop implementation.
    • Anything configured to process the data, even a simple span processor which calls a noop exporter, retains proper implementation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions