feat(OP-3108): Initialize OpenTelemetry in foundry-python-core#96
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests.
|
a424026 to
3a3efe0
Compare
|
Notes from testing this against a locally-generated Foundry service (OP-3108, sandbox). A few design points worth considering before merge, none blocking: 1.
|
8abdc9c to
d5c2a40
Compare
Adds process-wide OTel instrumentation via boot(), following the existing Sentry pattern: independently-toggleable traces/metrics/logs, default HTTPX/SQLAlchemy/FastAPI instrumentors (with full override), GCP resource detection, and a combined CA bundle for the internal OTel gateway's TLS. Request-level FastAPI tracing is applied automatically by init_api() (to the root app and every versioned sub-app), gated on a real TracerProvider actually being registered — no per-service wiring needed. See docs/decisions/0006-cloud-run-otel-telemetry-export.md for the design rationale and consequences. Ref: OP-3108 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
d5c2a40 to
3dab10d
Compare
Two changes requested in review: - Default OTEL_SEMCONV_STABILITY_OPT_IN=http, opting HTTPXClientInstrumentor and FastAPIInstrumentor into the stable HTTP semantic conventions instead of the old, experimental ones. The old conventions name a span after the literal request path (unbounded cardinality for any route with a path parameter); the stable ones require a low-cardinality route template. - Move internal-CA trust from a Python-side combined bundle (certifi + bundled cert, merged into a temp file at runtime) to the OS trust store, installed by the generated Dockerfile via `update-ca-certificates` (foundry-python#385). OTEL_EXPORTER_OTLP_CERTIFICATE now just points at the resulting OS bundle if present, falling back to certifi's alone otherwise — trust becomes independent of any one language/library, and this repo no longer needs to bundle/ship the CA cert itself, write a temp file, or register atexit cleanup for it. Matches the same pattern already used in pathosearch-portal-onboarder and pviz. Ref: OP-3108 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fault tests _default_otlp_certificate_setdefault() checks pathlib.Path(...).is_file(), which doesn't call os.path.isfile() internally, so patching os.path.isfile was a no-op. The tests only passed locally because /etc/ssl/certs/ca-certificates.crt happens to be absent on macOS dev machines; on the Linux CI runner it genuinely exists, so the real (unmocked) is_file() returned True regardless of the intended mock, breaking the two tests that expected the certifi fallback branch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|



What this does
Gives every Foundry service OpenTelemetry support (traces, metrics, and optionally logs) via a single core version bump — no per-service instrumentation code required. Once a service turns it on, its telemetry lands in the internal Tempo/Loki/Prometheus stack over OTLP/gRPC, via ADR 0006.
Part of OP-3108. This is the client/SDK side; the gateway it talks to is gitops#3459, and the one-line template wiring (now largely unnecessary — see below) is foundry-python#385.
How it's shaped
Follows the same pattern already used for Sentry: a pydantic-settings-driven
enabledflag, a singleotel_initialize()entry point called fromboot(), and graceful no-ops when the SDK isn't installed or isn't configured.OTEL_EXPORTER_OTLP_ENDPOINT,OTEL_SERVICE_NAME,OTEL_EXPORTER_OTLP_CERTIFICATE) rather than reinventing project-prefixed ones, so services already familiar with the OTel spec don't need to learn a second set of names.logging, and loguru has no first-party OTel integration. Added a sink that converts each loguru record into a stdlibLogRecordand forwards it to OTel's ownLoggingHandler, reusing the SDK's conversion/trace-correlation logic instead of reimplementing OTLP log export.HTTPXClientInstrumentor(so an outboundhttpxcall to another Foundry service continues the caller's trace instead of starting a new, disconnected one) andSQLAlchemyInstrumentor(every Foundry service gets a SQLAlchemy-backed database layer, so DB spans are as much a baseline expectation as HTTP ones). Both are optional-dependency-gated and only applied once a realTracerProvideris actually registered — a project can pass its own list, or[]to opt out entirely.init_api()(the function that actually constructs the app instance, and always runs afterboot()) instruments the returned app and every versioned sub-app before returning, and skips it entirely when no realTracerProvideris registered (mirroring the guard the other instrumentors use, so a service with OTel disabled doesn't pay forOpenTelemetryMiddlewareon every request for nothing). This closes the original "known follow-up" below — outside of thefoundry-pythontemplate, nothing extra needs to be called.OTEL_EXPORTER_OTLP_CERTIFICATEdefaults to a combined CA bundle (certifi's system roots + the fleet's internalaignx-ca-root-authority), since the gateway's TLS-facing endpoint is signed by that internal CA — bundling both keeps a service that points at a public vendor instead working too.boot()logs whether it actually initialized —otel: on/offalongside the existingsentry: on/off, so you can tell from the boot log whether telemetry is actually flowing without checking env vars by hand.boot()running twice in the same process (tests, a subprocess that inherited parent state) — each provider no-ops if a real one is already registered, instead of stacking a second exporter on top.Verified end-to-end
Beyond unit/integration tests, this was smoke-tested against the real sandbox OTLP gateway from a Docker container: TLS handshake succeeds via the combined CA bundle (where a raw
curlwith only system roots fails), and bothSpanExportResult.SUCCESSandLogRecordExportResult.SUCCESScome back from the gateway.Test plan
mise run lint— ruff, pyrefly, deptry all pass.mise run test_unit— 346 unit tests passing,otel.pyat 100% coverage.mise run test_integrationmise run pre_commit_run_all🤖 Generated with Claude Code