report-query-api — operations
FastAPI + DuckDB service that serves the Atrax fleet-report Parquet exports on Wasabi as per-car /
per-driver JSON reports. Read half of #6138 Track B. Source: infra-src/report-query-api/.
Design: docs/designs/6808-report-query-api.md.
- Write half (upstream):
scripts/nightly-etl/export_atrax.py— nightly on vps-i1 (01:00 UTC, cronatrax-etl) →s3://p24-infra/reporting/atrax/{table}/{YYYY-MM-DD}.parquet. - This service reads those Parquet files in place via DuckDB
httpfs(no re-ingest) and never writes to Wasabi.
Architecture
- One long-lived in-process DuckDB
:memory:connection;httpfs+ Wasabi read credentials set once at startup (duck.py). - Two read modes, matching the exporter’s two write modes (
tables.py):- PER_DAY (
p24_gps_trips,p24_route_consumption_anomalies,p24_atrax_drivers_daily,p24_driver_ecodriving_daily): each daily file holds one day. A range query globs{table}/*.parquetand filters the date column. - SNAPSHOT (
p24_route_consumption_norm,p24_consumption_rolling_averages): each daily file is a full dump; only the newest file is read (older files are superseded).
- PER_DAY (
- Car identifier: the same Atrax car ObjectId is stored as
atrax_car_idin trips/ecodriving and asvehicle_idin the route/rolling tables. The API path param{vehicle_id}is that ObjectId; each table is filtered on its own column.vehicle_name(ecodriving only) is the human plate (e.g.ZPL71820), surfaced by/api/v1/cars. - All user input (ids, dates, window types) is bound as DuckDB
?parameters; only the Parquet path (from the fixed table whitelist) is interpolated — no SQL-injection surface.
Endpoints
All /api/v1/* require Authorization: Bearer $REPORT_QUERY_API_KEY; /health, /ping,
/metrics are open. from/to are inclusive YYYY-MM-DD. Empty backing table → row_count: 0
(never a 500). See README.md for the full table.
Deployment (vps-i1) — DELEGATED sys-admin + secret-manager tasks
This service is not deployed by the implementation PR. It requires a server operation and a new
credential, which a dev-coder cannot perform. Ordered checklist:
1. Credentials → secret-manager (#6808 design §6)
- New Bearer key
REPORT_QUERY_API_KEY→ narrowsecrets/report-query-api.env.sops(2 recipients: developer + CIAGE_KEY_GHA; thesecrets/anthropic-admin.env.sopsprecedent), with a dedicatedsecrets-sync.ymljob that writes only this service’s env and recreates only this container. - Wasabi read access: either reuse the existing
P24_INFRA_WASABI_*read keys already on vps-i1, or (preferred hardening) a dedicated read-only Wasabi IAM key scoped toreporting/atrax/*. - Deliver the same
REPORT_QUERY_API_KEYto the et-op consumer via that repo’s Vercel env (secrets/et-operational-platform.env.sops) at consumer-integration time.
2. PoC latency validation → sys-admin on vps-i1 (design §4)
Before committing to direct-read (vs the materialisation fallback), run a real DuckDB-over-S3 query
against live Wasabi (creds already in /opt/p24-infra/monitoring/.env — never echo them) and record
p50/p95 for a single-day read and a 30-day glob on the PR. If 30-day p95 > ~2 s, adopt the §4
materialisation fallback (additive, no API-contract change).
3. Compose service (add to monitoring/docker-compose.yml)
report-query-api:
build: ../infra-src/report-query-api
container_name: report-query-api
restart: unless-stopped
env_file:
- .env.report-query-api # written 0600 by the dedicated secrets-sync job
environment:
- WASABI_BUCKET=p24-infra
- WASABI_ENDPOINT=s3.eu-central-2.wasabisys.com
- WASABI_REGION=eu-central-2
expose:
- "8000"
networks:
- monitoring4. Prometheus scrape (add to monitoring/prometheus/prometheus.yml)
- job_name: report-query-api
static_configs:
- targets: ['report-query-api:8000']5. Caddy reverse proxy
Add an internal route on the existing monitoring Caddy (HTTPS termination, not exposed beyond need), matching the other stack services.
6. Compliance (CLAUDE.md §Compliance — do at deploy time)
- Insert a
dev_r_servicesrow forreport-query-api(compliance_workbook='yes',workbook_url→ this doc).dev_r_servicesis a VIEW — insert against its underlying relation, neverALTERit (seedocs/playbooks/supabase-migrations.md). - EU AI Act: read-only serving of pre-computed, rule-based statistics (incl. the ecodriving
eco_score); no scoring/ranking/model of its own → no new Annex III system, nodev_r_ai_systemsentry (matches the Atrax ETL precedent). Confirm with the compliance owner at deploy time (design §7).
Local dev / test
cd infra-src/report-query-api
pip install -r requirements.txt -r requirements-test.txt
python -m pytest -q # local Parquet fixtures — no Wasabi, no creds
REPORT_QUERY_API_KEY=dev uvicorn main:app --port 8000Troubleshooting
| Symptom | Cause / fix |
|---|---|
503 service not configured on /api/v1/* | REPORT_QUERY_API_KEY unset — fails closed by design. Set it. |
401 on /api/v1/* | missing/wrong Bearer key. |
row_count: 0 for a known car | check the date range; snapshot tables (norm, rolling-averages) ignore from/to (latest snapshot only). |
502 backend query failed | DuckDB/Wasabi error (bad creds, endpoint). Check container logs; the actual error is logged, never returned. |
| rolling-averages always empty | p24_consumption_rolling_averages had 0 rows until the first nightly compute after #6777; verify the export ran and the table populated. |