|
| 1 | +# Observability Guide |
| 2 | + |
| 3 | +## Metrics — `/metrics` |
| 4 | + |
| 5 | +The `/metrics` endpoint (not prefixed with `/api`) exposes Prometheus text format. |
| 6 | +Restrict it at the ingress/firewall level — it must not be publicly reachable. |
| 7 | + |
| 8 | +### HTTP metrics |
| 9 | + |
| 10 | +| Metric | Type | Labels | Description | |
| 11 | +|---|---|---|---| |
| 12 | +| `http_request_duration_seconds` | Histogram | `method`, `route`, `status_code` | Request latency. Buckets: 10 ms → 10 s | |
| 13 | +| `http_requests_total` | Counter | `method`, `route`, `status_code` | Total requests | |
| 14 | +| `http_5xx_errors_total` | Counter | `method`, `route` | 5xx responses only | |
| 15 | + |
| 16 | +### RPC metrics |
| 17 | + |
| 18 | +| Metric | Type | Labels | Description | |
| 19 | +|---|---|---|---| |
| 20 | +| `rpc_call_duration_seconds` | Histogram | `rpc_method`, `status` | Soroban RPC call latency | |
| 21 | +| `rpc_calls_total` | Counter | `rpc_method`, `status` | Total RPC calls | |
| 22 | +| `rpc_errors_total` | Counter | `rpc_method`, `error_type` | RPC errors by type | |
| 23 | + |
| 24 | +`rpc_method` values: `simulate_generate_premium`, `build_initiate_policy`, |
| 25 | +`build_file_claim`, `send_transaction`, `get_events`, `get_latest_ledger`. |
| 26 | + |
| 27 | +`error_type` values: `client_error`, `unavailable`, `unknown`. |
| 28 | + |
| 29 | +### Cardinality notes |
| 30 | + |
| 31 | +- `route` is normalised: numeric path segments → `:id`, UUIDs → `:uuid`, |
| 32 | + Stellar addresses → `:address`. Raw URLs are never used as labels. |
| 33 | +- `status_code` is the exact HTTP code (200, 400, 404, 500…). The set is |
| 34 | + bounded so cardinality is safe. |
| 35 | +- Never add wallet addresses, policy IDs, or claim IDs as metric labels. |
| 36 | + |
| 37 | +--- |
| 38 | + |
| 39 | +## Structured JSON Logs |
| 40 | + |
| 41 | +All log entries are newline-delimited JSON written to stdout. |
| 42 | +Ship to your centralised stack (Loki, CloudWatch, Datadog, etc.) via the |
| 43 | +container log driver. |
| 44 | + |
| 45 | +### Log field dictionary |
| 46 | + |
| 47 | +| Field | Type | Description | |
| 48 | +|---|---|---| |
| 49 | +| `timestamp` | ISO 8601 | UTC time of the log entry | |
| 50 | +| `level` | string | `error` / `warn` / `info` / `debug` | |
| 51 | +| `message` | string | Human-readable summary | |
| 52 | +| `service` | string | Always `niffyinsure-api` | |
| 53 | +| `requestId` | string | Correlation ID — propagated from `x-request-id` header or generated as a UUID | |
| 54 | +| `method` | string | HTTP verb (GET, POST, …) | |
| 55 | +| `url` | string | Request path only — query string is omitted to avoid leaking tokens | |
| 56 | +| `statusCode` | number | HTTP response status | |
| 57 | +| `durationMs` | number | Request duration in milliseconds | |
| 58 | +| `ip` | string | Client IP address | |
| 59 | +| `userAgent` | string | `User-Agent` header value | |
| 60 | +| `context` | string | NestJS class / module name | |
| 61 | +| `stack` | string | Error stack trace (error level only) | |
| 62 | +| `rpcMethod` | string | Soroban RPC method name (RPC log entries only) | |
| 63 | +| `rpcStatus` | string | `success` or `error` (RPC log entries only) | |
| 64 | +| `contentLength` | number | Response body size in bytes | |
| 65 | + |
| 66 | +### Fields intentionally omitted |
| 67 | + |
| 68 | +- `Authorization` / `Cookie` / `x-api-key` headers — always `[REDACTED]` |
| 69 | +- Request and response bodies — never logged |
| 70 | +- IPFS file contents — never logged |
| 71 | +- Private keys, seeds, mnemonics, Ed25519 signatures |
| 72 | +- Full wallet addresses in log messages (use short prefix for debugging) |
| 73 | + |
| 74 | +### Request ID propagation |
| 75 | + |
| 76 | +Every request receives a `requestId`: |
| 77 | +1. If the client sends `x-request-id`, that value is used. |
| 78 | +2. Otherwise a UUID v4 is generated. |
| 79 | + |
| 80 | +The ID is echoed back in the `x-request-id` response header and included in |
| 81 | +every log entry and error response body for end-to-end correlation. |
| 82 | + |
| 83 | +--- |
| 84 | + |
| 85 | +## Grafana Dashboard |
| 86 | + |
| 87 | +Import `docs/grafana-dashboard.json` into Grafana (Dashboards → Import). |
| 88 | +Select your Prometheus datasource when prompted. |
| 89 | + |
| 90 | +Panels: |
| 91 | +- Request rate by route/method |
| 92 | +- HTTP latency p50 / p95 / p99 |
| 93 | +- 5xx error rate |
| 94 | +- RPC call rate by method |
| 95 | +- RPC error rate |
| 96 | +- RPC latency p95 |
| 97 | +- Node.js heap usage |
| 98 | +- Event loop lag |
| 99 | + |
| 100 | +--- |
| 101 | + |
| 102 | +## Alerting |
| 103 | + |
| 104 | +Load `docs/prometheus-alerts.yml` into your Prometheus `rule_files`. |
| 105 | + |
| 106 | +| Alert | Threshold | Severity | |
| 107 | +|---|---|---| |
| 108 | +| `High5xxRate` | > 0.5 errors/s for 2 min | critical | |
| 109 | +| `HighRpcErrorRate` | > 0.2 errors/s for 2 min | warning | |
| 110 | +| `HighP99Latency` | p99 > 3 s for 5 min | warning | |
| 111 | +| `HighRpcP95Latency` | p95 > 10 s for 5 min | warning | |
| 112 | + |
| 113 | +--- |
| 114 | + |
| 115 | +## OpenTelemetry Extension Point |
| 116 | + |
| 117 | +`AppLoggerService.structured()` is the single place to inject OTel trace |
| 118 | +context. When you add `@opentelemetry/sdk-node`: |
| 119 | + |
| 120 | +```ts |
| 121 | +// In app-logger.service.ts — structured() |
| 122 | +import { trace } from '@opentelemetry/api'; |
| 123 | +const span = trace.getActiveSpan(); |
| 124 | +const traceId = span?.spanContext().traceId; |
| 125 | +const spanId = span?.spanContext().spanId; |
| 126 | +this.winston.log(level, message, { ...fields, traceId, spanId }); |
| 127 | +``` |
| 128 | + |
| 129 | +Similarly, `MetricsService.recordHttpRequest` / `recordRpcCall` map directly |
| 130 | +to OTel `Meter` histogram/counter calls — swap the prom-client calls for OTel |
| 131 | +Meter API calls when you're ready to migrate. |
0 commit comments