Implementação Node.js / TypeScript equivalente do
tracing-go, para a aula de tracing do Unnamed-Lab.
Autor: Rafael Friederick — Unnamed-Lab.
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.
- 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 parapg,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.
.
├── 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
Precisa do Postgres e Tempo do tracing-go rodando (ou de qualquer Postgres + Tempo):
cp .env.example .env
pnpm install
pnpm devA 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, PrometheusEndereços:
- API: http://localhost:8091
- Swagger UI: http://localhost:8091/swagger/index.html (raiz
/redireciona) - OpenAPI JSON: http://localhost:8091/swagger/doc.json
- OpenAPI YAML: http://localhost:8091/swagger/doc.yaml
Mesmo formato de URL do
tracing-go(/swagger/index.html,/swagger/doc.json) — o spec versionado fica emdocs/e é regenerado offline commake swagger(scriptscripts/generate-openapi.ts, igual aoswag initdo 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.
-
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. -
Abrir o Grafana (do tracing-go) → Tracing — Home dashboard. Os traces de
tracing-nodeaparecem ao lado dos detracing-gocom o mesmo formato de waterfall. -
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 entreawaits.- Pool de conexões (
max: 1) estrangula/paralleligual no Go. É a mesma demonstração. - Erros parciais:
Promise.allfalha rápido (rejeita assim que qualquer promise rejeita). Equivalente aoerrgroup.WithContextque cancela as goroutines irmãs. Promise.allSettledseria o oposto: espera todas, retorna sucesso/falha individual. Mostre quando faz sentido cada um.- Ordem dos resultados:
Promise.allpreserva a ordem do array de entrada (não a ordem de conclusão). Equivalente ao sliceresults[i] = ...do Go.
- Trocar
Promise.allporPromise.allSettledem/parallele ver o que muda no comportamento de erro. - Limitar
max: 1noPooldesrc/infra/db/postgres.ts— observar/parallelficar igual a/syncno trace. - Adicionar uma quarta query que dependa do resultado da primeira (
Promise.all([a, b, c])seguido derunQuery(useResultOf(a))) — observar como o waterfall mostra a dependência. - Comparar overhead OTEL: rodar com
OTEL_DEBUG=truee comparar latência base vs com SDK.
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| 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) |
MIT — Rafael Friederick / Unnamed-Lab. Material livre para uso educacional, com atribuição.