b74d3f1b9b
## Summary
Implements ADR-0012 phase 1, BFF side. The SPA wiring is a separate phase-2 PR.
The BFF now emits structured JSON logs to stdout, tagged with `trace_id` / `span_id` from the active OTel context, and exports OTLP traces over HTTP/Protobuf to the Collector that already runs in the local-dev compose. Anything Nest, Express, HTTP-out, Prisma (Postgres) or `ioredis` does is auto-spanned. A `GET /api/health` liveness endpoint is added to round things out.
## What lands
**Runtime libs added** (production deps):
- `nestjs-pino`, `pino`, `pino-http` — structured logging
- `nestjs-cls` — request-scoped context
- `@opentelemetry/api` / `sdk-node` / `resources` / `semantic-conventions`
- `@opentelemetry/exporter-trace-otlp-proto` (HTTP/Protobuf, port 4318)
- `@opentelemetry/instrumentation-{http,express,nestjs-core,pg,ioredis,pino}` — curated, **no** `auto-instrumentations-node` mass-import (anti-bricolage)
Dev: `pino-pretty` (gated by `NODE_ENV`).
**Code:**
- `apps/portal-bff/src/observability/tracing.ts` — OTel `NodeSDK` bootstrap. Documents the load-order constraint inline (must be the very first import of `main.ts`). Pure side-effect module.
- `apps/portal-bff/src/observability/observability.module.ts` — composes `ClsModule` (UUID per request stored as `request_id`) and `LoggerModule` (`pino-pretty` in dev, raw JSON in prod, `LOG_LEVEL` env-driven, `/health` excluded from auto-logging, `X-Request-Id` honoured if inbound).
- `apps/portal-bff/src/health/{health.controller,health.module,health.controller.spec}.ts` — `GET /api/health` returning `{status, uptimeSeconds, service, version}`. Cheap liveness only — `/readiness` lands when dependencies have a readiness story.
- `apps/portal-bff/src/config/check-database-url.{ts,spec.ts}` — fail-fast validator called from `main.ts` before NestFactory boots. Catches the same family of bug that bit pgweb in #63: a literal special character in `POSTGRES_PASSWORD` that needs URL-encoding in `DATABASE_URL`. Prisma requires a URL string (no discrete-flag escape hatch), so early validation + a clear error message is the v1 mitigation. Six unit tests cover happy path, missing URL, wrong scheme, encoded special chars, literal `@` in password, malformed URL.
**Wiring:**
- `main.ts` imports `./observability/tracing` as line 1, then uses `app.get(Logger)` from `nestjs-pino` with `bufferLogs: true` so early-bootstrap lines are not lost.
- `app.module.ts` imports `ObservabilityModule` first, then `PrismaModule`, then `HealthModule`.
- `apps/portal-bff/.env.example` promotes `LOG_LEVEL`, `OTEL_SERVICE_NAME`, `OTEL_SERVICE_VERSION`, `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_PROTOCOL`, `OTEL_TRACES_SAMPLER` from the "future" comment to active settings — defaults target the local-dev Collector.
- Both `apps/portal-bff/.env.example` and `infra/local/.env.example` now spell out the URL-encoding constraint on `POSTGRES_PASSWORD` with the char-by-char encoding table (`@` → `%40`, etc.).
**ADR-0012 §Confirmation** rewritten to distinguish what landed in this PR from what is wired as the corresponding feature ADRs ship (CLS keys for `session_id` / `user_id_hash` / `audience`, `LOG_USER_ID_SALT` enforcement, redact list, custom spans, SPA-side SDK, full integration tests, prod Collector config).
## Trace ↔ log correlation
Automatic via `@opentelemetry/instrumentation-pino` — every Pino record gets `trace_id` and `span_id` injected from the active OTel context. No CLS gymnastics needed for that concern.
## Verification
```bash
pnpm exec nx run-many -t lint test build # 8 projects green
pnpm audit --audit-level=moderate # 0 vulnerabilities
./infra/local/dev.sh up observability # start Collector + Jaeger
cp apps/portal-bff/.env.example apps/portal-bff/.env
pnpm nx serve portal-bff
curl http://localhost:3000/api/health
# → {"status":"ok","uptimeSeconds":N,"service":"portal-bff","version":"dev"}
```
Then hit `GET http://localhost:3000/api` once or twice and open http://localhost:16686 — the corresponding spans appear in Jaeger, and Pino logs on stdout carry the matching `trace_id`.
## Test plan
- [ ] `nx run-many -t lint test build` green on this PR's CI run.
- [ ] `pnpm audit` clean.
- [ ] BFF boots, `/api/health` returns the expected JSON.
- [ ] Pino logs in dev are colourised one-liners; in prod they would be raw JSON (toggled by `NODE_ENV=production`).
- [ ] With the local-dev stack's `--profile observability` active, traces are visible in Jaeger UI.
- [ ] Each Pino log line for a request carries the same `trace_id` as the trace span in Jaeger.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #70
74 lines
3.3 KiB
Bash
74 lines
3.3 KiB
Bash
# BFF environment template
|
|
# Copy to .env (which is gitignored) and fill in actual values for local development.
|
|
# Production values are managed by the platform's secret manager (see future infrastructure ADR).
|
|
|
|
# Postgres connection (per ADR-0006)
|
|
# Local dev default: dockerised Postgres on port 5432, schema 'public'.
|
|
# Username / password / db must match infra/local/.env (POSTGRES_USER /
|
|
# POSTGRES_PASSWORD / POSTGRES_DB) — those are the source of truth,
|
|
# this is the BFF view of the same connection.
|
|
#
|
|
# IMPORTANT — URL encoding. The password is part of the URL userinfo
|
|
# segment, so any of these characters must be URL-encoded:
|
|
# @ → %40 # → %23 : → %3A / → %2F ? → %3F
|
|
# % → %25 & → %26 = → %3D + → %2B ; → %3B
|
|
# i.e. if your POSTGRES_PASSWORD is "p@ss#1", DATABASE_URL must read
|
|
# "postgresql://portal:p%40ss%231@localhost:5432/portal_dev?schema=public"
|
|
# The BFF aborts at boot with a clear error if it detects an unencoded
|
|
# special character (see apps/portal-bff/src/config/check-database-url.ts).
|
|
DATABASE_URL="postgresql://portal:portal_dev_change_me@localhost:5432/portal_dev?schema=public"
|
|
|
|
# Observability (per ADR-0012)
|
|
# All OTEL_* keys are honoured by the OpenTelemetry SDK directly — see
|
|
# apps/portal-bff/src/observability/tracing.ts for the bootstrap.
|
|
# Pino log level: 'info' in prod, 'debug' in dev (default if unset).
|
|
LOG_LEVEL=debug
|
|
OTEL_SERVICE_NAME=portal-bff
|
|
OTEL_SERVICE_VERSION=dev
|
|
# Default endpoint targets the Collector provisioned in
|
|
# infra/local/dev.compose.yml. The /v1/traces suffix is required by
|
|
# the HTTP/Protobuf transport.
|
|
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces
|
|
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
|
|
# v1 samples 100 % at the app; tail sampling is delegated to the
|
|
# Collector (per ADR-0012). Override only for spike investigations.
|
|
OTEL_TRACES_SAMPLER=always_on
|
|
|
|
# Future env vars introduced by upcoming phases / ADRs:
|
|
#
|
|
# Auth flow (ADR-0009):
|
|
# ENTRA_TENANT_ID
|
|
# ENTRA_CLIENT_ID
|
|
# ENTRA_CLIENT_SECRET (or ENTRA_CLIENT_CERT_PATH)
|
|
# ENTRA_ACCEPTED_TENANT_IDS
|
|
# ENTRA_REDIRECT_URI
|
|
# ENTRA_POST_LOGOUT_REDIRECT_URI
|
|
# SESSION_SECRET
|
|
#
|
|
# Sessions (ADR-0010):
|
|
# REDIS_URL (or REDIS_SENTINEL_HOSTS + REDIS_SENTINEL_NAME)
|
|
# REDIS_PASSWORD
|
|
# REDIS_TLS ('true' in prod)
|
|
# SESSION_ENCRYPTION_KEY (32-byte base64)
|
|
# SESSION_IDLE_TIMEOUT_SECONDS (default 1800)
|
|
# SESSION_ABSOLUTE_TIMEOUT_SECONDS (default 43200)
|
|
#
|
|
# MFA (ADR-0011):
|
|
# MFA_FRESHNESS_SECONDS (default 600)
|
|
#
|
|
# Observability — additional keys to be wired as features land:
|
|
# LOG_USER_ID_SALT (per-environment salt for hashing user_id
|
|
# in CLS context — needed once auth lands)
|
|
#
|
|
# Audit trail (ADR-0013):
|
|
# AUDIT_DATABASE_URL (separate creds, role 'audit_writer')
|
|
# AUDIT_ARCHIVER_DATABASE_URL (role 'audit_archiver', for the retention purge job)
|
|
# AUDIT_RETENTION_DAYS (default 365)
|
|
#
|
|
# Downstream API access (ADR-0014):
|
|
# OBO_CACHE_ENCRYPTION_KEY (32-byte base64, distinct from SESSION_ENCRYPTION_KEY)
|
|
# BFF_JWKS_PRIVATE_KEY_PATH
|
|
# BFF_JWKS_KID
|
|
# <SERVICE>_API_BASE_URL (per integrated downstream)
|
|
# <SERVICE>_TIMEOUT_MS (optional, defaults to 5000)
|