Observability
Observability — OpenTelemetry → GCP-native
The observability stack uses OpenTelemetry for instrumentation and GCP-native backends for storage and visualization: Cloud Logging (+ Log Analytics), Cloud Trace, Cloud Monitoring, and Error Reporting. Telemetry stays inside the existing Google BAA — no second vendor boundary, no standing Elasticsearch cluster.
This replaces the older ELK / Elastic Cloud design. OTel is the hedge: the backend is exporter-agnostic, so switching or adding a backend later (a SIEM, Datadog, self-hosted Grafana) is exporter config, not a rewrite. Revisit only if a real Kibana-search or SIEM need emerges. Cost is roughly free at launch volumes (Cloud Logging: 50 GiB/project/mo ingest free, then ~$0.50/GiB).
API OTel Setup — backend/api
Dependencies (pyproject.toml)
opentelemetry-sdkopentelemetry-instrumentation-fastapiopentelemetry-exporter-otlp
Initialization (main.py)
- Initialize the OTel tracer + meter on application startup.
- Auto-instrument FastAPI with
opentelemetry-instrumentation-fastapi. - The OTLP endpoint is configured via
OTEL_EXPORTER_OTLP_ENDPOINT.
In production, OTLP is exported to GCP's managed endpoint (telemetry.googleapis.com) → Cloud Trace / Cloud Monitoring. In development, it points at the local OTel Collector.
Cloud Run automatically ships stdout/stderr to Cloud Logging. Emit structured JSON logs so they land in Log Analytics with queryable fields.
The Restate durable-execution worker (backend/worker) is instrumented the same way — same OTLP exporter and OTEL_EXPORTER_OTLP_ENDPOINT — so its traces and metrics land in the same GCP backends.
PHI Scrub Filter
CRITICAL: Patient health information must never appear in logs — or in spans.
A PHIScrubFilter stub class is included in the Python backend services (backend/api and backend/worker):
import logging
class PHIScrubFilter(logging.Filter):
def filter(self, record):
raise NotImplementedError(
"Implement PHI scrubbing before production use"
)This filter is attached to the root logger in each service (backend/api and backend/worker) and will intentionally fail if any log message is emitted before the scrubbing logic is implemented. (This matches the PHIScrubFilter control in HIPAA Compliance, which covers both backends.)
TODO before production:
- Implement regex-based scrubbing for:
- Medical Record Numbers (MRNs)
- Dates of Birth (DOB patterns)
- Patient names
- Test the filter with representative log data
- Verify no PHI appears in Cloud Logging, Cloud Trace spans, or Cloud Build logs
Local Development Stack — docker-compose.otel.yml
For local development, run a lightweight OTel Collector — no Elasticsearch/Kibana required:
| Service | Image | Port |
|---|---|---|
| OTel Collector | otel/opentelemetry-collector-contrib:latest | 4317 (gRPC), 4318 (HTTP) |
| Jaeger (optional local trace viewer) | jaegertracing/all-in-one:latest | 16686 (UI) |
The collector receives OTLP on 4317/4318 and, in dev, exports to the console and/or a local Jaeger UI. In production the same OTLP stream is pointed at telemetry.googleapis.com.
One collector, one story. This is the same collector the local dev stack brings up as the otel-collector process — Developer Tooling's process-compose runs docker compose -f docker-compose.otel.yml up, and the api/worker OTEL_EXPORTER_OTLP_ENDPOINT defaults to it (http://localhost:4317).
OTel Collector Configuration
An otel-collector-config.yml stub is provided:
- Dev:
debug(console) exporter + optionalotlp→ Jaeger. - Prod:
googlecloudexporter (or OTLP →telemetry.googleapis.com).
Data Flow
FastAPI App (OTel SDK)
→ OTLP (gRPC/HTTP)
→ OTel Collector
→ Cloud Trace / Cloud Monitoring / Cloud Logging (prod: telemetry.googleapis.com)
→ console / local Jaeger (dev)
Dashboards
Use Cloud Monitoring dashboards first. If Grafana-grade dashboards are needed, self-host Grafana on Cloud Run against Cloud Monitoring / Managed Prometheus data sources — GCP has no first-party managed Grafana, and Grafana Cloud SaaS would add a third-party boundary.
Scrub PHI from logs and spans. Cloud Logging and Cloud Trace are inside the Google BAA, but PHI still must not be written there.
Alerting / On-Call
Alerting is GCP-native, matching the rest of the observability stack — no separate paging vendor at launch:
- Cloud Monitoring alert policies fire on metric thresholds, log-based metrics, and uptime checks.
- SLOs (Cloud Monitoring service-level objectives with burn-rate alerts) drive the signal — page on error-budget burn, not on every transient blip.
- Notification channels: alerts route to Microsoft Teams (via an incoming-webhook channel) and email. Teams is the primary real-time surface; email is the durable backstop.
Alert payloads are PHI-free — send resource names, metric values, and trace IDs only, never PHI. Teams and email are outside the trust zone.
Dedicated paging (PagerDuty / Opsgenie) is deferred. Cloud Monitoring → Teams + email is sufficient for a small team. Add a paging tool with on-call rotations, escalation policies, and SMS/phone paging as the team grows and 24/7 coverage is needed.
Error Tracking
Error tracking is split by tier, each staying inside an existing boundary:
- Backends (
backend/api,backend/worker) → GCP Error Reporting (part of Cloud Operations; groups exceptions from Cloud Run logs, in-boundary under the Google BAA). - Clients (
ui/web-app,ui/marketing,ui/ios) → PostHog error tracking — already the BAA-covered analytics/replay vendor, so it covers web + Expo/React Native with no extra boundary. - Escalation: Sentry if richer release-health / source-map grouping is needed later (adds a vendor boundary + BAA).
Error payloads can carry PHI — exception messages, request context, and breadcrumbs. Scrub PHI before it reaches Error Reporting or PostHog, exactly as for logs and spans, and enable PostHog's masking on captured errors.
LLM Observability & Evals
LLM calls (Gemini via Vertex) are traced and evaluated in PostHog (LLM analytics — prompt/response capture, latency + cost, and eval scoring), consolidating on the vendor already inside the BAA.
- Escalation: self-hosted Langfuse on Cloud Run (in-boundary) if deeper trace trees or dataset-based eval workflows are needed.
Prompts and completions on clinical tasks are PHI. Keep LLM traces inside the boundary (the PostHog BAA), mask/scrub PHI in captured prompts and outputs, and never key a trace on PHI. See HIPAA Compliance.