From d045dacca6f701ab1ce114efc656f7464a2d80cc Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 19 Jul 2026 11:22:02 +0300 Subject: [PATCH 1/4] docs(planning): spec OTLP HTTP exporter for free-threaded OTLP export Add an opentelemetry_exporter_protocol config (grpc|http, default grpc) and an otl-http extra (opentelemetry-exporter-otlp-proto-http, no grpcio) so OTLP trace export works on free-threaded Python; repoint otl to the grpc exporter package. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-07-19.02-otlp-http-exporter.md | 122 ++++++++++++++++++ .../2026-07-19-otlp-http-exporter-shape.md | 47 +++++++ 2 files changed, 169 insertions(+) create mode 100644 planning/changes/2026-07-19.02-otlp-http-exporter.md create mode 100644 planning/decisions/2026-07-19-otlp-http-exporter-shape.md diff --git a/planning/changes/2026-07-19.02-otlp-http-exporter.md b/planning/changes/2026-07-19.02-otlp-http-exporter.md new file mode 100644 index 0000000..3e9569f --- /dev/null +++ b/planning/changes/2026-07-19.02-otlp-http-exporter.md @@ -0,0 +1,122 @@ +--- +summary: Add an `opentelemetry_exporter_protocol` config (grpc|http, default grpc) and an `otl-http` extra (opentelemetry-exporter-otlp-proto-http, no grpcio) so OTLP trace export works on free-threaded Python; repoint `otl` to the grpc exporter package. +--- + +# Design: OTLP HTTP exporter option (free-threaded OTLP export) + +## Summary + +The `otl` extra is the last observability surface with no free-threaded story: +its OTLP exporter goes over gRPC, which needs `grpcio`, which ships no +free-threaded wheels. OpenTelemetry also offers an **HTTP/protobuf** exporter +(`requests` + `protobuf`, no grpcio) that talks to the same collectors. Add an +`opentelemetry_exporter_protocol` config knob (default `"grpc"`, unchanged +behavior) and an `otl-http` extra so a free-threaded service can export traces. + +## Motivation + +`otl` installs `opentelemetry-exporter-otlp` (a meta package pulling **both** the +grpc exporter → `grpcio` and the http exporter), so `pip install +lite-bootstrap[otl]` cannot resolve on a free-threaded interpreter +([grpc/grpc#38762](https://github.com/grpc/grpc/issues/38762)). The instrument +hardwired the gRPC exporter. The HTTP exporter +(`opentelemetry-exporter-otlp-proto-http`) has no `grpcio` dependency (verified: +`requests`, `opentelemetry-proto`, `googleapis-common-protos`), so it installs +on ft. This was deferred from the 2026-07-14 free-threading work; picking it up. + +## Design + +### 1. Config: exporter protocol + +```python +# OpenTelemetryConfig +opentelemetry_exporter_protocol: typing.Literal["grpc", "http"] = "grpc" +``` + +Default `"grpc"` preserves current behavior exactly. `opentelemetry_insecure` +applies to gRPC only; for HTTP the endpoint is a full URL +(`http://collector:4318/v1/traces`) whose scheme carries security — documented, +not warned (see `decisions/2026-07-19-otlp-http-exporter-shape.md`). + +### 2. Two guarded, aliased exporter imports + +`import_checker.py`: add +`is_otlp_http_exporter_installed = _safe_find_spec("opentelemetry.exporter.otlp.proto.http.trace_exporter")` +alongside the existing `is_otlp_grpc_exporter_installed`. + +`opentelemetry_instrument.py`: import each under its own guard, aliased to avoid +the shared `OTLPSpanExporter` name: + +```python +if import_checker.is_otlp_grpc_exporter_installed: + from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter as OTLPGrpcSpanExporter +if import_checker.is_otlp_http_exporter_installed: + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter as OTLPHttpSpanExporter +``` + +### 3. Protocol-selecting `bootstrap()` + +```python +if self.bootstrap_config.opentelemetry_endpoint: + if self.bootstrap_config.opentelemetry_exporter_protocol == "grpc": + if import_checker.is_otlp_grpc_exporter_installed: + tracer_provider.add_span_processor( # pragma: no cover (live collector) + BatchSpanProcessor(OTLPGrpcSpanExporter(endpoint=..., insecure=...))) + else: + warnings.warn("… gRPC OTLP exporter is not installed … install lite-bootstrap[otl].", …) + elif import_checker.is_otlp_http_exporter_installed: + tracer_provider.add_span_processor( # pragma: no cover (live collector) + BatchSpanProcessor(OTLPHttpSpanExporter(endpoint=self.bootstrap_config.opentelemetry_endpoint))) + else: + warnings.warn("… HTTP OTLP exporter is not installed … install lite-bootstrap[otl-http].", …) +``` + +The exporter-construction lines keep `# pragma: no cover` (need a live +collector); both missing-exporter warn branches are covered by tests. + +### 4. Extras + +`pyproject.toml`: repoint `otl` from the meta package to the grpc exporter, and +add `otl-http`: + +```toml +otl = ["opentelemetry-api", "opentelemetry-sdk", "opentelemetry-exporter-otlp-proto-grpc", "opentelemetry-instrumentation"] +otl-http = ["opentelemetry-api", "opentelemetry-sdk", "opentelemetry-exporter-otlp-proto-http", "opentelemetry-instrumentation"] +``` + +`otl` users are unaffected functionally (it already defaulted to gRPC and never +used the http exporter). No framework `*-otl-http` variants — a free-threaded +framework service composes `[fastapi, otl-http]` and adds its instrumentation +package if it wants auto-instrumentation. + +### 5. CI + proof + +Add `otl-http` to both ft legs' extras in `_checks.yml` (proves it resolves on +3.13t/3.14t). Extend `scripts/ft_smoke.py` to bootstrap an +`OpenTelemetryInstrument` with `opentelemetry_exporter_protocol="http"` and an +endpoint, then tear down — proving the http export path constructs on ft with no +live collector needed. + +## Non-goals + +- **Framework `*-otl-http` extras.** Avoided to keep the extras matrix small. +- **Changing the default protocol.** Stays `grpc` for backward compatibility. +- **An `insecure`-style flag or http:// warning for HTTP.** The URL scheme + carries security; documented, per the decision file. + +## Testing + +- Unit (TDD): with `opentelemetry_exporter_protocol="http"` + an endpoint, + `bootstrap()` constructs the **http** exporter (patch `OTLPHttpSpanExporter`, + assert called with the endpoint); `"grpc"` still constructs the grpc exporter; + the http-absent and grpc-absent branches each emit + `InstrumentDependencyMissingWarning` naming the right extra. +- ft CI leg installs `otl-http` and `scripts/ft_smoke.py` bootstraps a + protocol=http instrument on 3.13t/3.14t. +- `just test` 100% coverage; `just lint-ci` clean. + +## Risk + +- **`otl` dependency-set change (low × low).** From the `opentelemetry-exporter-otlp` + meta to `opentelemetry-exporter-otlp-proto-grpc`. Same grpc exporter, minus the + unused http package; grpc behavior unchanged. Noted in the release note. diff --git a/planning/decisions/2026-07-19-otlp-http-exporter-shape.md b/planning/decisions/2026-07-19-otlp-http-exporter-shape.md new file mode 100644 index 0000000..175128a --- /dev/null +++ b/planning/decisions/2026-07-19-otlp-http-exporter-shape.md @@ -0,0 +1,47 @@ +--- +status: accepted +summary: otl repoints to the grpc exporter package and a new otl-http adds the http exporter (no framework variants); the insecure warning stays gRPC-only (http security is the endpoint URL scheme). +supersedes: null +superseded_by: null +--- + +# OTLP HTTP exporter: extras shape and http security signal + +**Decision:** `otl` → `opentelemetry-exporter-otlp-proto-grpc`; new `otl-http` → +`opentelemetry-exporter-otlp-proto-http` (no grpcio); no framework `*-otl-http` +variants. The `__post_init__` insecure warning stays tied to the gRPC `insecure` +flag; for HTTP, the endpoint URL scheme (`http://` vs `https://`) carries +security and is documented, not warned. + +## Context + +`otl` bundled `opentelemetry-exporter-otlp` (a meta package pulling grpc→grpcio +and http). To make OTLP export work on free-threaded Python (no grpcio wheels), +the exporter must be selectable and the http path must be installable without +grpcio. Two shape questions arose. + +## Decision & rationale + +**Extras — `otl`=grpc, `otl-http`=http, base only.** Repointing `otl` to the +grpc exporter package is functionally identical for existing users (it already +defaulted to gRPC and never used the http exporter) and drops only the unused, +transitively-bundled http package. `otl-http` carries the grpcio-free http +exporter for ft. Rejected: keeping `otl` as the meta and layering `otl-http` +on top — leaves `otl` grpcio-bound and redundant (already ships http). Rejected: +framework `*-otl-http` variants (`fastapi-otl-http`, …) — the same combinatorial +extras sprawl avoided in the free-threading work; a ft framework service composes +`[fastapi, otl-http]` and adds its instrumentation package directly. + +**HTTP security signal — no warning.** The gRPC exporter has an `insecure` bool +that the config warns about for non-local endpoints. The HTTP exporter has no +such flag; its endpoint is a full URL whose scheme is the security signal. +Re-deriving an "insecure" state by parsing `http://` vs `https://` would add +scheme-parsing to `__post_init__` for a signal the user already controls +explicitly in the URL. Documented instead. Rejected: an http:// non-local +warning — more code for a weaker nudge than the URL itself already gives. + +## Revisit trigger + +A user asks for a framework-specific ft OTLP-http convenience extra, or for the +http endpoint to accept a bare `host:port` (auto-building the URL) — either would +reopen the extras/URL-handling shape. From 9d522d521c438284b48207479167661ddf218cc1 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 19 Jul 2026 11:48:00 +0300 Subject: [PATCH 2/4] feat(otel): select OTLP exporter protocol (grpc|http); add otl-http extra Add opentelemetry_exporter_protocol (default grpc, unchanged behavior) and a grpcio-free otl-http extra (opentelemetry-exporter-otlp-proto-http) so OTLP trace export works on free-threaded Python. bootstrap() picks the aliased exporter by protocol and warns naming the right extra when it is absent; the insecure warning is gRPC-only. otl now pulls the grpc exporter package directly. Co-Authored-By: Claude Opus 4.8 (1M context) --- lite_bootstrap/import_checker.py | 1 + .../instruments/opentelemetry_instrument.py | 38 +++++++++---- pyproject.toml | 8 ++- .../test_opentelemetry_instrument.py | 53 +++++++++++++++++++ 4 files changed, 90 insertions(+), 10 deletions(-) diff --git a/lite_bootstrap/import_checker.py b/lite_bootstrap/import_checker.py index 0e26a3a..b6a5ddb 100644 --- a/lite_bootstrap/import_checker.py +++ b/lite_bootstrap/import_checker.py @@ -34,6 +34,7 @@ def _safe_find_spec(module_name: str) -> bool: is_opentelemetry_installed and is_litestar_installed and _safe_find_spec("opentelemetry.instrumentation.asgi") ) is_otlp_grpc_exporter_installed = _safe_find_spec("opentelemetry.exporter.otlp.proto.grpc.trace_exporter") +is_otlp_http_exporter_installed = _safe_find_spec("opentelemetry.exporter.otlp.proto.http.trace_exporter") is_pyroscope_installed = find_spec("pyroscope") is not None is_fastmcp_installed = find_spec("fastmcp") is not None is_orjson_installed = find_spec("orjson") is not None diff --git a/lite_bootstrap/instruments/opentelemetry_instrument.py b/lite_bootstrap/instruments/opentelemetry_instrument.py index ebf8d6c..2978601 100644 --- a/lite_bootstrap/instruments/opentelemetry_instrument.py +++ b/lite_bootstrap/instruments/opentelemetry_instrument.py @@ -24,7 +24,10 @@ # opentelemetry-api can be present without the grpc otlp exporter package (e.g. # lite-bootstrap[fastmcp] pulls bare opentelemetry-api transitively); this must # stay a separate guard from is_opentelemetry_installed above. - from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter + from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter as OTLPGrpcSpanExporter + +if import_checker.is_otlp_http_exporter_installed: + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter as OTLPHttpSpanExporter if import_checker.is_pyroscope_installed: import pyroscope @@ -56,6 +59,7 @@ class OpenTelemetryConfig(OpenTelemetryServiceFieldsConfig): ) opentelemetry_endpoint: str | None = None opentelemetry_insecure: bool = True + opentelemetry_exporter_protocol: typing.Literal["grpc", "http"] = "grpc" opentelemetry_instrumentors: list[typing.Union[InstrumentorWithParams, "BaseInstrumentor"]] = dataclasses.field( default_factory=list ) @@ -75,6 +79,8 @@ def __post_init__(self) -> None: def _parse_remote_insecure_host(self) -> str | None: """Return the host name if the endpoint is insecure AND non-local; else None.""" + if self.opentelemetry_exporter_protocol != "grpc": + return None if not self.opentelemetry_endpoint or not self.opentelemetry_insecure: return None if self.opentelemetry_endpoint.startswith("unix://"): @@ -184,19 +190,33 @@ def bootstrap(self) -> None: if self.bootstrap_config.opentelemetry_log_traces: tracer_provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter(formatter=_format_span))) if self.bootstrap_config.opentelemetry_endpoint: - if import_checker.is_otlp_grpc_exporter_installed: - tracer_provider.add_span_processor( # pragma: no cover - BatchSpanProcessor( - OTLPSpanExporter( - endpoint=self.bootstrap_config.opentelemetry_endpoint, - insecure=self.bootstrap_config.opentelemetry_insecure, + if self.bootstrap_config.opentelemetry_exporter_protocol == "grpc": + if import_checker.is_otlp_grpc_exporter_installed: + tracer_provider.add_span_processor( + BatchSpanProcessor( + OTLPGrpcSpanExporter( + endpoint=self.bootstrap_config.opentelemetry_endpoint, + insecure=self.bootstrap_config.opentelemetry_insecure, + ), ), + ) + else: + warnings.warn( + "opentelemetry_endpoint is set but the gRPC OTLP exporter is not installed; " + "spans will not be exported. Install lite-bootstrap[otl].", + category=InstrumentDependencyMissingWarning, + stacklevel=2, + ) + elif import_checker.is_otlp_http_exporter_installed: + tracer_provider.add_span_processor( + BatchSpanProcessor( + OTLPHttpSpanExporter(endpoint=self.bootstrap_config.opentelemetry_endpoint), ), ) else: warnings.warn( - "opentelemetry_endpoint is set but the gRPC OTLP exporter is not installed; " - "spans will not be exported. Install lite-bootstrap[otl].", + "opentelemetry_endpoint is set but the HTTP OTLP exporter is not installed; " + "spans will not be exported. Install lite-bootstrap[otl-http].", category=InstrumentDependencyMissingWarning, stacklevel=2, ) diff --git a/pyproject.toml b/pyproject.toml index 1cea818..4e7bbcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,13 @@ pyroscope = [ otl = [ "opentelemetry-api", "opentelemetry-sdk", - "opentelemetry-exporter-otlp", + "opentelemetry-exporter-otlp-proto-grpc", + "opentelemetry-instrumentation", +] +otl-http = [ + "opentelemetry-api", + "opentelemetry-sdk", + "opentelemetry-exporter-otlp-proto-http", "opentelemetry-instrumentation", ] logging = [ diff --git a/tests/instruments/test_opentelemetry_instrument.py b/tests/instruments/test_opentelemetry_instrument.py index 2afd206..6c1f75f 100644 --- a/tests/instruments/test_opentelemetry_instrument.py +++ b/tests/instruments/test_opentelemetry_instrument.py @@ -7,6 +7,7 @@ import pytest from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +import lite_bootstrap.instruments.opentelemetry_instrument as otel_module from lite_bootstrap import import_checker from lite_bootstrap.exceptions import InstrumentDependencyMissingWarning, TeardownError from lite_bootstrap.instruments.opentelemetry_instrument import ( @@ -239,3 +240,55 @@ def test_bootstrap_warns_when_endpoint_set_without_grpc_exporter() -> None: instrument.bootstrap() finally: instrument.teardown() + + +def test_bootstrap_http_protocol_uses_http_exporter() -> None: + instrument = OpenTelemetryInstrument( + bootstrap_config=OpenTelemetryConfig( + opentelemetry_endpoint="http://collector:4318/v1/traces", + opentelemetry_exporter_protocol="http", + ) + ) + try: + with patch.object(otel_module, "OTLPHttpSpanExporter") as mock_http: + instrument.bootstrap() + mock_http.assert_called_once_with(endpoint="http://collector:4318/v1/traces") + finally: + instrument.teardown() + + +def test_bootstrap_grpc_protocol_uses_grpc_exporter() -> None: + instrument = OpenTelemetryInstrument(bootstrap_config=OpenTelemetryConfig(opentelemetry_endpoint="localhost:4317")) + try: + with patch.object(otel_module, "OTLPGrpcSpanExporter") as mock_grpc: + instrument.bootstrap() + mock_grpc.assert_called_once_with(endpoint="localhost:4317", insecure=True) + finally: + instrument.teardown() + + +def test_bootstrap_warns_when_endpoint_set_without_http_exporter() -> None: + instrument = OpenTelemetryInstrument( + bootstrap_config=OpenTelemetryConfig( + opentelemetry_endpoint="http://collector:4318/v1/traces", + opentelemetry_exporter_protocol="http", + ) + ) + try: + with ( + patch.object(import_checker, "is_otlp_http_exporter_installed", False), + pytest.warns(InstrumentDependencyMissingWarning, match="HTTP OTLP exporter"), + ): + instrument.bootstrap() + finally: + instrument.teardown() + + +def test_http_protocol_does_not_emit_insecure_warning() -> None: + # The insecure warning is gRPC-only; an http:// non-local endpoint must not warn. + with warnings.catch_warnings(): + warnings.simplefilter("error") + OpenTelemetryConfig( + opentelemetry_endpoint="http://remote-collector:4318/v1/traces", + opentelemetry_exporter_protocol="http", + ) From 2196b2ab98fceb6671d2633092f422cba178c72a Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 19 Jul 2026 11:54:32 +0300 Subject: [PATCH 3/4] ci: prove OTLP-http export on the free-threaded legs Install otl-http on both ft legs and have ft_smoke.py bootstrap a protocol=http OpenTelemetryInstrument (grpc/grpcio stays absent). Verified on real 3.13t and 3.14t interpreters. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/_checks.yml | 6 +++--- scripts/ft_smoke.py | 21 ++++++++++++++++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/.github/workflows/_checks.yml b/.github/workflows/_checks.yml index c45425c..985efe1 100644 --- a/.github/workflows/_checks.yml +++ b/.github/workflows/_checks.yml @@ -54,9 +54,9 @@ jobs: matrix: include: - python-version: "3.13t" - extras: "logging,sentry,fastapi,faststream,fastapi-metrics,faststream-metrics" + extras: "logging,sentry,fastapi,faststream,fastapi-metrics,faststream-metrics,otl-http" - python-version: "3.14t" - extras: "logging,sentry,fastapi,litestar,faststream,fastmcp,fastapi-metrics,litestar-metrics,faststream-metrics,fastmcp-metrics" + extras: "logging,sentry,fastapi,litestar,faststream,fastmcp,fastapi-metrics,litestar-metrics,faststream-metrics,fastmcp-metrics,otl-http" steps: - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v8.2.0 @@ -65,7 +65,7 @@ jobs: cache-dependency-glob: "**/pyproject.toml" - run: uv python install ${{ matrix.python-version }} - run: uv venv --python ${{ matrix.python-version }} - - name: Install core + ft-ready extras (orjson/otl/pyroscope excluded; litestar/fastmcp 3.14t-only) + - name: Install core + ft-ready extras (orjson/otl-grpc/pyroscope excluded; otl-http included; litestar/fastmcp 3.14t-only) run: uv pip install ".[${{ matrix.extras }}]" - name: Free-threaded smoke test run: .venv/bin/python scripts/ft_smoke.py diff --git a/scripts/ft_smoke.py b/scripts/ft_smoke.py index 78ed04e..a26207c 100644 --- a/scripts/ft_smoke.py +++ b/scripts/ft_smoke.py @@ -3,15 +3,17 @@ Run under a free-threaded interpreter with the ft-ready extras installed (no orjson). Exits non-zero on failure. Proves: the interpreter is free-threaded, orjson is absent, the logging serializer's stdlib-json fallback produces -parseable output, and a FastAPI bootstrap runs bootstrap()/teardown() clean. -Not a pytest test (conftest.py hard-imports opentelemetry, which the ft leg -does not install). See architecture/free-threading.md. +parseable output, a FastAPI bootstrap runs bootstrap()/teardown() clean, and +OTLP export works over the http exporter (grpc/grpcio stays absent). Not a +pytest test (conftest.py hard-imports opentelemetry, which the ft leg does +not install). See architecture/free-threading.md. """ import sys from lite_bootstrap import FastAPIBootstrapper, FastAPIConfig, import_checker from lite_bootstrap.instruments.logging_factory import StructuredLogPayload, _serialize_log_to_string +from lite_bootstrap.instruments.opentelemetry_instrument import OpenTelemetryConfig, OpenTelemetryInstrument def main() -> None: @@ -42,6 +44,19 @@ def main() -> None: bootstrapper.teardown() assert not bootstrapper.is_bootstrapped + # OTLP export on ft: the http exporter installs and constructs (grpc/grpcio does not). + assert import_checker.is_otlp_http_exporter_installed is True, "otl-http must be installed on the ft leg" + assert import_checker.is_otlp_grpc_exporter_installed is False, "grpc exporter (grpcio) must be absent on ft" + otel = OpenTelemetryInstrument( + bootstrap_config=OpenTelemetryConfig( + service_name="ft-smoke", + opentelemetry_endpoint="http://localhost:4318/v1/traces", + opentelemetry_exporter_protocol="http", + ) + ) + otel.bootstrap() + otel.teardown() + print("ft smoke OK:", sys.version) # noqa: T201 From 7cb583f9a69437aa8f48cbee709141a8522e70d3 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 19 Jul 2026 12:01:22 +0300 Subject: [PATCH 4/4] docs: promote OTLP-http exporter (matrix, guard, release note) Add otl-http to the free-threading matrix, document the api/sdk/exporter guards plus protocol selection in instruments.md, clear the OTLP-http deferral, and note it in 1.3.0. Co-Authored-By: Claude Opus 4.8 (1M context) --- architecture/free-threading.md | 12 +++++++----- architecture/instruments.md | 14 ++++++++++++++ docs/introduction/configuration.md | 5 +++-- planning/deferred.md | 12 ------------ planning/releases/1.3.0.md | 15 ++++++++++----- 5 files changed, 34 insertions(+), 24 deletions(-) diff --git a/architecture/free-threading.md b/architecture/free-threading.md index 258d3bd..8cf3cb4 100644 --- a/architecture/free-threading.md +++ b/architecture/free-threading.md @@ -14,15 +14,17 @@ its extra, not `lite-bootstrap` itself. | `litestar` (+ `litestar-metrics`) | ❌ | ✅ | `msgspec` gates `Py_GIL_DISABLED` to Python 3.14+ — its `_core.c` contains `#error "Py_GIL_DISABLED is only supported in Python 3.14+"` (v0.21.1), so the source build fails on 3.13t. See [`planning/deferred.md`](../planning/deferred.md) | | `fastmcp` (+ `fastmcp-metrics`) | ❌ | ✅ | 3.13t: `cffi` (via `fastmcp`→`cryptography`) refuses to build free-threaded. 3.14t: works (on the leg) since the opentelemetry api/sdk split. See [`planning/deferred.md`](../planning/deferred.md) | | `orjson` (opt-in speedup) | ❌ | ❌ | no ft wheels, build refuses ft ([ijl/orjson#530](https://github.com/ijl/orjson/issues/530)). Omit it on ft; the serializer falls back to stdlib json | -| `otl` (gRPC exporter) | ❌ | ❌ | needs `grpcio`, no ft wheels ([grpc/grpc#38762](https://github.com/grpc/grpc/issues/38762)). An HTTP-exporter path is deferred (`planning/deferred.md`) | +| `otl` (gRPC exporter) | ❌ | ❌ | needs `grpcio`, no ft wheels ([grpc/grpc#38762](https://github.com/grpc/grpc/issues/38762)) | +| `otl-http` (HTTP exporter) | ✅ | ✅ | `opentelemetry-exporter-otlp-proto-http` (requests + protobuf, no grpcio); set `opentelemetry_exporter_protocol="http"` | | `pyroscope` | ❌ | ❌ | `pyroscope-io` is abi3-only, unmaintained, no ft wheels ([`planning/deferred.md`](../planning/deferred.md)) | The CI matrix (`.github/workflows/_checks.yml`, `free-threaded` job) installs per Python version to match this table exactly: the 3.13t leg's extras stop at -`logging,sentry,fastapi,faststream,fastapi-metrics,faststream-metrics`; the -3.14t leg adds `litestar,litestar-metrics,fastmcp,fastmcp-metrics`. `orjson`, -`otl`, and `pyroscope` are excluded from both legs; `fastmcp` runs on 3.14t only -(3.13t is `cffi`-blocked). +`logging,sentry,fastapi,faststream,fastapi-metrics,faststream-metrics,otl-http`; +the 3.14t leg adds `litestar,litestar-metrics,fastmcp,fastmcp-metrics`. `orjson` +and `pyroscope` are excluded from both legs, and `otl` (the gRPC exporter) is +excluded in favor of `otl-http`, which is included on both legs; `fastmcp` runs +on 3.14t only (3.13t is `cffi`-blocked). ## The `orjson` fallback diff --git a/architecture/instruments.md b/architecture/instruments.md index 1780950..02b42fb 100644 --- a/architecture/instruments.md +++ b/architecture/instruments.md @@ -86,6 +86,20 @@ separate PyPI distributions — a real environment can have any subset. Three `InstrumentDependencyMissingWarning` ("…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`, no `grpcio`, + so it installs on free-threaded builds; see `architecture/free-threading.md`). + Same guarded-import/guarded-use shape as the gRPC flag above; the missing-package + warning names `lite-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. ## Why instruments are not frozen diff --git a/docs/introduction/configuration.md b/docs/introduction/configuration.md index 80b1580..935ee2d 100644 --- a/docs/introduction/configuration.md +++ b/docs/introduction/configuration.md @@ -72,9 +72,10 @@ Additional parameters: - `opentelemetry_service_name` - if provided, will be passed to the `Resource` instead of `service_name`. - `opentelemetry_container_name` - will be passed to the `Resource`. -- `opentelemetry_endpoint` - will be passed to `OTLPSpanExporter` as endpoint. +- `opentelemetry_endpoint` - will be passed to `OTLPSpanExporter` as endpoint. Under `opentelemetry_exporter_protocol="http"` this is a full URL (e.g. `http://collector:4318/v1/traces`). - `opentelemetry_namespace` - will be passed to the `Resource`. -- `opentelemetry_insecure` - is opentelemetry connection secure. +- `opentelemetry_exporter_protocol` - OTLP exporter transport: `"grpc"` (default, needs the `otl` extra) or `"http"` (needs the `otl-http` extra, no `grpcio` - installable on free-threaded Python). +- `opentelemetry_insecure` - whether the gRPC OTLP connection is insecure (gRPC only; for `http` the endpoint URL scheme carries security). - `opentelemetry_instrumentors` - a list of extra instrumentors. - `opentelemetry_log_traces` - traces will be logged to stdout. - `opentelemetry_generate_health_check_spans` - generate spans for health check handlers if `True`. diff --git a/planning/deferred.md b/planning/deferred.md index 0db8ae0..272c04c 100644 --- a/planning/deferred.md +++ b/planning/deferred.md @@ -8,18 +8,6 @@ change file in [`changes/active/`](changes/active/); see [CLAUDE.md](../CLAUDE.m ## Open -### OTLP export on free-threaded Python (HTTP exporter path) - -The `otl` extra pulls `grpcio` (via the gRPC OTLP exporter), which has no -free-threaded wheels, so `otl` is uninstallable on ft. `opentelemetry_instrument.py` -hardwires the gRPC exporter (`from opentelemetry.exporter.otlp.proto.grpc...`). -Fix would add an `opentelemetry_exporter_protocol` field (grpc|http) + an -`otl-http` extra on `opentelemetry-exporter-otlp-proto-http` (no grpcio; protobuf -falls back to pure-python). Deferred from the 2026-07-18 free-threading change to -keep that change orjson-only. -**Trigger:** a user needs OTLP trace export on ft, **or** `grpcio` ships ft wheels -([grpc/grpc#38762](https://github.com/grpc/grpc/issues/38762)) making the swap moot. - ### Pyroscope on free-threaded Python `pyroscope-io` is abi3-only, unmaintained, ships no ft wheels and has no pure diff --git a/planning/releases/1.3.0.md b/planning/releases/1.3.0.md index fdfae98..a9b68eb 100644 --- a/planning/releases/1.3.0.md +++ b/planning/releases/1.3.0.md @@ -22,6 +22,12 @@ plus two import-safety fixes surfaced while verifying it. the full support matrix and [`planning/deferred.md`](../deferred.md) for the ecosystem blockers. +- **OTLP-http exporter**: new `opentelemetry_exporter_protocol` config (`grpc` + default | `http`) and an `otl-http` extra (no `grpcio`) so OTLP trace export + works on free-threaded Python. `otl` now pulls + `opentelemetry-exporter-otlp-proto-grpc` directly (was the `opentelemetry-exporter-otlp` + meta); grpc behavior is unchanged. + - **`orjson` is now an opt-in extra** (`lite-bootstrap[orjson]`) instead of a mandatory core dependency — it shipped no free-threaded wheels and its build refuses to compile under a free-threaded interpreter, which meant nothing, @@ -50,11 +56,10 @@ plus two import-safety fixes surfaced while verifying it. absent (e.g. `lite-bootstrap[fastmcp]`, which pulls in bare `opentelemetry-api` transitively). The exporter import — and its use in `bootstrap()` — now sit behind their own `is_otlp_grpc_exporter_installed` - guard. Note the trade-off this leaves open: setting `opentelemetry_endpoint` - in an environment with `opentelemetry-api` but not the exporter package now - silently skips OTLP export rather than warning; see - [`architecture/instruments.md`](../../architecture/instruments.md) and - [`planning/deferred.md`](../deferred.md). + guard. When `opentelemetry_endpoint` is set but the selected exporter package + is absent, `bootstrap()` emits an `InstrumentDependencyMissingWarning` naming + the extra to install (`[otl]` for gRPC, `[otl-http]` for HTTP); see + [`architecture/instruments.md`](../../architecture/instruments.md). Both bugs are not ft-specific — they affect any environment with a partial `opentelemetry` stack — but ft verification work is what surfaced them.