|
| 1 | +# Database Row-Level Security Service — Telemetry |
| 2 | + |
| 3 | +Issue #1468. Every merchant-scoped transaction runs through `withMerchantContext()` |
| 4 | +(`src/lib/db-rls.js`), which sets `app.current_merchant_id` inside a transaction so |
| 5 | +Postgres Row-Level Security policies apply. This document covers the Prometheus |
| 6 | +metrics, health endpoint and alert rules added for that service. |
| 7 | + |
| 8 | +## Metrics |
| 9 | + |
| 10 | +Exported from `src/lib/db-rls-telemetry.js` on its own registry, merged into the |
| 11 | +main `/metrics` scrape by `src/routes/prometheus.js`. Label values are a fixed |
| 12 | +server-side set — never merchant ids, SQL text or error strings — so request |
| 13 | +traffic cannot inflate series cardinality or leak customer data into scrapes. |
| 14 | + |
| 15 | +| Metric | Type | Labels | Meaning | |
| 16 | +| --- | --- | --- | --- | |
| 17 | +| `database_rls_transactions_total` | Counter | `outcome` | Transaction attempts by outcome | |
| 18 | +| `database_rls_transaction_duration_seconds` | Histogram | `outcome` | Wall time of the whole `withMerchantContext` call | |
| 19 | +| `database_rls_active_transactions` | Gauge | — | Transactions currently in flight | |
| 20 | +| `database_rls_health_state` | Gauge | — | `0` healthy, `1` degraded, `2` unhealthy (rolling window) | |
| 21 | +| `database_rls_error_ratio` | Gauge | — | Share of attempts that failed before completing | |
| 22 | +| `database_rls_rollback_ratio` | Gauge | — | Share of attempts rolled back by a failing callback | |
| 23 | +| `database_rls_last_transaction_timestamp_seconds` | Gauge | — | Unix time of the most recent attempt | |
| 24 | + |
| 25 | +`outcome` values: |
| 26 | + |
| 27 | +- `committed` — transaction committed successfully |
| 28 | +- `rolled_back` — the caller's callback threw; the transaction was rolled back |
| 29 | +- `invalid_input` — merchantId or callback failed validation; the pool was never touched |
| 30 | +- `connection_error` — `pool.connect()` failed (pool exhausted, DB down) |
| 31 | +- `transaction_error` — `BEGIN` / `SET LOCAL` / `COMMIT` failed, or `ROLLBACK` itself failed |
| 32 | + |
| 33 | +## Health endpoint |
| 34 | + |
| 35 | +`GET /health/database-row-level-security` returns the rolling-window snapshot: |
| 36 | + |
| 37 | +```json |
| 38 | +{ |
| 39 | + "status": "healthy", |
| 40 | + "reasons": [], |
| 41 | + "window_ms": 300000, |
| 42 | + "total": 1421, |
| 43 | + "committed": 1418, |
| 44 | + "rolled_back": 3, |
| 45 | + "invalid_input": 0, |
| 46 | + "connection_error": 0, |
| 47 | + "transaction_error": 0, |
| 48 | + "errors": 0, |
| 49 | + "rollback_ratio": 0.0021, |
| 50 | + "error_ratio": 0, |
| 51 | + "last_transaction_at": "2026-10-17T12:00:00.000Z", |
| 52 | + "thresholds": { |
| 53 | + "min_samples": 20, |
| 54 | + "error_ratio": 0.05, |
| 55 | + "rollback_ratio": 0.5, |
| 56 | + "invalid_input": 10 |
| 57 | + } |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +Returns 503 only when `status` is `unhealthy`; `degraded` still returns 200. The |
| 62 | +same status is reported (informationally) under `services.database_row_level_security` |
| 63 | +in the main `GET /health` payload. The body contains no merchant ids, query text |
| 64 | +or error strings. |
| 65 | + |
| 66 | +Status rules over the rolling window (default 5 minutes, 20-sample minimum): |
| 67 | + |
| 68 | +- **unhealthy** — connection + transaction errors ≥ 5% of attempts |
| 69 | +- **degraded** — rolled-back callbacks ≥ 50% of attempts, or ≥ 10 `invalid_input` outcomes |
| 70 | +- **healthy** — everything else |
| 71 | + |
| 72 | +Thresholds are overridable with `DATABASE_RLS_HEALTH_WINDOW_MS`, |
| 73 | +`DATABASE_RLS_HEALTH_MIN_SAMPLES`, `DATABASE_RLS_ERROR_RATIO_THRESHOLD`, |
| 74 | +`DATABASE_RLS_ROLLBACK_RATIO_THRESHOLD` and `DATABASE_RLS_INVALID_INPUT_THRESHOLD`. |
| 75 | + |
| 76 | +## Alert rules |
| 77 | + |
| 78 | +`docs/alerts/database-row-level-security.rules.yml` — load with |
| 79 | +`rule_files` and validate with |
| 80 | +`promtool check rules docs/alerts/database-row-level-security.rules.yml`: |
| 81 | + |
| 82 | +- `DatabaseRlsUnhealthy` (critical) — health state ≥ 2 for 5m |
| 83 | +- `DatabaseRlsHighErrorRatio` (critical) — > 5% connection/transaction errors over 10m |
| 84 | +- `DatabaseRlsHighRollbackRatio` (warning) — > 50% callback rollbacks over 15m |
| 85 | +- `DatabaseRlsInvalidInputSpike` (warning) — > 10 invalid merchant ids in 10m (a caller is misbehaving) |
| 86 | +- `DatabaseRlsNoTraffic` (info) — no RLS transactions for 2h |
| 87 | + |
| 88 | +## Notes on privacy |
| 89 | + |
| 90 | +Telemetry records only aggregate counts, durations and derived ratios. Merchant |
| 91 | +ids are never used as label values or logged into the health snapshot, so the |
| 92 | +scrape and health outputs stay safe to expose to the same monitoring stack as the |
| 93 | +rest of the API. |
0 commit comments