An instrument is one observability or middleware concern (logging, tracing,
metrics, …). Each lives in its own file under lite_bootstrap/instruments/.
A bootstrapper owns a list of instrument instances and drives their lifecycle.
BaseInstrument[ConfigT] (lite_bootstrap/instruments/base.py) is a generic,
non-frozen @dataclasses.dataclass(kw_only=True, slots=True) holding a
single bootstrap_config: ConfigT. Subclasses implement:
bootstrap()/teardown()— lifecycle hooks, called in order / reverse by the bootstrapper.is_configured(cls, bootstrap_config) -> bool(classmethod) — return False when the user's config means this instrument should not run. Default: always True. Drives the silent-skip path.check_dependencies() -> bool(staticmethod) — return False when the optional package is absent. Default: always True.- class attributes
not_ready_messageandmissing_dependency_message— human-readable reasons surfaced in skip reporting and warnings.
One file per instrument:
logging_instrument.py— structlog setup (LoggingInstrument), skipped whenlogging_enabled=False. The Litestar subclass also owns Litestar'sLoggingMiddleware: it is off unlesslitestar_logging_middleware_enabledis set, and when on it logs request/response metadata only (never bodies, headers, cookies or query strings) and excludes the swagger, static, health-check and metrics paths, matched as the path itself or a sub-path. A caller-suppliedlitestar_logging_middleware_configreplaces those defaults wholesale.opentelemetry_instrument.py— OTel tracer provider + span export.sentry_instrument.py— Sentry SDK init, skipped whensentry_dsnempty.prometheus_instrument.py— Prometheus metrics; framework variants wrap it.pyroscope_instrument.py— continuous profiling, skipped whenpyroscope_endpointempty.cors_instrument.py— CORS headers, requires an origins/regex setting.swagger_instrument.py— Swagger / offline docs.healthchecks_instrument.py— health-check route, gated byhealth_checks_enabled.
logging_factory.py was split out of logging_instrument.py to keep each file
scoped to one job. It holds MemoryLoggerFactory, _MemoryLoggerFactoryConfig,
the orjson structlog serializer, and the ASGI AddressProtocol /
RequestProtocol typing protocols.
_MemoryLoggerFactoryConfig.log_stream resolves sys.stdout through a
default_factory, so the stream is bound when the instrument bootstraps rather
than when the module is imported. A process that rebinds sys.stdout before
bootstrap — contextlib.redirect_stdout, a supervisor, a test harness — is
honored, and the structlog path agrees with the root-logger handler
_configure_foreign_loggers installs at the same moment.
Optional packages stay optional. lite_bootstrap/import_checker.py exposes
booleans computed once at import via importlib.util.find_spec
(is_opentelemetry_installed, is_sentry_installed, is_fastapi_installed,
is_pyroscope_installed, …). Optional imports sit behind
if import_checker.is_X_installed: blocks. Code that references the optional
symbol is only reached after check_dependencies() has already returned True,
so the runtime invariant holds even though static analyzers that don't model the
guard may report spurious "possibly unbound" diagnostics. The project uses ty,
which handles the pattern correctly.
Guarding dotted find_spec checks. find_spec imports a dotted name's
parent package first, so a present-but-incomplete namespace — e.g.
opentelemetry-api installed without opentelemetry-instrumentation — raised
ModuleNotFoundError instead of returning False, crashing import lite_bootstrap. import_checker._safe_find_spec wraps find_spec for dotted
names and treats that exception as absent; is_fastapi_opentelemetry_installed
and is_litestar_opentelemetry_installed both route through it.
OpenTelemetry resolves as three independent distributions: api, sdk, exporter.
opentelemetry-api (the opentelemetry.trace/.metrics/.context namespace),
opentelemetry-sdk (opentelemetry.sdk.*), and each OTLP exporter package are
separate PyPI distributions — a real environment can have any subset. Three
import_checker flags mirror that:
is_opentelemetry_installed(find_spec("opentelemetry")) — the api. The six api-only consumers (logging trace-injection, frameworkget_tracer_provider, faststream health-check spans) import onlyopentelemetry.trace/.metricsand gate on this.is_opentelemetry_sdk_installed(_safe_find_spec("opentelemetry.sdk")) — the sdk.opentelemetry_instrument.pyimportsopentelemetry.sdk.*, so its module-level import block gates on this, andcheck_dependencies()requires both api and sdk. Without the split, bareopentelemetry-api(e.g.lite-bootstrap[fastmcp], which pulls it transitively without the sdk) crashedimport lite_bootstrapatfrom opentelemetry.sdk import resources.is_otlp_grpc_exporter_installed(_safe_find_spec("opentelemetry.exporter.otlp.proto.grpc.trace_exporter")) — the gRPC OTLP exporter. Its import and its use inbootstrap()sit behind this guard; importing it unconditionally under the api flag previously crashedimport lite_bootstrapthe same way. Whenopentelemetry_endpointis set but the exporter package is absent,bootstrap()emits anInstrumentDependencyMissingWarning("…spans will not be exported. Install lite-bootstrap[otl].") rather than silently omitting the span processor — the standard configured-but-missing signal.is_otlp_http_exporter_installed(_safe_find_spec("opentelemetry.exporter.otlp.proto.http.trace_exporter")) — the HTTP OTLP exporter (opentelemetry-exporter-otlp-proto-http, nogrpcio, so it installs on free-threaded builds; seearchitecture/free-threading.md). Same guarded-import/guarded-use shape as the gRPC flag above; the missing-package warning nameslite-bootstrap[otl-http]instead.
OpenTelemetryConfig.opentelemetry_exporter_protocol ("grpc" default | "http")
selects which exporter bootstrap() builds when opentelemetry_endpoint is set:
"grpc" passes endpoint/insecure to OTLPGrpcSpanExporter; "http" passes
only endpoint (a full URL) to OTLPHttpSpanExporter — the HTTP exporter has no
insecure parameter, since that's carried by the URL scheme. Each branch checks
its own installed-flag and warns independently if the corresponding exporter
package is missing.
All *Config classes are frozen, but *Instrument classes drop frozen=True
because two of them cache mutable runtime state: LoggingInstrument caches a
_logger_factory (MemoryLoggerFactory | None) and OpenTelemetryInstrument
caches _tracer_provider. Python's dataclass rules require the whole hierarchy
to be non-frozen, so BaseInstrument is non-frozen too. Both caches are reset
to None inside a try/finally during teardown(), so a raised shutdown
leaves no stale references.
Litestar's PrometheusConfig defaults group_path=False, so the path metric
label holds the raw URL; parameterized routes then mint one series per distinct
value and grow the registry unbounded (memory growth — see
litestar#4891).
LitestarConfig.prometheus_group_path defaults to True to bind the label to
the route template (/users/{id}). LitestarPrometheusInstrument.bootstrap
merges {"group_path": <field>, **prometheus_additional_params}, so precedence
is prometheus_additional_params["group_path"] > prometheus_group_path >
Litestar's own default. Set prometheus_group_path=False for raw paths. FastAPI
is unaffected: prometheus_fastapi_instrumentator already labels by route
template.
Logging ↔ Sentry. logging_instrument.py renders every structlog line to a
flat JSON object via the shared serializer in logging_factory.py. The seam
between the two instruments is StructuredLogPayload (also in
logging_factory.py): its parse classmethod reconstructs that line into
message / extra / skip_sentry, owning the meta-key vocabulary
(STRUCTLOG_META_KEYS) and stripping it from extra — so neither the parsing
detail nor the key set lives in sentry_instrument.py. The Sentry side's
enrich_sentry_event_from_structlog_log (chained after the user's before_send
via wrap_before_send_callbacks()) only maps the parsed payload onto the Sentry
event: a truthy skip_sentry suppresses the event (checked before the
message-presence test), otherwise it lifts message and attaches extra under
contexts.structlog. IGNORED_STRUCTLOG_ATTRIBUTES remains in
sentry_instrument.py as a back-compat alias of STRUCTLOG_META_KEYS.
OTel ↔ Logging. The logging instrument injects span/trace IDs from the active OpenTelemetry context into every log record, so logs and traces correlate.
Pyroscope ↔ OTel. When both are enabled, a PyroscopeSpanProcessor is added
to the tracer provider so traces and profiles link in Grafana.
OpenTelemetryInstrument.bootstrap() calls
opentelemetry.trace.set_tracer_provider(...), which the OTel SDK enforces as
set-once. A second instance's call is ignored (the SDK logs "Overriding of
current TracerProvider is not allowed"). teardown() calls shutdown() on the
cached provider — flushing batched spans and closing exporters — and resets
_tracer_provider = None, but it cannot reset the process-global pointer.
Construct exactly one OpenTelemetryInstrument per process; do not bootstrap a
second.