Skip to content

Losing the set_tracer_provider race leaves an orphaned provider and a live exporter thread, silently #227

Description

@lesnik512

Affects every bootstrapper (FastAPI, Litestar, Free, and FastStream since #223), not one framework.

What happens

OpenTelemetryInstrument.bootstrap() builds a provider and installs it:

tracer_provider = TracerProvider(resource=self._build_resource(), sampler=config.opentelemetry_sampler)
set_tracer_provider(tracer_provider)
self._tracer_provider = tracer_provider
...
if config.opentelemetry_endpoint and (span_exporter := self._build_span_exporter()):
    tracer_provider.add_span_processor(BatchSpanProcessor(span_exporter))

The OTel SDK enforces set_tracer_provider as set-once per process. When an application installed its
own provider first, the call is refused (the SDK logs "Overriding of current TracerProvider is not
allowed", and _silence_otel_loggers() has just disabled that logger). self._tracer_provider is then
not the global one. Every bootstrapper reads get_tracer_provider() when wiring its middleware or
instrumentor, so tracing itself is fine and goes to the application's provider. The provider
lite-bootstrap built is what is left over.

Why it is not merely a wasted object

It carries the configured resource, sampler and exporter, and nothing will ever feed it a span.
BatchSpanProcessor starts its worker on construction:

threads before/after BatchSpanProcessor: 1 2
after shutdown: 1

So an OTLP-configured service that loses the race runs an idle exporter worker thread and an open
connection to the collector for the process lifetime, until teardown() calls shutdown(). With
opentelemetry_log_traces=True there is also an orphaned SimpleSpanProcessor.

None of this is reported. The one signal the SDK emits is suppressed by _silence_otel_loggers(),
which runs immediately before.

Proposal

A diagnostic, not a behaviour change:

set_tracer_provider(tracer_provider)
if get_tracer_provider() is not tracer_provider:
    warn_at_caller("a TracerProvider is already installed; the configured exporter, sampler and "
                   "resource will not be used")

Deliberately not proposed: skipping provider construction when one already exists. That would
silently discard an explicit opentelemetry_endpoint, which is worse than the leak.

The design cost, which is the real work here

The warning would fire on nearly every test in the suite, since the first test to bootstrap wins the
process global and all the others then lose it. It needs either a filter in tests/conftest.py or a
restructure of how the otel tests share the global. That decision is the substance of this issue; the
three-line diff is not.

Related

The set-once lifecycle is already documented in the OpenTelemetryInstrument docstring ("The supported
lifecycle is one OpenTelemetryInstrument per process"), but that covers bootstrapping twice. This is
the different case where the application owns the provider, which is legitimate and is exactly what
keeps the #223 fix from breaking existing FastStream users.

Activity

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

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions