- Backend: OTel metrics via OTLP -> Alloy -> Prometheus (OTEL_METRICS_EXPORTER=otlp) - New business metrics (src/metrics.js): searches, CIMA latency/errors, cache hits/misses, logins, rate-limits, pharmacy writes/links, push sent/failed, DB + Redis timings/errors, HTTP req count/duration, heartbeat - Backend health endpoints /healthz and /readyz - Mobile (Expo): Grafana Faro RUM via @grafana/faro-react-native - redis/postgres exporters in docker-compose + Prometheus scrape jobs - Grafana dashboards (backend, datastores, mobile RUM, overview) - Prometheus alert rules (farmafinder_*) -> existing Alertmanager (Telegram) - Design/spec saved to docs/superpowers/specs/
12 KiB
FarmaFinder — End-to-End Observability Plan & Progress
Purpose: Single source of truth for adding metrics + monitoring across the FarmaFinder stack. If the session runs out of tokens, a fresh session can read this file and continue from the Status section.
Created: 2026-07-13
1. Current State (verified)
1.1 Monitoring stack — srv84-macos:/root/Monitoring (Docker Compose)
Running services (verified live):
| Service | Port | Role |
|---|---|---|
Prometheus v2.55.1 |
9090 | metrics + alerting (config /etc/prometheus/) |
Grafana grafana/grafana |
3000 | dashboards (admin foobar) |
Alertmanager v0.27.0 |
9093 | Telegram notifications (already configured) |
Loki latest |
3100 | logs (/otlp receiver enabled) |
Tempo latest |
3200 | traces |
| cAdvisor | 8080 | container metrics |
| node-exporter | 9100 | host metrics (srv84-macos + srv102-hosting + srv104-omv) |
Alloy v1.6.1 |
4317 gRPC / 4318 HTTP / 12345 self | unified OTel collector |
Alloy pipeline (alloy-config.alloy):
otelcol.receiver.otlp (gRPC+HTTP) → batch →
- traces →
otelp.tempo(Tempo:4317) - logs →
otlphttp.loki(Loki:3100/otlp) - metrics →
exporter.prometheus→prometheus.remote_write→http://prometheus:9090/api/v1/write
So any OTLP metrics sent to Alloy land in Prometheus automatically — no new scrape job needed for app services.
1.2 Grafana datasources (provisioned, sufficient)
Prometheus (default), Tempo, Loki, PostgreSQL, Alertmanager.
1.3 Grafana plugins (installed, sufficient)
Built-in panels + Drilldown apps: grafana-lokiexplore-app, grafana-metricsdrilldown-app, grafana-exploretraces-app, grafana-pyroscope-app.
No extra plugins required.
1.4 Existing app instrumentation
| App | Traces | Metrics | Logs | Health | Notes |
|---|---|---|---|---|---|
| backend (Express) | ✅ OTel→Alloy | ❌ none | ✅ pino→Alloy | ❌ none | src/tracing.js boots NodeSDK; only traces+logs exported |
| frontend (React/Vite PWA) | ✅ Faro→Alloy:4318 | ❌ | ✅ Faro→Loki | — | src/utils/faro.js; dashboards faro-overview/-web-vitals/-js-errors exist |
| frontend-mobile (Expo RN) | ❌ | ❌ | console only | — | no observability at all |
| pip-platform (FastAPI) | ✅ OTel | ✅ /metrics (prometheus_client) |
✅ structlog | ✅ /system/health,/ready,/status | not scraped by Prometheus yet |
| scraper (Puppeteer) | ✅ OTel | ❌ | console | — |
1.5 Prometheus scrape jobs (today)
prometheus, cadvisor, node-exporter (x3 hosts). No farmafinder/pip/redis/postgres jobs.
Comment in prometheus.yml: app metrics are expected via OTLP→Alloy→remote_write.
1.6 Existing Prometheus alerts (alert.rules)
service_down, high_load, high_memory, disk_almost_full, pm2_process_down, container_down. All infra-level.
2. Decisions (confirmed with user)
- Metrics transport = OTLP for backend + mobile (aligns with stack design; no new app scrape jobs). Infra exporters (Redis/Postgres/pip) stay as scrape jobs.
- Mobile Faro endpoint: env-configurable
EXPO_PUBLIC_FARO_URL, defaulthttp://grafana.hacecalor.net:4318. User will open router port 4318 → srv84-macos:4318 (and/or fix Nginx Proxy Manager/faroproxy to Alloy:4318). Devices are NOT on the docker host, solocalhostwill NOT work. - Alertmanager: Telegram already configured — no contact-point change. New rules just route to it.
3. Design / Tasks
Task A — Backend metrics (OTel → Alloy → Prometheus)
Files: apps/backend/src/tracing.js, apps/backend/src/metrics.js (new), apps/backend/package.json, docker-compose.yml.
- Add metrics SDK to
tracing.js:MeterProvider+OTLPMetricExporter+PeriodicExportingMetricReader(interval 15s).- Reuse existing
resource+getNodeAutoInstrumentations(already includes http/express/redis → giveshttp.server.request.duration,http.client.request.duration,redis.client.commands.duration, etc.). - Keep
PinoInstrumentation. Gate onNODE_ENV !== 'test'.
- New
src/metrics.jsusing@opentelemetry/apimetrics:medicine_searches_total(counter)cima_request_duration_ms(histogram),cima_errors_total(counter)cache_hits_total/cache_misses_total(counters)login_success_total/login_failure_total(counters)rate_limit_rejected_total(counter, labelroute)pharmacy_write_total(counter, labelop)pharmacy_medicine_link_total(counter)push_sent_total/push_failed_total(counters, labelchannel=expo|web)db_query_duration_ms(histogram),db_errors_total(counter, labelengine=sqlite|pg)redis_errors_total(counter),redis_cmd_duration_ms(histogram)heartbeat_total(counter, incremented every 30s) → drives "backend down" alert- Node runtime metrics via
views/runtime instrumentation (event loop lag, heap, gc).
- Instrument the relevant handlers in
server.js(search, auth, admin, notifications, db, redis) by importing the meters. docker-compose.yml: addOTEL_METRICS_EXPORTER: otlpunder backend env (alongside existing traces/logs exporters).package.json: ensure@opentelemetry/sdk-metrics+@opentelemetry/exporter-metrics-otlp-grpcare present (add if missing; align versions with existing^0.55.0).
Task B — Backend health endpoints
File: apps/backend/server.js.
GET /healthz→{status:"ok"}(liveness).GET /readyz→ ping Redis + run a trivial DB query; return 200/503 JSON.- Used by Docker healthcheck / k8s; not scraped.
Task C — Mobile RUM (Expo / React Native)
Files: apps/frontend-mobile/package.json, apps/frontend-mobile/app/_layout.tsx (root), .env.example.
- Add dependency
@grafana/faro-react-native(^1.2.1). - Init in
_layout.tsx(orApp.tsx) entry:import { initializeFaro } from '@grafana/faro-react-native'; initializeFaro({ app: { name: 'farmafinder-mobile', version: '1.0.0', environment: 'production' }, url: process.env.EXPO_PUBLIC_FARO_URL || 'http://grafana.hacecalor.net:4318', sessionTracking: { enabled: true }, // captures crashes, JS errors, console, ANR(Android), app-start, memory vitals }); EXPO_PUBLIC_FARO_URLin.env.example(default the public URL above).- Optional follow-up:
@grafana/faro-react-native-tracingfor distributed traces +@grafana/faro-metro-pluginfor source maps (post-MVP).
Task D — Infra exporters (scrape jobs on srv84-macos)
Files (remote): /root/Monitoring/docker-compose.yml, /root/Monitoring/prometheus/prometheus.yml.
- Add containers
redis-exporter(→ farmafinder redis) andpostgres-exporter(→ farmafinder postgres) to the Monitoring compose. Reach farmafinder's redis/postgres via the docker gateway / service DNS on the same host. - Add scrape jobs in
prometheus.yml:farmafinder-redis→ redis-exporterfarmafinder-postgres→ postgres-exporterpip-platform→http://<pip-host>:8000/metrics(pip already exposes it)
- Reload Prometheus (
POST /-/reload).
Task E — Grafana dashboards (provisioned JSON)
Files: /root/Monitoring/grafana/provisioning/dashboards/*.json + dashboard.yml entry.
farmafinder-backend.json— req rate, error %, latency p50/p95/p99 by route, CIMA latency/errors, cache hit rate, Redis/DB errors, push counts, auth failures, rate-limit hits, Node runtime.farmafinder-datastores.json— Redis + Postgres + (SQLite via app metric) panels.farmafinder-mobile-rum.json— Faro RN data from Loki (app.name="farmafinder-mobile").farmafinder-overview.json— combined RUM + backend metrics + deep-links to Tempo (traces) & Loki (logs). (Keep existingfaro-*PWA dashboards.)
Task F — Alerts
Files (remote): /root/Monitoring/prometheus/alert.rules + Grafana-managed alert.
Prometheus rules (group farmafinder):
farmafinder_backend_down—absent(heartbeat_total{job="farmafinder-backend"}) > 0for 5m → criticalfarmafinder_backend_5xx—rate(http.server.request.errors ...)ratio > 5% over 5m → warningfarmafinder_cima_errors— CIMA error rate high → warningfarmafinder_high_latency— p95http.server.request.duration> threshold → warningfarmafinder_redis_down/farmafinder_postgres_down/pip_platform_down—up==0Grafana-managed alert (Loki): mobile/PWA JS-error spike — count of Faro exceptions over 10m > N. All route to existing Alertmanager (Telegram).
Task G — Mobile Faro reachability (user action)
- Open router
TCP 4318 → srv84-macos:4318, or fix Nginx Proxy Managergrafana.hacecalor.net/faro→http://<alloy-host>:4318ensuring OTLP HTTP paths (/v1/logs,/v1/metrics,/v1/traces) are proxied. Verify from a phone / external curl thatPOST http://grafana.hacecalor.net:4318/v1/logsis accepted.
4. Status (update as you go)
| Task | Status | Notes |
|---|---|---|
| A. Backend metrics | ✅ done (code) | src/metrics.js, tracing.js meter provider, instrumented server.js/cima-service.js/redis-client.js. Needs backend rebuild+redeploy to take effect. |
| B. Backend health | ✅ done (code) | /healthz, /readyz added. |
| C. Mobile Faro | ✅ done (code) | services/faro.ts + _layout.tsx init + dep @grafana/faro-react-native. Requires dev/EAS build (native module). |
| D. Infra exporters | ✅ done | redis/postgres exporters added to farmafinder docker-compose.yml; Prometheus scrape jobs + alert rules applied on srv84-macos. Needs farmafinder redeploy. |
| E. Dashboards | ✅ done + LIVE | 4 dashboards pushed to /root/Monitoring/grafana/provisioning/dashboards/ and provisioned in Grafana. |
| F. Alerts | ✅ done + LIVE | farmafinder group applied to alert.rules; routes to existing Alertmanager (Telegram). |
| G. Mobile Faro URL | ⏳ user action | open :4318 → srv84-macos:4318, or fix Nginx Proxy Manager /faro. |
5. Deployment & Verification
Backend (apply on next deploy)
The running farmafinder-backend-1 still runs old code. To activate metrics:
# from repo root on srv84-macos (or wherever farmafinder is deployed)
docker compose build backend && docker compose up -d backend
# or push to main and let .gitea deploy
Verify:
# after ~30s, a heartbeat sample should exist
curl -s "http://srv84-macos:9090/api/v1/query?query=app_heartbeat_total{job=%22farmafinder-backend%22}"
# health probes
curl -s http://localhost:3001/healthz ; curl -s http://localhost:3001/readyz
# dashboards populate: Grafana → FarmaFinder — Backend
Mobile (user)
cd apps/frontend-mobile && npm install(pulls@grafana/faro-react-native).- Build a dev/EAS build (not Expo Go — native module).
- Set
EXPO_PUBLIC_FARO_URL(defaulthttp://grafana.hacecalor.net:4318) reachable from device. - Verify in Grafana → FarmaFinder — Mobile RUM (Loki
{app="farmafinder-mobile"}).
Datastore exporters (apply on farmafinder redeploy)
docker compose up -d brings up redis-exporter (:9121) and postgres-exporter (:9187);
Prometheus already scrapes them. Verify: up{job="farmafinder-redis"}, up{job="farmafinder-postgres"}.
6. Verification checklist (end)
- 4 Grafana dashboards live (backend, datastores, mobile-rum, overview)
- Prometheus scrape jobs
farmafinder-redis/farmafinder-postgrespresent - Alert rules
farmafinder_*loaded (no config errors) - Backend redeployed →
app_heartbeat_total,http_requests_total,cima_requests_total, etc. appear /healthz,/readyzreturn 200- Mobile build → Faro data in Loki + Mobile RUM dashboard
- Trigger a test alert (stop backend) → Telegram receives it
7. Key commands / references
- Remote stack:
ssh root@srv84-macos, configs under/root/Monitoring. - Prometheus reload:
curl -X POST http://srv84-macos:9090/-/reload. - Grafana dashboards reload:
curl -u admin:foobar -X POST http://srv84-macos:3000/api/admin/provisioning/dashboards/reload. - Grafana API:
curl -u admin:foobar http://srv84-macos:3000/api/...(admin passwordfoobar). - Backend OTel env (in
docker-compose.yml):OTEL_SERVICE_NAME=farmafinder-backend,OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:4317,OTEL_TRACES_EXPORTER=otlp,OTEL_METRICS_EXPORTER=otlp(added),OTEL_LOGS_EXPORTER=otlp. - New metrics module:
apps/backend/src/metrics.js. - Dashboards source:
monitoring/dashboards/*.json(mirrored on the host).