ADR 0015 — OTLP Through a Local Collector (no TLS in-process)
- Status: Accepted
- Date: 2026-09-30
- Deciders: repository maintainers
- Supersedes: the transport choices of ADR 0010 phases 19b
(
reqwest) and 19c (tonic). Its reliability primitives (RetryPolicy,CircuitBreaker) stand.
Context
rlg-otlp carried two optional transports from ADR 0010:
async:reqwestwithrustls-tls.grpc:tonicwithtls-ring. It was a scaffold only: the send path returnedGrpcNotImplemented.
Both brought rustls and ring into the tree. cargo deny --all-features failed on them: webpki-roots is licensed
CDLA-Permissive-2.0, outside the allow list, and ring pulled
duplicate getrandom (0.2) and windows-sys (0.52) versions.
The CI gate could only check default features.
The default, blocking exporter already had no TLS: ureq is built
without its rustls feature, so an https:// endpoint failed at
runtime with “TLS required, but transport is unsecured”, while the
crate’s own documentation showed one.
Decision
The exporter speaks plain OTLP/HTTP to an OpenTelemetry Collector (or another OTLP/HTTP forwarder) on the same host or in the same pod. The Collector owns TLS, credentials, batching and buffering towards the backend.
asynckeepsAsyncOtlpExporterand its API, with its HTTP/1.1 exchange written in-house (src/http.rs) over a TokioTcpStream: one connection per request,Connection: close, aContent-Lengthbody, and a response read only as far as the final status line.grpcis removed. Collectors accept OTLP/HTTP on 4318, and an in-house HTTP/2 + HPACK client would be 1–2k lines to own and fuzz for no capability OTLP/HTTP lacks.- Both builders default to
http://localhost:4318/v1/logs(DEFAULT_ENDPOINT). The async builder rejectshttps://endpoints, header names that are not RFC 9110 tokens, header values with CR, LF or NUL, and the headers the client writes itself.
The same change removes rlg’s two other optional dependencies that
failed the all-features check: miette (replaced by
RlgError::code, help and report) and notify (replaced by a
polling watcher in Config::hot_reload_async).
Consequences
cargo deny --all-features checkpasses, and the CI gate checks all features.ring,rustls,webpki-roots,reqwest,hyper,tonicandprostare gone fromrlg-otlp’s tree.- No cryptographic code runs in the exporter’s process. CVE tracking for TLS moves to the Collector, which is patched on its own release cycle.
- Breaking (0.0.x): the
grpcfeature,GrpcOtlpExporter,OtlpError::GrpcEndpointandGrpcNotImplementedare removed;OtlpError::AsyncTransportnow wrapsstd::io::Error; new variantsInvalidEndpointandInvalidHeader;AsyncOtlpExporterBuilder::buildfails on anhttps://endpoint. In rlg, themiettefeature is removed andConfigError::WatcherErrorwrapsstd::io::Error. - Deployments that exported straight to a SaaS endpoint over
https://with theasyncfeature must add a Collector. The crate documentation carries a minimal configuration. - In 0.0.14 the blocking exporter moved onto
src/http.rstoo, over astd::net::TcpStreamwith one deadline per attempt, andureqleft the tree with 37 crates it pulled in.OtlpError::Transportnow wrapsstd::io::Error, and the blocking exporter reportsInvalidEndpointandInvalidHeaderon export, since itsbuild()stays infallible.
Alternatives considered
- Keep rustls, own only the HTTP layer. Rejected:
ringkeeps the duplicategetrandomandwindows-sysversions and the crypto-audit burden, for a capability the Collector provides. - Allow CDLA-Permissive-2.0 and skip the duplicates. Rejected: it silences the gate instead of shrinking the tree.
- Own HTTP/2 to keep OTLP/gRPC. Rejected on cost; see above.