7f8db86a699be6cfe8e5e1f84f4541c0a2c8fa3a
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8f2cd4e068 |
feat(portal-shell): wire spa-side opentelemetry tracing (#72)
## Summary Phase 2 of ADR-0012 — closes the loop SPA → BFF → DB. After this PR, a single user action (initial page load, click, form submit) produces one trace whose root span is owned by the SPA and whose child spans cover the BFF request, Postgres queries through Prisma, and (eventually) Redis / downstream-API hops. ## What lands **Browser-side OTel libs** (production deps): - `@opentelemetry/sdk-trace-web` — browser tracer + provider - `@opentelemetry/exporter-trace-otlp-http` — OTLP/HTTP+JSON exporter - `@opentelemetry/instrumentation` — auto-instrumentation runtime - `@opentelemetry/instrumentation-fetch` — `fetch` + W3C `traceparent` propagation - `@opentelemetry/instrumentation-document-load` — initial-paint timings - `@opentelemetry/instrumentation-user-interaction` — click / keypress / submit No `@opentelemetry/context-zone`: the workspace is zoneless per ADR-0004; the default `StackContextManager` covers the auto-instrumented paths. Custom spans across `await` will need explicit `context.with(...)` plumbing — fine, encountered as code lands. **Code**: - [`apps/portal-shell/src/observability/tracing.ts`](apps/portal-shell/src/observability/tracing.ts) — `WebTracerProvider` bootstrap. Documents the load-order constraint inline (same pattern as the BFF: must be the very first import of `main.ts`, otherwise auto-instrumentations miss everything imported above). - `apps/portal-shell/src/main.ts` now imports the tracing module as line 1. **CORS plumbing** for end-to-end trace propagation: - BFF (`apps/portal-bff/src/main.ts`) calls `enableCors` with a minimal dev allowlist (`http://localhost:4200`) and explicit permission for the W3C `traceparent` / `tracestate` headers. The full security-grade CORS (per-environment allowlists, helmet, cookie-session, CSRF) belongs to the future phase-2 security ADR — this PR adds the strict minimum for the SPA→BFF trace context to survive cross-origin pre-flight. - OTel Collector (`infra/local/otel-collector.yaml`) gains a `cors` block on its OTLP/HTTP receiver so the browser's own OTLP POST clears its pre-flight. **ADR-0012 §Confirmation** rewritten: a new "Wired in the SPA foundation PR (phase 2)" block enumerates what landed here; the carry-over "Wired as features land" list drops the SPA-side SDK item and adds a follow-up note about the security-grade CORS. ## Verification ```bash pnpm exec nx run-many -t lint test build # 8 projects green pnpm audit # 0 vulns ./infra/local/dev.sh up observability # bring up Collector + Jaeger ./infra/local/dev.sh # (separately, BFF stack — your choice) pnpm nx serve portal-bff # localhost:3000 pnpm nx serve portal-shell # localhost:4200 ``` Open http://localhost:4200 → a `document_load` trace appears in http://localhost:16686 with `service.name=portal-shell`. From DevTools, run `fetch('http://localhost:3000/api/health').then(r => r.json())` → a fetch span appears with a child BFF span on the same trace. ## Test plan - [ ] CI green on this PR. - [ ] After local up, `document_load` span visible in Jaeger UI for the SPA. - [ ] Cross-origin fetch from SPA carries `traceparent` (visible in Network tab) and produces a single end-to-end trace SPA → BFF in Jaeger. - [ ] DevTools console shows no CORS warnings about `traceparent`, `tracestate`, or the `localhost:4318/v1/traces` POST. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #72 |
||
|
|
b74d3f1b9b |
feat(portal-bff): observability foundations (Pino + CLS + OTel) (#70)
## 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
|
||
|
|
0e58e32d29 |
chore: relocate ADRs from decisions/ to docs/decisions/ to consolidate documentation
Move the ADR folder under docs/ alongside the rest of the project
documentation. Convention (flat folder, globally-sequential 4-digit
numbering, tags-based categorization, MADR 4.0.0 format) is unchanged
- only the path moved.
- git mv decisions docs/decisions preserves history for all 18 ADRs +
README + template (19 files renamed in this commit).
- ADR-0001 amended in-place with a dated note documenting the
relocation. Status remains 'accepted' - the location detail
changed, the decision did not.
- All cross-references updated:
- CLAUDE.md (~17 ADR links + 3 mentions of decisions/ in the Project
rules section)
- docs/README.md (now references decisions/ as a sibling under docs/)
- docs/setup/03-angular-nx-monorepo.md (paths shortened from
../../decisions/ to ../decisions/, since setup/ and decisions/ are
now both inside docs/)
- docs/decisions/0003 ../CLAUDE.md adjusted to ../../CLAUDE.md
(one extra level of nesting)
- docs/decisions/template.md mention of the README path
- notes/asvs-level-decision-briefing-rssi.md mention of the index
Sanity verified: every ADR link in CLAUDE.md, docs/setup/03, and
docs/decisions/0001 resolves to an existing file. pnpm nx run-many
-t lint passes on 8 projects.
|