refactor(observability): infer OTLP TLS from endpoint scheme#90
Conversation
lucasmundim
left a comment
There was a problem hiding this comment.
Solid security fix and the bug call-out on the env-var lookup is correct. A few items before merging — flagged inline. Two non-inline items not in the diff that also want attention:
RELEASE.md:352anddocs/overview.md:935-952still documentOTLP_GRPC_ENDPOINT_SECURITY/ don't mention the new scheme-based inference. Worth a docs pass in this PR or a follow-up.
shingonoide
left a comment
There was a problem hiding this comment.
LGTM on the security direction; +1 to Mundão's metrics-factory regression — that's the merge blocker.
Two complementary notes:
OTLP_GRPC_ENDPOINT_SECURITYwas a full no-op, not a half-working knob — worth being precise inRELEASE.mdbeyond just deleting the stale doc.os.getenv(name, True)returns the literalTrueonly when unset; any set value came back as a string, and every non-empty string is truthy, soinsecure=Trueregardless of what an operator wrote. No one was ever getting TLS through this var. The actual behavior change is narrower: operators with barehost:portendpoints flip from plaintext to TLS. That's the line worth in the changelog.- New hard import coupling.
from openfilter.observability.tracing import infer_otlp_insecureat the top of the metrics factory means the metrics module now fails to import iftracingdoes. Low-probability but easy to avoid with a lazy import inside theotlp_grpcbranch, or by moving the helper to a smallopenfilter/observability/_otlp.pyso neither side imports the other's heavy module.
Otherwise clean — urlparse over regex is the right call, and the gRPC path now infers transport the same way otlp_http already does, which is good consistency.
136ffde to
b9405dd
Compare
|
Updated in b9405dd — see inline thread replies for the per-comment items. @lucasmundim — five inline points addressed; on the docs you flagged: the dead @shingonoide — both points taken:
|
lucasmundim
left a comment
There was a problem hiding this comment.
All five inline points addressed and the two follow-ups from @shingonoide too. Verified locally:
- Metrics-factory
or "http://localhost:4317"fallback lands correctly — with empty env and no kwargs, the built exporter opens an insecure channel againstlocalhost:4317. The original regression I flagged would have produced a secure channel against the SDK's default endpoint here. - End-to-end TLS behavior verified by intercepting
grpc.{secure,insecure}_channel:http://→ insecure,https://→ TLS, barehost:port→ TLS, and explicitinsecure=Trueagainst bare host correctly overrides to insecure. - 27/27 tests pass across
tests/test_tracing.pyand the newtests/test_open_telemetry_exporter_factory.py. - Decoupling clean:
openfilter/observability/_otlp.pyis the single source, metrics factory imports from there (notracingimport), andtracing.pyre-exportsinfer_otlp_insecurein__all__for any external callers. RELEASE.mdv0.1.31 entry is precise about the actual behavior change (only barehost:portoperators flip plaintext → TLS), and the stale v0.1.6 line is gone.docs/overview.mdgot the TLS-inference callout.
One minor observation, not blocking: https://... + explicit insecure=True ends up as a secure channel — the underlying OTel SDK preserves TLS when the URL scheme is https:// regardless of the kwarg. The factory passes the kwarg through faithfully; this is SDK behavior, not a PR bug, and the realistic bare-host override case works correctly.
LGTM. Thanks for the careful follow-through on every point.
Replaces the always-`insecure=True` default for the OTLP gRPC span exporter and metrics exporter with TLS inferred from the endpoint scheme via `urllib.parse.urlparse`: - `http://...` → `insecure=True` (plaintext) - `https://...` → `insecure=False` (TLS) - bare `host:port` → `insecure=False` (TLS — secure default, matches the OTel SDK) - Explicit `insecure=` in `exporter_config` / kwargs always wins The metrics factory now also has the `http://localhost:4317` fallback that the tracing factory already had, so an unset endpoint stays plaintext-against-localhost rather than falling through to the SDK's TLS default. Drops the broken `OTLP_GRPC_ENDPOINT_SECURITY` env-var lookup in the metrics factory, which never bool-parsed strings and so was effectively `insecure=True` regardless of the env var's value — no operator was ever getting TLS through this var. The actual behavior change for deployments is narrow: bare `host:port` endpoints flip from plaintext to TLS; operators relying on plaintext with a bare-host endpoint must now pass `insecure=True` (or use an `http://` URL). The `infer_otlp_insecure` helper lives in a new tiny `openfilter/observability/_otlp.py` so the metrics factory does not have to import the heavy `tracing` module to read the scheme. Tests: 27/27 pass — covers helper input matrix, scheme inference in both factories, explicit `insecure=` override, and the metrics factory's unset-env-var localhost fallback. Signed-off-by: stwilt <swilt@plainsight.ai>
b9405dd to
610df4d
Compare
lucasmundim
left a comment
There was a problem hiding this comment.
Code review
Found 1 issue:
- The "Mirror the tracing factory's localhost fallback" comment overstates parity — only the fallback is mirrored, not the env-var precedence chain.
…th tracing Per review feedback on PR #90: the metrics ExporterFactory only checked `OTEL_EXPORTER_OTLP_GRPC_ENDPOINT`, while `build_span_exporter` walked `TELEMETRY_EXPORTER_OTLP_ENDPOINT` -> `OTEL_EXPORTER_OTLP_ENDPOINT` -> `OTEL_EXPORTER_OTLP_GRPC_ENDPOINT` -> fallback. An operator setting the Plainsight-convention `TELEMETRY_EXPORTER_OTLP_ENDPOINT` got traces but metrics silently fell through to localhost. - `open_telemetry_exporter_factory.py`: extend `otlp_grpc` and `otlp_http` endpoint resolution to the same 4-step chain that `build_span_exporter` uses; replace the "Mirror the tracing factory's localhost fallback" comment with the explicit precedence list. - `tracing.py`: drop the latent-inconsistency note from `build_span_exporter`'s docstring — the chain it called out is now aligned across both factories. - `test_open_telemetry_exporter_factory.py`: add two precedence tests covering `TELEMETRY_EXPORTER_OTLP_ENDPOINT` (wins over the OTel-spec and legacy GRPC vars) and `OTEL_EXPORTER_OTLP_ENDPOINT` (wins over the legacy GRPC var when the Plainsight var is unset). Tests: 6/6 in test_open_telemetry_exporter_factory.py, 23/23 in test_tracing.py. Signed-off-by: stwilt <swilt@plainsight.ai>
lucasmundim
left a comment
There was a problem hiding this comment.
Re-review on 3cecd64.
The env-var precedence chain is the right shape and the comment block at lines 25-33 clearly documents the ordering. Two items inline — neither blocking, but the test one is the same shape as the assertIsNotNone weakness we dropped in the prior round and worth tightening before this merges.
Per review feedback on PR #90: the six tests in ``test_open_telemetry_exporter_factory.py`` asserted only ``assertIsInstance(exporter, OTLPGrpcExporter)``, which is the same class regardless of which env var wins. The precedence-named tests would pass identically if the precedence were reversed. - Patch ``OTLPGrpcExporter`` in the factory module's namespace and assert ``call_args`` for each case so the test names match what is actually being verified. - Pin the localhost fallback to the literal ``http://localhost:4317`` and ``insecure=True`` so a regression that lets ``endpoint=None`` or TLS reach the SDK against a plaintext local collector fails loudly. - ``otlp_http`` branch: add an inline note that the missing literal fallback is intentional — the OTLP HTTP SDK already defaults endpoint=None to ``http://localhost:4318/v1/metrics`` and hard-coding a bare-host fallback here would drop the ``/v1/metrics`` path suffix. Tests: 29/29 in test_open_telemetry_exporter_factory.py + test_tracing.py. Signed-off-by: stwilt <swilt@plainsight.ai>
lucasmundim
left a comment
There was a problem hiding this comment.
Re-review on 20c0c65.
Both inline items from the prior round are addressed:
- Test assertions are now load-bearing. Every case patches
OTLPGrpcExporterin the factory module's namespace and asserts the exact(endpoint, insecure)kwargs.test_unset_endpoint_falls_back_to_localhost_plaintextpins("http://localhost:4317", True); the two precedence tests pin the winning endpoint and inferredinsecure. Reversing precedence or lettingendpoint=Nonereach the SDK would now fail loudly. otlp_httpasymmetry documented. The new comment at L55-60 makes the intentional divergence vs. the gRPC branch explicit, and the SDK-default claim checks out —OTLPMetricExporter(endpoint=None)._endpoint == "http://localhost:4318/v1/metrics", so a bare-host fallback would have silently dropped the path suffix.
29/29 tests pass locally. LGTM.
One non-blocking follow-up worth noting: the otlp_http branch grew the same env-var precedence chain but the new test file only covers otlp_grpc. Reasonable scope cut for this PR; if symmetric coverage is wanted later, the existing tests are a cheap template.
build_exporter in openfilter.observability.client is the metrics builder
that OpenTelemetryClient (and therefore every running filter pod) actually
uses — the file docstring calls it "for the demo" but Filter.__init__ in
filter_runtime imports it on every init. The otlp branch passed neither
insecure= nor a scheme-aware default to OTLPMetricExporter, so the SDK
default (TLS) kicked in against every endpoint shape — including the
Plainsight in-cluster otel-collector's bare host:port form, which is
plaintext. The live failure mode in prod filter pods is a continuous
stream of:
ssl_transport_security.cc:2125] Handshake failed with error SSL_ERROR_SSL:
error:100000f7:SSL routines:OPENSSL_internal:WRONG_VERSION_NUMBER
WARNING Transient error StatusCode.UNAVAILABLE encountered while
exporting metrics to otel-collector.monitoring.svc.cluster.local:4317
— TLS client bytes hitting a plaintext gRPC listener.
- client.py: import infer_otlp_insecure from ._otlp and apply the same
scheme inference build_span_exporter and the metrics ExporterFactory
use. Explicit insecure= in exporter_config still wins.
- tests/test_observability_client_build_exporter.py: mirror the
exporter-factory test style — patch OTLPMetricExporter on its source
module and pin (endpoint, insecure) kwargs for http://, https://, bare
host:port (the production-regression case), explicit-insecure override,
unset-everything localhost fallback, and TELEMETRY_EXPORTER_OTLP_ENDPOINT.
Env-var precedence chain is intentionally left as-is here (kwarg ->
TELEMETRY_EXPORTER_OTLP_ENDPOINT -> http://localhost:4317 fallback)
rather than expanded to the four-step chain build_span_exporter uses;
TELEMETRY_EXPORTER_OTLP_ENDPOINT is the only var the live filter
Deployment spec sets, so the narrower chain covers production while
keeping this commit a behavior fix and not a separate refactor.
Tests: 35/35 across tests/test_observability_client_build_exporter.py +
tests/test_open_telemetry_exporter_factory.py + tests/test_tracing.py.
Signed-off-by: stwilt <swilt@plainsight.ai>
Signed-off-by: stwilt <swilt@plainsight.ai>
lucasmundim
left a comment
There was a problem hiding this comment.
Re-review on 440a52f. The new commit closes the third call site — client.py:build_exporter is the metrics builder OpenTelemetryClient wires up in the live runtime (line 157), so this is the path that was producing the WRONG_VERSION_NUMBER handshake error in filter pods against the in-cluster collector.
35/35 tests pass locally across the three relevant files. The new test file pins exact (endpoint, insecure) kwargs — same load-bearing shape as test_open_telemetry_exporter_factory.py from the prior round.
LGTM. Two non-blocking notes inline, both about scope-symmetry vs. the other two builders, neither introduced by this PR.
| @@ -69,7 +70,15 @@ def shutdown(self, timeout: float = 30000) -> None: | |||
| or os.getenv("TELEMETRY_EXPORTER_OTLP_ENDPOINT") | |||
There was a problem hiding this comment.
Pre-existing, not introduced here, but worth a follow-up: this env-var chain is shorter than the other two builders we aligned in 3cecd64. ExporterFactory.otlp_grpc and tracing.build_span_exporter.otlp_grpc both also consult OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_GRPC_ENDPOINT before falling back to localhost. An operator setting only the standard OTel env vars would get them honored by traces and by filter_runtime's metrics factory, but not by this builder. Out of scope for this PR (the diff doesn't touch the chain) — flagging for a follow-up that fans out the same 4-step chain here.
|
|
||
| ### Changed | ||
|
|
||
| - **OTLP gRPC `insecure` flag now inferred from endpoint scheme**: The exporter factory and tracing builder now use `urllib.parse.urlparse` on the configured endpoint — `http://` infers plaintext, `https://` infers TLS, and bare `host:port` infers TLS (secure default, matches the OTel SDK). Explicit `insecure=` in `exporter_config` / kwargs always wins. The default `http://localhost:4317` endpoint still infers `insecure=True`, so local collectors keep working with no config change. The metrics factory's missing localhost fallback was also fixed in this change — an unset endpoint no longer falls through to the SDK's TLS default against a plaintext local collector. |
There was a problem hiding this comment.
Worth a small tightening: this entry says "the exporter factory and tracing builder," but 440a52f adds the same inference to a third site — observability/client.py:build_exporter — which is the metrics path OpenTelemetryClient uses in the live runtime and the one that produced the WRONG_VERSION_NUMBER symptom operators would search the changelog for. Suggest something like "the exporter factory, tracing builder, and OpenTelemetryClient's metrics builder now use urllib.parse.urlparse..." so the changelog matches symptom to fix.
📋 What does this PR do?
Replaces the always-
insecure=Truedefault for the OTLP gRPC span exporter and metrics exporter with TLS inferred from the endpoint scheme viaurllib.parse.urlparse:http://...→insecure=True(plaintext)https://...→insecure=False(TLS)host:port→insecure=False(TLS — secure default, matches the OTel SDK)insecure=inexporter_config/ kwargs always winsAlso drops the broken
OTLP_GRPC_ENDPOINT_SECURITYenv-var lookup in the metrics factory, which never bool-parsed strings and so was effectivelyinsecure=Trueregardless of the env var's value.🔍 Why is this needed?
The previous default would silently send telemetry over plaintext gRPC even when the configured endpoint was an
https://URL — a quiet security footgun. Inferring from the URL scheme matches what most OTel SDKs (and the Go exporters this code originally mirrored) do, and giveshttps://and bare-host endpoints a secure-by-default behavior. The defaulthttp://localhost:4317still infersinsecure=True, so local collectors keep working with no config change.🧪 How was it tested?
pytest tests/test_tracing.py— 24/24 passhttp://,https://, uppercase scheme, barehost:port, empty/None, and explicitinsecure=Trueoverride against a bare-host endpointpython -c "from openfilter.filter_runtime.open_telemetry.open_telemetry_exporter_factory import ExporterFactory"— imports clean🔗 Related Issues
None — no ticket for this change.
✅ Checklist
git commit -s)