Repository navigation
Description
warnings.warn(..., stacklevel=<literal>) raised from inside an instrument's bootstrap() cannot reach the user, because the frame depth between the warn site and the user's bootstrapper.bootstrap() call is not constant across bootstrappers.
Measured
With is_otlp_grpc_exporter_installed patched False and an endpoint configured, calling bootstrapper.bootstrap() from user code:
| bootstrapper | warning attributed to |
|---|---|
FreeBootstrapper |
bootstrappers/base.py:127 |
FastAPIBootstrapper |
bootstrappers/fastapi_bootstrapper.py:118 |
Two different depths, neither of them the caller. The difference is FastAPIOpenTelemetryInstrument.bootstrap(), which adds a super().bootstrap() frame that the free path does not have. No single literal is correct for both.
This is the same defect class as #200 fixed for config construction — there the variable depth came from the __post_init__ MRO cascade, here it comes from the optional framework-subclass override — and the same conclusion applies: the depth is a property of the call chain, so no literal can name the user's frame.
Scope
Warn sites raised outside config construction, to be surveyed as a set rather than one at a time:
instruments/opentelemetry_instrument.py:199,211— the two missing-OTLP-exporter warnings. Confirmed wrong.bootstrappers/fastapi_bootstrapper.py:159— theswagger_pathdiffers fromdocs_urlwarning,stacklevel=2insideFastAPISwaggerInstrument.bootstrap(). Same shape; not yet measured.helpers/fastapi_helpers.py:28— theroot_pathallowlist warning, reached throughenable_offline_docsfrom an instrumentbootstrap(). Not yet measured.bootstrappers/base.py:42,93— both fire during bootstrapper construction rather than instrument bootstrap, so their depth is likely stable. Verify before touching.
Proposal
helpers/warn.py::warn_at_caller already walks up until it leaves lite_bootstrap, which is exactly what is needed — verified empirically: routing the gRPC warning through it lands on the user's own bootstrap() line for both bootstrappers. The only gap is that it takes no category, so InstrumentDependencyMissingWarning would be downgraded to UserWarning.
Add a category parameter to warn_at_caller, then route each confirmed-wrong site through it. Pin the result with an invariant test that asserts attribution from more than one bootstrapper, since a single-bootstrapper test cannot see the variable depth that causes this.
Docs
AGENTS.md § Code style currently states the rule this contradicts:
Warnings raised outside config construction keep their literal
stacklevel: their target is the bootstrapper's caller, not the user.
The intent there is right — these warnings should target whoever called bootstrap(), as distinct from whoever built the config. The mechanism is what fails. Rewrite the sentence in the same commit as the fix.
Note
Found while reviewing #198, which deliberately preserved the existing (wrong) attribution and only paid for the frame its extraction added. Nothing in #198 needs revisiting.
Activity
lesnik512 commentedon Sep 12, 2026
Duplicate of #202, which was filed first and is strictly more complete: it measures
fastapi_bootstrapper.py:159as well, establishes thatbase.py:42andbase.py:93are correct today rather than merely suspect, excludeshelpers/fastapi_helpers.py:28on the grounds that it fires inside a request handler with no user frame on the stack, and names the design decision this turns on — whether the target is thebootstrapper.bootstrap()call or the instrument.Filed while reviewing #198 without checking open issues first. Nothing here that #202 does not already say better.