Skip to content

feat: add an opt-in OpenTelemetry metrics pipeline - #265

Merged
lesnik512 merged 1 commit into
mainfrom
feat/179-otel-metrics
Sep 27, 2026
Merged

lesnik512 merged 1 commit into
mainfrom
feat/179-otel-metrics

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Closes #179.

OpenTelemetry defines three signals and this library shipped one. No MeterProvider was
constructed anywhere, so the metrics signal was absent, and three integrations that accept a meter
provider were being handed nothing: opentelemetry-instrumentation-asgi, -fastapi, and
faststream's telemetry middleware, whose protocol at faststream_bootstrapper.py:40 already
declared meter_provider while the call site passed tracer_provider alone. Those integrations
built their duration histograms against the API's no-op provider and the data went nowhere.

Opt-in, through its own field

opentelemetry_metrics_endpoint turns the signal on. Set it and bootstrap() builds a
MeterProvider with a PeriodicExportingMetricReader over the OTLP metric exporter, sharing the
resource the tracer provider already gets, and installs it. Leave it unset and no meter provider is
constructed at all.

Its own field rather than riding on opentelemetry_endpoint, which is the decision #179 called
blocked. Reusing the trace endpoint would start exporting metrics for every existing user on
upgrade, with request metrics then counted twice wherever the Prometheus instrument is also on.
Opt-in matches how this repo has handled new signals twice recently (#241, #243) and keeps the
cheap direction open: opt-in to default-on later is easy, the reverse is breaking. docs/adr/ gets
nothing, because nothing here is hard to reverse.

opentelemetry_exporter_protocol and opentelemetry_insecure are shared with traces rather than
duplicated. The export interval stays the SDK's, overridable with OTEL_METRIC_EXPORT_INTERVAL,
following ADR-0008's line that a knob the SDK already reads from the environment does not need a
field.

is_configured now accepts a metrics-only config, since metrics are the same concern and asking
only for them is asking for the instrument. Teardown shuts the meter provider down through the
existing error collector, and the missing-exporter warning has a metrics twin naming [otl] or
[otl-http].

The #227 lesson, carried over

set_meter_provider is set-once exactly like set_tracer_provider, so this PR would have shipped
a second silent orphan: a configured exporter, a live export thread, and nothing feeding it. It
warns the same way, and the conftest fixture from #264 now resets both globals. The meter
provider's pair lives in opentelemetry.metrics._internal, one module below the public re-export.

Two changes beyond the strict ask

  • The insecure-endpoint warning now covers both endpoints. Leaving it on
    opentelemetry_endpoint alone would mean a remote insecure metrics endpoint warned about
    nothing, a gap this PR would have introduced. Its wording moves from "sending traces
    unencrypted" to "sending telemetry unencrypted", which is now the accurate word. No test
    asserted the old text; all three match on unencrypted.
  • floor_smoke.py sets the metrics endpoint on all four otel targets, so the 25 floors legs
    exercise MeterProvider, PeriodicExportingMetricReader and both OTLP metric exporters at the
    declared floor instead of leaving the new surface untested there. It costs a second exporter
    retry loop per leg at shutdown, on top of the one traces already pays.

Tests

Written failing first, red for the right reason each time. On the instrument: the meter provider is
installed and carries the resource, the http protocol picks the http exporter, an unset endpoint
installs nothing, teardown shuts it down, each protocol warns when its exporter package is absent,
a metrics-only config is configured, and the lost-race warning fires with caller attribution. One
binding test per framework pins that the meter provider actually reaches the integration.

Three existing Litestar tests construct LitestarOpenTelemetryInstrumentationMiddleware directly
and now pass a meter provider. The parameter is required, like tracer_provider; the class is not
exported from lite_bootstrap/__init__.py and appears in no docs.

Verified

eof-fixer --check, ruff format --check, ruff check --no-fix, ty check clean.
345 passed under -p no:randomly and under random order. Coverage 100.00%, gate satisfied.
mkdocs build --strict passes with the new Metrics section in docs/introduction/configuration.md.

Floors: ran the free target locally at Python 3.10 against opentelemetry-api/-sdk 1.28.0 and
opentelemetry-exporter-otlp-proto-grpc 1.28.0, with the metrics endpoint set:
floor smoke OK: free on 3.10.21.

@lesnik512
lesnik512 merged commit 6cb8322 into main Sep 27, 2026
37 checks passed
@lesnik512
lesnik512 deleted the feat/179-otel-metrics branch September 27, 2026 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Emit OpenTelemetry metrics from the existing OpenTelemetry instrument

1 participant