Skip to content

rafaelmfried/tracing-node

Repository files navigation

tracing-node

Implementação Node.js / TypeScript equivalente do tracing-go, para a aula de tracing do Unnamed-Lab.

Autor: Rafael Friederick — Unnamed-Lab.


1. Objetivo

Mesma demo da versão Go, em outra linguagem — para mostrar que paralelizar I/O é um conceito de modelagem, não de stack:

Linguagem Sequencial Paralelo
Go for { runQuery() } errgroup + goroutines
Node for...of com await Promise.all([...])

Duas rotas, exatamente o mesmo trabalho:

Rota Como executa Tempo esperado
GET /sync for...of com await — uma por vez ≈ N × SLEEP
GET /parallel Promise.all — todas dispara juntas ≈ 1 × SLEEP

Cada query "lenta" é um pg_sleep no Postgres, idêntico ao Go — para que o waterfall do Tempo seja diretamente comparável.


2. Stack

  • Node 24 + TypeScript (ESM)
  • Fastify 5 (HTTP) — schema declarativo, swagger gerado direto do schema
  • pg (driver Postgres, sem ORM)
  • OpenTelemetry SDK (@opentelemetry/sdk-node) com auto-instrumentation para pg, fastify, http
  • Vitest + @testcontainers/postgresql
  • pnpm

Instrumentação automática: spans HTTP via instrumentation-http/instrumentation-fastify, spans de query via instrumentation-pg. Eu adiciono spans manuais (queries.sequential, queries.parallel, query.<name>) para agrupar.


3. Estrutura

.
├── src/
│   ├── server.ts                       # entry, init OTEL, listen Fastify
│   ├── app/queries.ts                  # runSequential, runParallel, runQuery
│   └── infra/
│       ├── db/postgres.ts              # pool pg
│       ├── http/
│       │   ├── server.ts               # buildApp (Fastify + cors + swagger)
│       │   └── routes.ts               # /health, /sync, /parallel
│       └── observability/tracing.ts    # OTLP/HTTP exporter para Tempo
├── tests/
│   ├── helpers/postgres.ts             # testcontainers fixture
│   ├── unit/queries.test.ts            # integração: queries vs pool real
│   └── e2e/api.e2e.test.ts             # E2E HTTP via Fastify .inject()
├── docs/{swagger.json,swagger.yaml}    # spec OpenAPI versionado (gerado por make swagger)
├── docker/Dockerfile                   # build da app (multi-stage)
├── Makefile
├── package.json
└── tsconfig.json

4. Subindo o ambiente

4.1. Local (sem Docker no app)

Precisa do Postgres e Tempo do tracing-go rodando (ou de qualquer Postgres + Tempo):

cp .env.example .env
pnpm install
pnpm dev

4.2. Containerizado (via monorepo pai)

A infra (Postgres, Tempo, Grafana, Prometheus) vive no monorepo pai. Este submódulo contém apenas a aplicação Node.

cd ..              # entra no monorepo
make up node       # sobe Postgres + Tempo + esta API
# OU
make up all        # sobe também o Go, Grafana, Prometheus

Endereços:

Mesmo formato de URL do tracing-go (/swagger/index.html, /swagger/doc.json) — o spec versionado fica em docs/ e é regenerado offline com make swagger (script scripts/generate-openapi.ts, igual ao swag init do Go).

A porta padrão é 8091 para não bater com a versão Go (8090). Os dois podem rodar lado a lado e exportar para o mesmo Tempo.


5. Roteiro da aula

  1. Bater as rotas:

    curl -s localhost:8091/sync     | jq
    curl -s localhost:8091/parallel | jq

    A resposta JSON tem totalMs. Você já vê a diferença antes de abrir o Grafana.

  2. Abrir o Grafana (do tracing-go) → Tracing — Home dashboard. Os traces de tracing-node aparecem ao lado dos de tracing-go com o mesmo formato de waterfall.

  3. Discussão:

    • Promise.all é paralelismo de I/O, não de CPU. Node é single-threaded; o ganho vem de não bloquear o event loop entre awaits.
    • Pool de conexões (max: 1) estrangula /parallel igual no Go. É a mesma demonstração.
    • Erros parciais: Promise.all falha rápido (rejeita assim que qualquer promise rejeita). Equivalente ao errgroup.WithContext que cancela as goroutines irmãs.
    • Promise.allSettled seria o oposto: espera todas, retorna sucesso/falha individual. Mostre quando faz sentido cada um.
    • Ordem dos resultados: Promise.all preserva a ordem do array de entrada (não a ordem de conclusão). Equivalente ao slice results[i] = ... do Go.

6. Exercícios sugeridos

  1. Trocar Promise.all por Promise.allSettled em /parallel e ver o que muda no comportamento de erro.
  2. Limitar max: 1 no Pool de src/infra/db/postgres.ts — observar /parallel ficar igual a /sync no trace.
  3. Adicionar uma quarta query que dependa do resultado da primeira (Promise.all([a, b, c]) seguido de runQuery(useResultOf(a))) — observar como o waterfall mostra a dependência.
  4. Comparar overhead OTEL: rodar com OTEL_DEBUG=true e comparar latência base vs com SDK.

7. Comandos úteis

make help               # mostra todos os targets do Makefile
make test               # vitest completo (unit + e2e, requer Docker)
make test-unit          # só tests/unit
make test-e2e           # só tests/e2e
make swagger            # regera docs/swagger.{json,yaml}
make build              # tsc → ./dist
pnpm typecheck          # tsc --noEmit
# infra (Postgres + Tempo + Grafana): no monorepo pai → make up node

8. Equivalências com o Go

Conceito Go Node
Pool pgxpool.Pool pg.Pool
Tracer manual otel.Tracer(...) trace.getTracer(...)
Span tracer.Start(ctx, "...") tracer.startActiveSpan(...)
Sequencial for { runQuery(ctx, ...) } for await (const x of names)
Paralelo errgroup.Go(...) Promise.all(names.map(...))
Cancelamento em grupo errgroup.WithContext Promise.all rejeita na 1ª falha
Speedup esperado ~3x (3 queries, sleep=1s) ~3x (idem)

9. Licença

MIT — Rafael Friederick / Unnamed-Lab. Material livre para uso educacional, com atribuição.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors