Operating notes
Metrics exposure, crashes, long outages, re-processing a user and other things to know in production.
-
A token (
OHS_METRICS_TOKEN): the API listener serves/metricsto a scrape that sendsAuthorization: Bearer <token>, and answers 401 otherwise. In Prometheus:scrape_configs: - job_name: ohs authorization: credentials_file: /etc/prometheus/ohs-metrics-token static_configs: - targets: ["ohs:8080"] -
A private address (
OHS_METRICS_LISTEN, for example127.0.0.1:9464or an address on your monitoring network):/metricsmoves off the API listener to that address. Whoever can reach the address can scrape it, unlessOHS_METRICS_TOKENis also set. Never publish that port.
With neither, /metrics answers 403 and names the two settings. OHS_METRICS=false removes the
route altogether.
observability.md lists the metrics, and how to turn on OpenTelemetry
traces.
-
Run
etlsomewhere. User deletion is carried out by a worker that runs with theetlrole. Anapionly deployment acceptsDELETE /v1/users/{id}but never finishes it, and uploads are never processed, until a process with theetlrole is running against the same database. -
Deletion and providers. Deleting a user revokes their provider grants on a best effort basis. If a provider is down, the user's data is still deleted and the revocation is logged as failed; the grant may then stay valid at the provider until the user removes it there.
-
After a crash. A job interrupted mid-way is claimed again after 10 minutes. A failed job is retried with backoff and parked after 5 attempts (
parked_jobsin the coverage report). A raw file whose row was never written (the process died between the two) is not requeued: the daily file scanner reports it, and the data comes back when the client retries the upload, or when the reclaimed pull job fetches that window again. Records that failed for a recoverable reason (mapping, unit, or a whole object that failed) are replayed by the drainer once per process start, so deploy the fix and restart. Lines of a type the connector does not know are skipped, not failed, so the drainer never replays them: re-process the user (below) once a release maps that type. -
Long outages. The orphan scanner only looks at raw objects from the last 7 days. After an outage longer than that, raw objects that were stored but never queued stay unprocessed. Requeue them with this SQL, run once in
psql(it queues every raw object that has no job; the tier matches what the scanner uses):INSERT INTO ohs_sync.etl_jobs (tenant_id, user_id, provider, key, tier) SELECT o.tenant_id, o.user_id, o.provider, o.key, CASE WHEN o.provider = 'apple_health' THEN 'high_freq' ELSE 'standard' END FROM ohs_sync.raw_objects o WHERE o.created_at < now() - interval '1 minute' AND NOT EXISTS (SELECT 1 FROM ohs_sync.etl_jobs j WHERE j.key = o.key); -
Re-processing a user from the raw files. Raw payloads are never rewritten, so after a mapper fix (a new type mapped, a unit corrected) a user's data can be built again from them. Queue one ETL job per raw object of the user, run once in
psqlwith the two ids filled in (objects that already have a job waiting are left alone):INSERT INTO ohs_sync.etl_jobs (tenant_id, user_id, provider, key, tier) SELECT o.tenant_id, o.user_id, o.provider, o.key, CASE WHEN o.provider = 'apple_health' THEN 'high_freq' ELSE 'standard' END FROM ohs_sync.raw_objects o WHERE o.tenant_id = '<tenant id>' AND o.user_id = '<user id>' AND NOT EXISTS (SELECT 1 FROM ohs_sync.etl_jobs j WHERE j.key = o.key AND j.status IN ('pending', 'running'));Records are de-duplicated by content hash, so what was already stored is not stored twice, and the coverage report's
pending_jobsshows the progress. Rows the old mapper wrote are not removed: if the fix changes what identifies a record (its type name, its value after conversion), the old row stays beside the new one until you delete it. Drop theuser_idcondition to re-process a whole tenant. -
Rotating a provider secret. The Withings client secret also signs the notification URLs registered with Withings. After a rotation every connected user has to be subscribed again, or their notifications are refused (pulls keep working every 6 hours). See connectors/withings.md.
-
MCP over a redirecting URL.
ohs mcpdoes not follow redirects. SetOHS_MCP_URLto the final https URL of the instance, see mcp.md. -
OAuth state is not bound to a browser session. A consent link is single use and expires after 10 minutes, but whoever completes it connects the account they sign in with to the user it was minted for. Tenants must hand an authorize URL only to the user it belongs to, and never post it somewhere others can open it.
-
Compression. Chunks of
quantity_samplesolder than 30 days are compressed by a TimescaleDB background job. Late data for an old period (a re-upload, a history import) is written to the uncompressed part of the chunk and folded in when the job runs again.SELECT * FROM chunk_compression_stats('ohs_data.quantity_samples')shows the sizes before and after. -
Hourly statistics. A TimescaleDB job refreshes the
quantity_hourlycontinuous aggregate every 5 minutes, recomputing only the hours that changed, so late data and deletions (an erased user included) leave the statistics within about 5 minutes. On an existing install its first run builds the whole history once (about 11 s for 10 million samples in our test).SELECT * FROM timescaledb_information.job_statsshows its runs. -
Large uploads. An instance processes a bounded number of uploads at once (
OHS_INGEST_CONCURRENCY, default 8, 2 on the Pi) and answers 503 withRetry-Afterabove it; clients retry. -
Rate limits on the provider-facing routes. Notifications and OAuth callbacks over the limits answer 429 with
Retry-After; see rate limits. Behind a reverse proxy, setOHS_TRUSTED_PROXIES, or every request counts against the proxy's address.
Metrics
/metrics serves Prometheus counters for the whole instance (requests by route, ingest load, ETL
queues, sync state by provider), never per-tenant data. It is still information about your
instance, so it is closed unless you open it one of two ways: