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
147 lines
4.5 KiB
JSON
147 lines
4.5 KiB
JSON
{
|
|
"name": "apf-portal",
|
|
"version": "0.0.0",
|
|
"license": "MIT",
|
|
"packageManager": "pnpm@10.33.4",
|
|
"scripts": {
|
|
"prepare": "husky",
|
|
"ci:check": "pnpm exec nx affected -t format:check lint test build",
|
|
"ci:audit": "pnpm audit --audit-level=moderate",
|
|
"ci:commits": "pnpm exec commitlint --from ${COMMIT_LINT_FROM:-origin/main} --to HEAD --verbose",
|
|
"ci:perf": "pnpm exec nx build portal-shell --configuration=production && pnpm exec lhci autorun --config=./lighthouserc.js"
|
|
},
|
|
"private": true,
|
|
"lint-staged": {
|
|
"!(pnpm-lock).{ts,tsx,js,jsx,html,scss,css,md,json,yml,yaml}": [
|
|
"prettier --write"
|
|
]
|
|
},
|
|
"pnpm": {
|
|
"onlyBuiltDependencies": [
|
|
"@nestjs/core",
|
|
"@parcel/watcher",
|
|
"@prisma/client",
|
|
"@prisma/engines",
|
|
"@swc/core",
|
|
"esbuild",
|
|
"less",
|
|
"lmdb",
|
|
"msgpackr-extract",
|
|
"nx",
|
|
"prisma",
|
|
"unrs-resolver"
|
|
],
|
|
"overrides": {
|
|
"axios@<1.15.2": ">=1.15.2",
|
|
"@angular-devkit/core>ajv@<8.18.0": ">=8.18.0",
|
|
"brace-expansion@<5.0.5": ">=5.0.5",
|
|
"follow-redirects@<1.16.0": ">=1.16.0",
|
|
"ip-address@<10.1.1": ">=10.1.1",
|
|
"tmp@<0.2.4": ">=0.2.4",
|
|
"yaml@<2.8.3": ">=2.8.3"
|
|
}
|
|
},
|
|
"prisma": {
|
|
"schema": "apps/portal-bff/prisma/schema.prisma"
|
|
},
|
|
"devDependencies": {
|
|
"@analogjs/vite-plugin-angular": "~2.5.0",
|
|
"@analogjs/vitest-angular": "~2.5.0",
|
|
"@angular-devkit/core": "~21.2.0",
|
|
"@angular-devkit/schematics": "~21.2.0",
|
|
"@angular/build": "~21.2.0",
|
|
"@angular/cli": "~21.2.0",
|
|
"@angular/compiler-cli": "~21.2.0",
|
|
"@angular/language-service": "~21.2.0",
|
|
"@commitlint/cli": "^20.5.3",
|
|
"@commitlint/config-conventional": "^20.5.3",
|
|
"@eslint/js": "^9.8.0",
|
|
"@lhci/cli": "^0.15.1",
|
|
"@nestjs/schematics": "^11.0.0",
|
|
"@nestjs/testing": "^11.0.0",
|
|
"@nx/angular": "^22.7.1",
|
|
"@nx/devkit": "22.7.1",
|
|
"@nx/eslint": "^22.7.1",
|
|
"@nx/eslint-plugin": "22.7.1",
|
|
"@nx/jest": "22.7.1",
|
|
"@nx/js": "22.7.1",
|
|
"@nx/nest": "^22.7.1",
|
|
"@nx/node": "22.7.1",
|
|
"@nx/playwright": "22.7.1",
|
|
"@nx/vite": "^22.7.1",
|
|
"@nx/vitest": "22.7.1",
|
|
"@nx/web": "22.7.1",
|
|
"@nx/webpack": "22.7.1",
|
|
"@oxc-project/runtime": "^0.129.0",
|
|
"@playwright/test": "^1.36.0",
|
|
"@schematics/angular": "~21.2.0",
|
|
"@swc-node/register": "1.11.1",
|
|
"@swc/core": "1.15.33",
|
|
"@swc/helpers": "0.5.21",
|
|
"@tailwindcss/postcss": "^4.2.4",
|
|
"@types/jest": "^30.0.0",
|
|
"@types/node": "24.12.3",
|
|
"@typescript-eslint/utils": "^8.40.0",
|
|
"@vitest/coverage-v8": "~4.1.0",
|
|
"@vitest/ui": "~4.1.0",
|
|
"angular-eslint": "^21.2.0",
|
|
"eslint": "^9.8.0",
|
|
"eslint-config-prettier": "^10.0.0",
|
|
"eslint-plugin-playwright": "^1.6.2",
|
|
"husky": "^9.1.7",
|
|
"jest": "^30.0.2",
|
|
"jest-environment-node": "^30.0.2",
|
|
"jest-util": "^30.0.2",
|
|
"jsdom": "^29.0.0",
|
|
"jsonc-eslint-parser": "^2.1.0",
|
|
"lint-staged": "^17.0.0",
|
|
"nx": "22.7.1",
|
|
"pino-pretty": "^13.1.3",
|
|
"postcss": "^8.5.12",
|
|
"prettier": "^3.8.1",
|
|
"prisma": "^6.19.3",
|
|
"tailwindcss": "^4.2.4",
|
|
"ts-jest": "^29.4.0",
|
|
"ts-node": "10.9.2",
|
|
"tslib": "^2.3.0",
|
|
"typescript": "~5.9.2",
|
|
"typescript-eslint": "^8.40.0",
|
|
"vite": "^8.0.0",
|
|
"vitest": "^4.0.8",
|
|
"webpack-cli": "^5.1.4"
|
|
},
|
|
"dependencies": {
|
|
"@angular/common": "~21.2.0",
|
|
"@angular/compiler": "~21.2.0",
|
|
"@angular/core": "~21.2.0",
|
|
"@angular/forms": "~21.2.0",
|
|
"@angular/platform-browser": "~21.2.0",
|
|
"@angular/router": "~21.2.0",
|
|
"@nestjs/common": "^11.0.0",
|
|
"@nestjs/core": "^11.0.0",
|
|
"@nestjs/platform-express": "^11.0.0",
|
|
"@opentelemetry/api": "^1.9.1",
|
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.217.0",
|
|
"@opentelemetry/instrumentation-express": "^0.65.0",
|
|
"@opentelemetry/instrumentation-http": "^0.217.0",
|
|
"@opentelemetry/instrumentation-ioredis": "^0.65.0",
|
|
"@opentelemetry/instrumentation-nestjs-core": "^0.63.0",
|
|
"@opentelemetry/instrumentation-pg": "^0.69.0",
|
|
"@opentelemetry/instrumentation-pino": "^0.63.0",
|
|
"@opentelemetry/resources": "^2.7.1",
|
|
"@opentelemetry/sdk-node": "^0.217.0",
|
|
"@opentelemetry/semantic-conventions": "^1.40.0",
|
|
"@prisma/client": "^6.19.3",
|
|
"axios": "^1.6.0",
|
|
"class-transformer": "^0.5.1",
|
|
"class-validator": "^0.15.1",
|
|
"nestjs-cls": "^6.2.0",
|
|
"nestjs-pino": "^4.6.1",
|
|
"nestjs-prisma": "^0.27.0",
|
|
"pino": "^10.3.1",
|
|
"pino-http": "^11.0.0",
|
|
"reflect-metadata": "^0.2.0",
|
|
"rxjs": "~7.8.0"
|
|
}
|
|
}
|