|
| 1 | +# Monitoring & Alerting Configuration |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +StepFi API exposes three observability endpoints: |
| 6 | + |
| 7 | +| Endpoint | Purpose | Public | Route | |
| 8 | +|------------|-------------------------------|--------|-------------------| |
| 9 | +| `/health` | Comprehensive health check | Yes | `GET /health` | |
| 10 | +| `/metrics` | Prometheus metrics | No | `GET /metrics` | |
| 11 | +| `/docs` | Swagger/OpenAPI documentation | Yes | `GET /api/v1/docs`| |
| 12 | + |
| 13 | +> **`/metrics` must never be exposed publicly.** Block it at the reverse proxy (Nginx, |
| 14 | +> Cloudflare, or Render's network layer). Only the internal Prometheus server may |
| 15 | +> scrape this endpoint. |
| 16 | +
|
| 17 | +--- |
| 18 | + |
| 19 | +## Prometheus Metrics |
| 20 | + |
| 21 | +All metrics use the `stepfi_` prefix: |
| 22 | + |
| 23 | +### HTTP Request Metrics |
| 24 | +- `stepfi_http_requests_total` — Counter, labels: `method`, `status`, `path` |
| 25 | +- `stepfi_http_request_duration_seconds` — Histogram, labels: `method`, `status`, `path` |
| 26 | + - Buckets: 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2.5s, 5s, 10s |
| 27 | + |
| 28 | +### Application Metrics |
| 29 | +- `stepfi_bullmq_queue_depth` — Gauge, labels: `queue` |
| 30 | +- `stepfi_indexer_lag_ledgers` — Gauge (indexer ledger lag behind network tip) |
| 31 | +- `stepfi_horizon_up` — Gauge (1 = reachable, 0 = down) |
| 32 | +- `stepfi_db_pool_open` — Gauge (open database connections) |
| 33 | + |
| 34 | +### Default Node.js Metrics |
| 35 | +Provided by `prom-client` default metrics: |
| 36 | +- `process_cpu_user_seconds_total` |
| 37 | +- `process_resident_memory_bytes` |
| 38 | +- `nodejs_heap_size_used_bytes` |
| 39 | +- `nodejs_eventloop_lag_seconds` |
| 40 | + |
| 41 | +--- |
| 42 | + |
| 43 | +## Uptime Monitors |
| 44 | + |
| 45 | +### Recommended service (Render) |
| 46 | + |
| 47 | +If deployed on Render, configure a **cron job** health check: |
| 48 | + |
| 49 | +```yaml |
| 50 | +healthCheckPath: /health |
| 51 | +healthCheckInterval: 30 # seconds |
| 52 | +``` |
| 53 | +
|
| 54 | +### Recommended service (external — Pingdom / Better Uptime) |
| 55 | +
|
| 56 | +| Setting | Value | |
| 57 | +|------------------|--------------------------------| |
| 58 | +| Check URL | `https://api.stepfi.app/health`| |
| 59 | +| Check interval | 1 minute | |
| 60 | +| Timeout | 10 seconds | |
| 61 | +| Retries | 2 | |
| 62 | +| Regions | US East, US West, EU West | |
| 63 | + |
| 64 | +**Alert triggers:** |
| 65 | +- 2 consecutive failures → Pager (PagerDuty / Opsgenie) |
| 66 | +- Any single 5xx response → Slack notification |
| 67 | + |
| 68 | +--- |
| 69 | + |
| 70 | +## Prometheus & Alertmanager |
| 71 | + |
| 72 | +### Scrape configuration (`prometheus.yml`) |
| 73 | + |
| 74 | +```yaml |
| 75 | +scrape_configs: |
| 76 | + - job_name: 'stepfi-api' |
| 77 | + scrape_interval: 30s |
| 78 | + scrape_timeout: 10s |
| 79 | + metrics_path: /metrics |
| 80 | + static_configs: |
| 81 | + - targets: ['api.stepfi.app:443'] |
| 82 | + scheme: https |
| 83 | + authorization: |
| 84 | + credentials: '<bearer-token-if-used>' |
| 85 | +``` |
| 86 | + |
| 87 | +### Alert rules (`alerts.yml`) |
| 88 | + |
| 89 | +```yaml |
| 90 | +groups: |
| 91 | + - name: stepfi |
| 92 | + rules: |
| 93 | +
|
| 94 | + # API down |
| 95 | + - alert: StepFiAPIDown |
| 96 | + expr: probe_success{job="stepfi-api"} == 0 |
| 97 | + for: 1m |
| 98 | + labels: |
| 99 | + severity: critical |
| 100 | + annotations: |
| 101 | + summary: 'StepFi API is unreachable' |
| 102 | +
|
| 103 | + # HTTP 5xx rate |
| 104 | + - alert: StepFiHighErrorRate |
| 105 | + expr: | |
| 106 | + rate(stepfi_http_requests_total{status=~"5.."}[5m]) |
| 107 | + / |
| 108 | + rate(stepfi_http_requests_total[5m]) |
| 109 | + > 0.05 |
| 110 | + for: 5m |
| 111 | + labels: |
| 112 | + severity: warning |
| 113 | + annotations: |
| 114 | + summary: 'Error rate > 5% over 5m' |
| 115 | +
|
| 116 | + # p99 latency |
| 117 | + - alert: StepFiHighLatency |
| 118 | + expr: | |
| 119 | + histogram_quantile( |
| 120 | + 0.99, |
| 121 | + rate(stepfi_http_request_duration_seconds_bucket[5m]) |
| 122 | + ) > 3 |
| 123 | + for: 5m |
| 124 | + labels: |
| 125 | + severity: warning |
| 126 | + annotations: |
| 127 | + summary: 'p99 latency > 3s over 5m' |
| 128 | +
|
| 129 | + # Indexer lag |
| 130 | + - alert: StepFiIndexerStalled |
| 131 | + expr: stepfi_indexer_lag_ledgers > 500 |
| 132 | + for: 2m |
| 133 | + labels: |
| 134 | + severity: warning |
| 135 | + annotations: |
| 136 | + summary: 'Indexer lag > 500 ledgers' |
| 137 | +
|
| 138 | + - alert: StepFiIndexerCritical |
| 139 | + expr: stepfi_indexer_lag_ledgers > 2000 |
| 140 | + for: 1m |
| 141 | + labels: |
| 142 | + severity: critical |
| 143 | + annotations: |
| 144 | + summary: 'Indexer lag > 2000 ledgers — data may be stale' |
| 145 | +
|
| 146 | + # BullMQ queue depth |
| 147 | + - alert: StepFiQueueBacklog |
| 148 | + expr: stepfi_bullmq_queue_depth > 1000 |
| 149 | + for: 5m |
| 150 | + labels: |
| 151 | + severity: warning |
| 152 | + annotations: |
| 153 | + summary: 'BullMQ queue {{ $labels.queue }} has > 1000 waiting jobs' |
| 154 | +
|
| 155 | + # Horizon down |
| 156 | + - alert: StepFiHorizonDown |
| 157 | + expr: stepfi_horizon_up == 0 |
| 158 | + for: 1m |
| 159 | + labels: |
| 160 | + severity: critical |
| 161 | + annotations: |
| 162 | + summary: 'Stellar Horizon endpoint is unreachable' |
| 163 | +``` |
| 164 | + |
| 165 | +### Alert thresholds summary |
| 166 | + |
| 167 | +| Metric | Warning | Critical | |
| 168 | +|--------------------|----------------|----------------| |
| 169 | +| Error rate | > 5% | > 10% | |
| 170 | +| p99 latency | > 3s | > 5s | |
| 171 | +| Indexer lag | > 500 ledgers | > 2000 ledgers | |
| 172 | +| Queue depth | > 1000 jobs | > 5000 jobs | |
| 173 | +| Horizon up | — | 0 (down) | |
| 174 | + |
| 175 | +--- |
| 176 | + |
| 177 | +## Grafana |
| 178 | + |
| 179 | +The dashboard JSON is at [`grafana/dashboard.json`](../grafana/dashboard.json). |
| 180 | +Import it into Grafana via **Dashboards → Import**. |
| 181 | + |
| 182 | +### Panels included |
| 183 | + |
| 184 | +1. **Request rate** — Rate of HTTP requests per second by status class |
| 185 | +2. **Error rate** — Percentage of 5xx responses |
| 186 | +3. **Latency (p50 / p95 / p99)** — Request duration quantiles |
| 187 | +4. **Indexer lag** — Ledger lag gauge & timeseries |
| 188 | +5. **Queue depths** — BullMQ waiting job counts per queue |
| 189 | +6. **Horizon health** — Up/down status |
| 190 | +7. **Database pool** — Open connections |
| 191 | +8. **Node.js resource usage** — Memory, CPU, event loop lag |
| 192 | + |
| 193 | +--- |
| 194 | + |
| 195 | +## Logging |
| 196 | + |
| 197 | +- **Format:** JSON structured logs in production, pretty-print in development |
| 198 | +- **Levels:** `trace`, `info`, `warn`, `error`, `fatal` |
| 199 | +- **Correlation:** Every log line includes a `correlationId` — pass the |
| 200 | + `x-correlation-id` or `x-request-id` header from the client |
| 201 | +- **Redaction:** The following fields are automatically redacted: |
| 202 | + `authorization`, `cookie`, `x-api-key`, `password`, `secret`, `token`, `refreshToken` |
| 203 | + |
| 204 | +**Log level configuration:** |
| 205 | +``` |
| 206 | +LOG_LEVEL=info # default: info in prod, trace in dev |
| 207 | +LOG_PRETTY=false # default: false in prod, true in dev |
| 208 | +``` |
| 209 | +
|
| 210 | +--- |
| 211 | +
|
| 212 | +## Sentry (optional) |
| 213 | +
|
| 214 | +`@sentry/nestjs` is installed but not configured. To enable: |
| 215 | +
|
| 216 | +1. Set `SENTRY_DSN` in environment |
| 217 | +2. Initialize Sentry in `main.ts`: |
| 218 | +
|
| 219 | +```typescript |
| 220 | +import * as Sentry from '@sentry/nestjs'; |
| 221 | +
|
| 222 | +Sentry.init({ |
| 223 | + dsn: process.env.SENTRY_DSN, |
| 224 | + environment: process.env.NODE_ENV || 'development', |
| 225 | +}); |
| 226 | +``` |
0 commit comments