Skip to content

feat(telemetry): OTLP traces for cc-catalog-svc - #197

Merged
stewartshea merged 3 commits into
mainfrom
feat/cc-catalog-svc-otlp-telemetry
Nov 2, 2026
Merged

stewartshea merged 3 commits into
mainfrom
feat/cc-catalog-svc-otlp-telemetry

Conversation

@stewartshea

@stewartshea stewartshea commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

What

Adds opt-in OTLP telemetry to cc-catalog-svc — the producer half of the platform's built-in observability (the collector + OpenObserve ship with the platform chart; the collector side already exists).

The service now consumes the platform chart's documented standard surface as-is: OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf with the per-signal http://<release>-otel-collector:4318/v1/... endpoints. No bespoke variables, no extra config.

Off-by-default contract

Nothing changes unless the standard OTel env vars are set: no exporter is constructed, no provider installed, no network call made, no new required config. app/telemetry.py short-circuits to a no-op path (returns None) when a signal's switch is not otlp or no endpoint is present, and FastAPI is only instrumented when tracing is actually active. A deployment that sets nothing behaves exactly as before.

Exact env vars (standard names only — the chart needs no bespoke vars)

Protocol — picks the exporter:

var value effect
OTEL_EXPORTER_OTLP_PROTOCOL http/protobuf (default, the chart's value) OTLP over HTTP/protobuf (:4318)
grpc OTLP over gRPC (:4317)

Signal switches (value must be otlp; anything else leaves the signal off):

var effect
OTEL_TRACES_EXPORTER=otlp traces on
OTEL_LOGS_EXPORTER=otlp logs on
OTEL_METRICS_EXPORTER=otlp metrics on

Endpoints — the per-signal variable wins over the generic one, for both protocols:

  • OTEL_EXPORTER_OTLP_TRACES_ENDPOINT / OTEL_EXPORTER_OTLP_ENDPOINT
  • OTEL_EXPORTER_OTLP_LOGS_ENDPOINT / OTEL_EXPORTER_OTLP_ENDPOINT
  • OTEL_EXPORTER_OTLP_METRICS_ENDPOINT / OTEL_EXPORTER_OTLP_ENDPOINT

For HTTP the SDK's endpoint rule is reproduced exactly: a per-signal endpoint is used verbatim (the chart's already end in /v1/<signal>), while a generic endpoint gets /v1/<signal> appended. This is asserted in tests — getting it wrong double-paths the URL and breaks the chart's endpoints.

Service / resource / other:

  • OTEL_SERVICE_NAME — per-component name; wins over the in-code default (cc-catalog-svc).
  • OTEL_RESOURCE_ATTRIBUTES — merged into the shared Resource; deployment.environment is read from it.
  • OTEL_TRACES_SAMPLER (+ OTEL_TRACES_SAMPLER_ARG) and OTEL_SDK_DISABLED — honored where the SDK reads them itself.

What is instrumented

  • Traces — a TracerProvider + BatchSpanProcessor(<protocol> span exporter), and FastAPIInstrumentor on the app (a server span per request). setup_telemetry() / instrument_fastapi().
  • Logs (only when OTEL_LOGS_EXPORTER=otlp) — a LoggerProvider + OTLP log exporter attached to the root stdlib logger, so every record is exported with the active span's trace/span id (that is what lets OpenObserve correlate logs to traces). setup_logging_export().
  • Metrics (only when OTEL_METRICS_EXPORTER=otlp) — a MeterProvider + periodic OTLP metric reader, installed as the global meter provider. setup_metrics().

One resolve_service_name() resolves the name for all three signals, and one _build_resource() builds the shared OTel Resource (service.name, service.version, deployment.environment), so traces/logs/metrics can never land under different service names.

Safety

Every exporter/provider is built inside try/except that logs and degrades to "no telemetry" instead of raising, because this runs on the FastAPI startup path. Provider shutdown is per-provider guarded too.

Wiring

  • app/main.py — telemetry is set up at the top of the lifespan (after logging is configured) and shutdown_telemetry() flushes on exit; instrument_fastapi(app) runs once before the app starts.

How I tested it

  • poetry run pytest tests/test_telemetry.py → 26 passed. Covers: nothing-configured no-op (no exporter constructed, no crash); OTEL_SERVICE_NAME precedence; protocol selection (http/protobuf default → HTTP exporter; grpc → gRPC exporter; unknown/http/json → http/protobuf); endpoint rules — per-signal verbatim for traces/logs/metrics, generic endpoint gets /v1/<signal> appended (incl. trailing-slash handling), gRPC endpoints never get a path; https → insecure=False; logs (root handler, idempotent) and metrics (global install) opt-in; and broken trace/log/metric exporters degrade to None instead of raising. All exporters are patched — no test performs network I/O.
  • Full suite: poetry run pytest → 180 passed, 2 failed. The two failures are pre-existing and unrelated (tests/test_git_http.py::test_info_refs_serves_repo_owned_by_another_user and ::test_clone_succeeds_through_symlinked_data_dir_when_foreign_owned); both fail identically on an unmodified main checkout here, because this machine's git reports "dubious ownership" for the foreign-owned test repositories. I did not touch them.
  • Smoke-tested the real app booting with the chart's exact env surface (HTTP/protobuf, per-signal /v1/... endpoints, no generic var) and patched exporters: GET /healthz → 200, HTTP span/log exporters constructed with the endpoints used verbatim, app instrumented, clean shutdown. Also smoke-tested with nothing configured (no-op).
  • ruff check on changed files: passes.

Deliberately not done

  • Did not touch k8s/ manifests or the chart — the platform's OTel env ConfigMap supplies the variables; this PR is code-only.
  • No http/json support: OTEL_EXPORTER_OTLP_PROTOCOL=http/json falls back to http/protobuf (the opentelemetry-exporter-otlp-proto-http package only speaks protobuf). gRPC remains fully supported via OTEL_EXPORTER_OTLP_PROTOCOL=grpc.
  • Did not merge.

Wire cc-catalog-svc up as an OTLP *producer* for the platform's built-in
observability (the collector + OpenObserve already ship with the chart).

Off by default and driven entirely by the standard OpenTelemetry env vars:
nothing is exported, constructed or dialed until OTEL_* asks for it, so an
unconfigured deployment behaves exactly as before.

- app/telemetry.py: shared OTel Resource (service.name/version/environment),
  one resolve_service_name() used by every signal, a TracerProvider + OTLP
  (gRPC) span exporter with FastAPI instrumentation, and opt-in logs
  (root handler) and metrics (MeterProvider). Every exporter is built inside
  try/except so telemetry can never break startup.
- app/main.py: configure telemetry in the lifespan and shut it down on exit;
  instrument FastAPI before the app starts.
- pyproject.toml/poetry.lock: add opentelemetry-api/sdk,
  exporter-otlp-proto-grpc and instrumentation-fastapi.
- tests/test_telemetry.py: off-by-default no-op, service-name precedence,
  traces-enabled provider, per-signal endpoint precedence, and exporter
  failures degrading instead of raising.
The platform chart's standard surface is HTTP/protobuf on :4318 with
per-signal endpoints that already carry /v1/<signal>, and it deliberately
sets no generic OTEL_EXPORTER_OTLP_ENDPOINT. A gRPC-only exporter aimed
gRPC at the HTTP port and had no fallback, so the service could not talk
to the collector as deployed.

- Read OTEL_EXPORTER_OTLP_PROTOCOL (default http/protobuf; grpc supported)
  and build the matching exporter for traces, logs and metrics.
- Reproduce the SDK endpoint rule for both protocols: a per-signal endpoint
  is used verbatim, a generic endpoint gets /v1/<signal> appended, so the
  chart's endpoints are never double-pathed.
- Add opentelemetry-exporter-otlp-proto-http and refresh the lock.
- Tests: protocol selection (default http/protobuf, grpc), endpoint
  precedence and path-append rules for traces/logs/metrics, alongside the
  existing off-by-default, service-name, shared-resource and
  failure-degradation cases.
@github-actions

Copy link
Copy Markdown
Contributor

cc-catalog-svc image

Tag: feat-cc-catalog-svc-otlp-telemetry197-merge-03645820
Build: success

Image Tag
cc-catalog-svc feat-cc-catalog-svc-otlp-telemetry197-merge-03645820
us-docker.pkg.dev/runwhen-nonprod-shared/public-images/cc-catalog-svc:feat-cc-catalog-svc-otlp-telemetry197-merge-03645820

Date-tagged GHCR releases are published by release.yaml on merge to main.

@github-actions

Copy link
Copy Markdown
Contributor

cc-catalog-svc image

Tag: feat-cc-catalog-svc-otlp-telemetry197-merge-6fa3ac34
Build: success

Image Tag
cc-catalog-svc feat-cc-catalog-svc-otlp-telemetry197-merge-6fa3ac34
us-docker.pkg.dev/runwhen-nonprod-shared/public-images/cc-catalog-svc:feat-cc-catalog-svc-otlp-telemetry197-merge-6fa3ac34

Date-tagged GHCR releases are published by release.yaml on merge to main.

@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

cc-catalog-svc image

Tag: feat-cc-catalog-svc-otlp-telemetry197-merge-54566a4a
Build: success

Image Tag
cc-catalog-svc feat-cc-catalog-svc-otlp-telemetry197-merge-54566a4a
us-docker.pkg.dev/runwhen-nonprod-shared/public-images/cc-catalog-svc:feat-cc-catalog-svc-otlp-telemetry197-merge-54566a4a

Date-tagged GHCR releases are published by release.yaml on merge to main.

@stewartshea
stewartshea merged commit ff08e1b into main Nov 2, 2026
2 checks passed
@stewartshea
stewartshea deleted the feat/cc-catalog-svc-otlp-telemetry branch November 2, 2026 01:16
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.

1 participant