Commit Graph

2 Commits

Author SHA1 Message Date
Julien Gautier 2e663e1513 feat(portal-bff): observability foundations (Pino + CLS + OTel)
CI / commits (pull_request) Successful in 1m45s
CI / scan (pull_request) Successful in 1m55s
CI / check (pull_request) Successful in 2m8s
CI / a11y (pull_request) Successful in 1m10s
CI / perf (pull_request) Successful in 2m55s
Implements ADR-0012 phase 1, BFF side. The SPA wiring is a
separate phase-2 PR.

Runtime libraries added (production deps):

- nestjs-pino, pino, pino-http   structured JSON logging
- nestjs-cls                     request-scoped context
- @opentelemetry/api / sdk-node / resources / semantic-conventions
- @opentelemetry/exporter-trace-otlp-proto   OTLP HTTP/Protobuf
- @opentelemetry/instrumentation-{http,express,nestjs-core,pg,
  ioredis,pino}                  curated, no `auto-instrumentations-
                                 node` mass-import (anti-bricolage)

Dev dep: 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`). Exports nothing — pure
  side-effect file.
- 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).
- apps/portal-bff/src/health/{health.controller,health.module}.ts
  — `GET /api/health` returning `{status, uptimeSeconds, service,
  version}`. Cheap liveness only — no DB / Redis check; that
  belongs to a future `/readiness` once dependencies have a
  readiness story.
- apps/portal-bff/src/config/check-database-url.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
  char in POSTGRES_PASSWORD that needs URL-encoding in
  DATABASE_URL. Prisma requires a URL string, so we cannot use
  discrete CLI flags here — early validation + 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 now `import`s `./observability/tracing` as its first
  line, calls `assertDatabaseUrl()`, then bootstraps Nest with
  `app.useLogger(app.get(Logger))` from nestjs-pino with
  `bufferLogs: true` so early-bootstrap lines are not lost.
- app.module.ts imports ObservabilityModule first, then the
  PrismaModule, then HealthModule.
- .env.example (BFF and infra/local) document the URL-encoding
  constraint on POSTGRES_PASSWORD with the exact char-by-char
  encoding table.

Trace ↔ log correlation is automatic via
`@opentelemetry/instrumentation-pino`: every Pino record gets
`trace_id` / `span_id` injected from the active OTel context.
No CLS gymnastics needed for that specific concern.

ADR-0012 §Confirmation rewritten to clearly distinguish what
landed in this PR (phase 1) from what is wired as the
corresponding feature ADRs ship (CLS keys for session/user/
audience, LOG_USER_ID_SALT, redact list, custom spans, SPA-side
SDK, full integration tests, prod Collector config).

Verified locally:
- pnpm exec nx run-many -t lint test build → 8 projects green,
  4 test suites for portal-bff, 9 unit tests pass.
- `pnpm nx serve portal-bff` boots, `curl /api/health` returns
  the expected JSON; logs are pretty-printed JSON one-liners on
  stdout in dev.
- `pnpm audit --audit-level=moderate` reports 0 vulnerabilities.
2026-05-09 22:21:51 +02:00
julien 0f00d6d93f feat(infra): add local-dev Docker Compose stack (#57)
CI / commits (push) Has been skipped
CI / check (push) Successful in 1m12s
CI / scan (push) Successful in 1m20s
CI / a11y (push) Successful in 41s
CI / perf (push) Successful in 2m9s
## Summary
Bring up Postgres + Redis + OTel Collector in one command so contributors can run the BFF end-to-end without manually wiring each service. Replaces the throwaway `docker run postgres:17-alpine` one-liner that was in `docs/development.md` §3.

### What lands
- **`infra/local/dev.compose.yml`** — three core services (`postgres:17.2-alpine`, `redis:7.4-alpine`, `otel/opentelemetry-collector-contrib:0.115.0`) plus two viewers gated behind Compose profiles:
  - `--profile dbtools` → `sosedoff/pgweb:0.16.2` (Postgres GUI on port 8081)
  - `--profile observability` → `jaegertracing/all-in-one:1.62` (Jaeger UI on 16686)
  - All ports overridable via `.env`. State in named volumes. Healthchecks on data services.
- **`infra/local/.env.example`** — credentials + ports template. `POSTGRES_PASSWORD` and `REDIS_PASSWORD` are mandatory (compose refuses to boot without them); other keys default sensibly.
- **`infra/local/init/postgres/01-init.sql`** — bootstrap SQL per **ADR-0013**: `audit_owner` / `audit_writer` / `audit_reader` / `audit_archiver` roles + `audit` schema. Default privileges encode the append-only contract (INSERT to writer, SELECT to reader, DELETE to archiver, no UPDATE/TRUNCATE to anyone). Applied on first Postgres boot only; documented re-run procedure.
- **`infra/local/otel-collector.yaml`** — pipeline: OTLP gRPC/HTTP → batch → debug exporter (always) + forward to `jaeger:4317`. When the observability profile is off, the Jaeger export logs warn-level retries but doesn't block the debug pipeline.

### Surrounding doc updates
- **`infra/README.md`** — new "Local-dev stack" section: service inventory, port table, first-time setup walkthrough, persistence/bootstrap-replay tips. The previous `local/` placeholder line is removed.
- **`docs/development.md`** §3 — rewritten to walk through the compose-based setup; cross-links to `infra/README.md` for the full reference. Roadmap entry for "Local infra recipe" removed from §8 (now implemented); "Observability dev-loop" line adjusted to point at the new Jaeger profile.

### Out of scope
- **Production parity** — HA Postgres, Redis Sentinel, real OTel backend (Tempo / Loki / etc.) — defer to the on-prem infrastructure ADR (phase 3b). The dev-only nature of this stack is called out explicitly in `infra/README.md`.
- **Wiring the BFF** to actually use these endpoints (NestJS config, Prisma datasource URL, OTel SDK init) — that's the **B — Observability foundations** chantier, next up.

## Test plan
- [ ] `cd infra/local && cp .env.example .env && docker compose -f dev.compose.yml up -d` → all three core services come up healthy; verify with `docker compose ps`.
- [ ] `psql postgres://portal:<pwd>@localhost:5432/portal_dev -c "\dn"` shows the `audit` schema; `\dg` shows the four audit roles.
- [ ] `redis-cli -a <pwd> PING` → `PONG`.
- [ ] Send a fake OTLP trace via grpcurl → see it printed by `docker compose logs otel-collector`.
- [ ] `--profile dbtools up -d` → http://localhost:8081 shows pgweb UI, can navigate to the audit schema.
- [ ] `--profile observability up -d` → http://localhost:16686 shows Jaeger UI; collector logs no longer report Jaeger export retries.
- [ ] `docker compose down -v` cleanly removes everything; next `up -d` re-runs the bootstrap SQL.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #57
2026-05-08 19:23:43 +02:00