Observabilidade padrão CIAg para NestJS (@ciag/observability/nest) e Angular
(@ciag/observability/angular). O app fala só padrões abertos — OpenTelemetry (OTLP) para
traces/métricas/logs e protocolo Sentry para erros —, então trocar a plataforma
(Grafana ↔ SigNoz, GlitchTip ↔ Sentry) é configuração, nunca código do app.
- Desligado sem configuração. Sem
OTEL_EXPORTER_OTLP_ENDPOINTnada é exportado (e o SDK do OpenTelemetry nem é carregado); semERRORS_DSNnenhum erro é enviado (o Sentry nem é carregado). Local = custo zero e zero aviso. - Nunca derruba o app. Coletor/GlitchTip fora do ar, URL ou DSN inválidos: a API sobe e responde; só um aviso no log (no máximo 1 por minuto por motivo).
- PII não sai do app (LGPD) — ver LGPD.
npm i @ciag/observability \
@opentelemetry/api @opentelemetry/api-logs @opentelemetry/core @opentelemetry/resources \
@opentelemetry/semantic-conventions @opentelemetry/sdk-trace-node @opentelemetry/sdk-trace-base \
@opentelemetry/sdk-metrics @opentelemetry/sdk-logs @opentelemetry/exporter-trace-otlp-http \
@opentelemetry/exporter-metrics-otlp-http @opentelemetry/exporter-logs-otlp-http \
@opentelemetry/instrumentation @opentelemetry/instrumentation-http @opentelemetry/instrumentation-express \
@opentelemetry/instrumentation-nestjs-core @opentelemetry/instrumentation-mysql2 \
@prisma/instrumentation@<mesma versão do prisma> @sentry/nestjs pino(As dependências de cada lado são peerDependencies opcionais: quem usa só o Nest não instala
Angular, e vice-versa. As versões testadas estão em devDependencies deste pacote.)
-
src/instrumentation.ts(arquivo próprio, carregado ANTES do Nest):import { initTelemetry } from '@ciag/observability/nest'; // Padrões do código; OTEL_SERVICE_NAME / APP_RELEASE / DEPLOYMENT_ENVIRONMENT têm prioridade. initTelemetry({ serviceName: 'teatime-api', serviceVersion: pkg.version });
-
Primeira linha do
src/main.ts:import './instrumentation';e o logger JSON nocreate:import './instrumentation'; import { observabilityLogger } from '@ciag/observability/nest'; const app = await NestFactory.create(AppModule, { logger: observabilityLogger() });
-
AppModule:imports: [ObservabilityModule.forRoot(), ...]. -
CORS (front em outra origem): permita os cabeçalhos de trace e exponha o request id:
allowedHeaders: [...os atuais, 'traceparent', 'tracestate', 'baggage', 'sentry-trace', 'X-Request-Id'], exposedHeaders: ['X-Request-Id'],
npm i @ciag/observability @sentry/angular// app.config.ts
provideObservability({
serviceName: 'teatime-web',
release: VERSION,
environment: environment.DEPLOYMENT_ENVIRONMENT, // 'prod' | 'hml' | 'local'
errorsDsn: environment.ERRORS_DSN, // '' = desligado
apiOrigins: [environment.API_URL], // SÓ estes recebem traceparent
tracesSampleRate: 0.2,
}),- Mensagem de erro de API:
errorReference(err)devolve oerrorId(ou oX-Request-Id) para mostrar "Código do erro: …". - Login:
setObservabilityUser(userId); logout:clearObservabilityUser().
Sem DSN, provideObservability devolve uma lista vazia de providers (o app fica idêntico). Com DSN, o
SDK do Sentry vem num chunk separado carregado depois do bootstrap — não pesa no JS inicial. Erros
anteriores ao carregamento ficam numa fila (até 20) e são enviados quando o SDK chega.
| API | Faz |
|---|---|
initTelemetry(opts?) |
OTel: traces (http, express, nestjs-core, mysql2, Prisma), métrica http.server.request.duration por http.route/status, logs por OTLP/HTTP; erros por @sentry/nestjs (release = versão, environment). Shutdown com flush em SIGTERM/SIGINT (só quando algo está ligado). |
ObservabilityModule.forRoot({ userId?, ignorePaths? }) |
Middleware de request id (aceita X-Request-Id válido, senão UUID; devolve no cabeçalho); UMA linha de log por requisição (method, route padrão /projects/:id, status, duration_ms, userId, ip respeitando trust proxy, requestId, trace_id); filtro global de exceções. |
observabilityLogger() |
LoggerService do Nest sobre pino: todos os Logger existentes ([SharepointService], ExceptionsHandler…) saem em JSON com requestId/trace_id/span_id. |
| Filtro de exceções | Delega ao BaseExceptionFilter do próprio Nest (mesmo corpo, mesmo status, mesmo log) e só acrescenta errorId (= request id) ao JSON. 5xx e erros inesperados vão ao GlitchTip com as tags errorId, route, method, status, trace_id. 4xx não são erro; 401/403/429 viram contador ciag.http.server.rejected + breadcrumb. |
Exemplo de linha:
{"level":"info","time":"2026-10-12T18:28:12.327Z","context":"HTTP","requestId":"9c02…","method":"GET","route":"/projects/:id","status":200,"duration_ms":12.4,"ip":"10.0.0.7","userId":"6f1c…","trace_id":"a228…","span_id":"c3c0…","msg":"GET /projects/:id 200 12.4ms"}| API | Faz |
|---|---|
provideObservability(cfg) |
@sentry/angular: ErrorHandler (mantém o console.error padrão), tracing do roteador (nomes de rota parametrizados) e das chamadas HTTP com traceparent só para apiOrigins (tracePropagationTargets com regex exata + propagateTraceparent: true), sem replay, sem sessões, dataCollection restritivo, beforeSend com redação. |
errorReference(err) |
errorId do corpo de erro ou X-Request-Id; null se não houver. |
setObservabilityUser(id) / clearObservabilityUser() |
Usuário só por id. |
observabilityInterceptor / ObservabilityHttpInterceptor |
Breadcrumb HTTP <status> com errorId e URL redigida (já incluído por provideObservability). |
| Variável | Lado | Padrão | Uso |
|---|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
back | vazio = desligado | URL base OTLP/HTTP do coletor (Grafana Alloy), ex. http://alloy:4318. |
OTEL_SERVICE_NAME |
back | opção serviceName |
service.name (ex. teatime-api). Variáveis têm prioridade sobre as opções do código. |
APP_RELEASE |
back | opção serviceVersion, senão npm_package_version |
service.version e release do GlitchTip. |
DEPLOYMENT_ENVIRONMENT |
back | opção environment, senão local |
prod | hml | local (aceita production, staging). |
ERRORS_DSN |
back/front | vazio = desligado | DSN do projeto no GlitchTip. No front entra no build (build-arg). |
OTEL_TRACES_SAMPLER_ARG |
back | 1 |
Fração de traces iniciados no back (os que vêm do front seguem a decisão do front). |
OTEL_TRACES_EXPORTER / OTEL_METRICS_EXPORTER / OTEL_LOGS_EXPORTER |
back | otlp |
none desliga um sinal (ex.: logs já coletados do stdout pelo Alloy). |
OTEL_METRIC_EXPORT_INTERVAL |
back | 60000 |
ms entre envios de métricas. |
OTEL_SDK_DISABLED |
back | — | true desliga toda a telemetria mesmo com endpoint. |
ERRORS_SAMPLE_RATE |
back | 1 |
Fração de erros enviados. |
LOG_LEVEL |
back | info |
fatal…trace. |
Os exportadores também respeitam as variáveis padrão OTEL_EXPORTER_OTLP_HEADERS (ex.: token do coletor).
- Recurso:
service.name,service.version,deployment.environment(prod|hml|local),service.namespace = ciag— iguais em todos os projetos, para dashboards/alertas comuns. - Correlação: cabeçalho
X-Request-Idem toda resposta;errorId(= request id) no corpo de erro;trace_id/span_idem cada linha de log; W3Ctraceparentdo front para a API. - Rotas sempre no padrão do framework (
/projects/:id), nunca a URL com ids. /healthnão gera trace nem linha de log (opçãoignorePaths).
Nunca saem do app (logs, atributos de span, eventos de erro e breadcrumbs):
- valores das chaves
authorization,cookie,set-cookie,senha,password,newPassword,confirmedNewPassword,currentPassword,token,accessToken,refreshToken,verificationToken,secret,clientSecret,apiKey(qualquer profundidade, sem diferenciar caixa,-ou_); Bearer …/Basic …e JWTs soltos em texto; parâmetros de query sensíveis (?token=);- e-mails em texto são mascarados:
ana.souza@ciag.org.br→a***@ciag.org.br; - usuário identificado só por id; corpo de requisição não é logado nem enviado; cookies e
cabeçalhos HTTP não são coletados pelo Sentry (
dataCollection).
Retenção e acesso ficam na plataforma (GlitchTip/Loki/Tempo: 15–30 dias, acesso restrito).
Até a publicação no npm, os apps consomem o tarball versionado no próprio repositório:
npm run build && npm pack # gera ciag-observability-0.1.0.tgz
cp ciag-observability-0.1.0.tgz <app>/vendor/
# package.json do app: "@ciag/observability": "file:vendor/ciag-observability-0.1.0.tgz"Funciona no Docker/CI sem credencial de registry. Depois de publicado (npm publish --access public):
npm i @ciag/observability@0.1.0 --save-exact, apague vendor/ciag-observability-*.tgz e confira que o
lockfile resolve para registry.npmjs.org.
npm run check # typecheck + testes (vitest) + build (tsup) + teste de contratotest/*.test.ts: redação com corpos reais (login, troca de senha), módulo Nest real (supertest), lado Angular sem navegador (injector do Angular em Node).scripts/contract-test.cjs: app Nest real (CommonJS, a partir dedist/) contra receptores em memória de OTLP/HTTP e do protocolo Sentry. Prova recurso,traceparentde entrada,http.route, métrica, log OTLP com o mesmotrace_id, evento de erro comerrorId, ausência de PII, endpoint fora do ar, URL/DSN inválidos e modo desligado.
Decisões:
- Sem NodeSDK (
@opentelemetry/sdk-node): os mesmos componentes (TracerProvider, MeterProvider, LoggerProvider, exportadores OTLP/HTTP) montados à mão, carregados só quando há endpoint — o sdk-node puxaria gRPC, Zipkin, Prometheus e Jaeger para dentro de todo app. - ESM + CJS no lado Node: apps Nest compilam para CommonJS;
requiredo pacote precisa funcionar semrequire(esm). - Angular sem decorators (
makeEnvironmentProviders,inject(), providers por factory): build com tsup, sem ng-packagr. traceLifecycle: 'static'no front: o@sentry/*11 envia spans em streaming por padrão, formato que o GlitchTip 6.2.6 aceita (HTTP 200) mas descarta; comstaticas navegações chegam como transações (conferido no E2E:/projects/project-list/ ; navigation).- CSP só com
report-uri(semreport-to): o endpoint de segurança do GlitchTip 6.2.6 aceita o formatoapplication/csp-report(201) e recusa o da Reporting APIapplication/reports+json(422); comreport-too Chrome passaria a usar só o formato recusado. O DSN serve de base:<origem>/api/<projeto>/security/?sentry_key=<chave>(aceito pelo GlitchTip e pelo Sentry). - Sentry só para erros no back:
enableOpenTelemetrySetup: false, sem integrações automáticas de http/express (traces são do OTel; não há instrumentação dupla).