feat(api): foundation canon JCS — Pino+CLS+RFC9457+health+ping (Frente A)

Estabelece a fundação operacional de apps/api conforme STACK.md v2.2 e
CODING-RULES.md v2.0, substituindo o hello-world scaffolded.

- tracing.ts primeiro import (PGD-OBS-001) — stub OTel ativável por env
- EnvSchema Zod 4 com fail-fast (superRefine guarda prod) + EnvModule global
- nestjs-pino com redact LGPD (*.cpf|*.cardNumber|*.password|auth|cookie)
- main.ts hardenizado: helmet, CORS por env, compression, versionamento URI
  /api/v1, graceful shutdown
- ProblemDetailsFilter global (RFC 9457 application/problem+json), Zod -> 422
- Health endpoints /api/v1/health/{live,ready} com memory.checkHeap(350MB)
  (ready skeleton documenta K=3 LRU pool conforme PGD-OBS-003)
- WorkspaceModule via ClsModule.forRootAsync — requestId+workspaceId no CLS,
  idGenerator alinhado com pino-http para mesmo UUID em header e body
- GET /api/v1/ping retornando workspaceId+requestId (alvo de smoke test e
  futuro healthcheck do docker compose)
- ZodValidationPipe (nestjs-zod) como APP_PIPE global
- tsconfig.app.json target ES2023 (alinhado ao base)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 17:50:42 +00:00
parent 3a42723c71
commit 055f9f98f0
21 changed files with 1148 additions and 80 deletions

View File

@@ -0,0 +1,81 @@
import { Module } from '@nestjs/common';
import { LoggerModule as PinoLoggerModule } from 'nestjs-pino';
import { randomUUID } from 'node:crypto';
import type { IncomingMessage, ServerResponse } from 'node:http';
import { ConfigService } from '@nestjs/config';
import type { Env } from '../config/env.schema';
// CODING-RULES §09 (PGD-OBS-002): redact LGPD agressivo.
// Sempre cobrir: *.cpf, *.cardNumber, *.password, req.headers.authorization, req.headers.cookie.
const REDACT_PATHS = [
'*.cpf',
'*.cardNumber',
'*.password',
'*.passwordHash',
'*.token',
'*.secret',
'req.headers.authorization',
'req.headers.cookie',
'req.headers["set-cookie"]',
'res.headers["set-cookie"]',
];
@Module({
imports: [
PinoLoggerModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService<Env, true>) => {
const isProd = config.get('NODE_ENV', { infer: true }) === 'production';
const level = config.get('LOG_LEVEL', { infer: true });
const serviceName = config.get('OTEL_SERVICE_NAME', { infer: true });
return {
pinoHttp: {
level,
// pretty em dev; JSON estruturado em prod (Grafana LGTM consumirá).
transport: isProd
? undefined
: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:HH:MM:ss.l',
ignore: 'pid,hostname,req,res,responseTime',
messageFormat: '{msg} {req.method} {req.url} {res.statusCode} ({responseTime}ms)',
},
},
redact: {
paths: REDACT_PATHS,
censor: '[REDACTED]',
},
// Correlation ID: respeita header upstream ou gera UUID v4.
genReqId: (req: IncomingMessage, res: ServerResponse) => {
const headerVal =
req.headers['x-request-id'] ??
req.headers['x-correlation-id'] ??
req.headers['traceparent'];
const id = typeof headerVal === 'string' && headerVal.length > 0 ? headerVal : randomUUID();
res.setHeader('x-request-id', id);
return id;
},
customProps: () => ({ service: serviceName }),
// Reduz ruído em health checks.
customLogLevel: (_req, res, err) => {
if (err || res.statusCode >= 500) return 'error';
if (res.statusCode >= 400) return 'warn';
return 'info';
},
autoLogging: {
ignore: (req) => {
const url = req.url ?? '';
return url.startsWith('/api/v1/health');
},
},
},
};
},
}),
],
exports: [PinoLoggerModule],
})
export class LoggerModule {}