Material de aula sobre distributed tracing com OpenTelemetry, Tempo e Grafana — usando uma API Go que executa as mesmas três queries de duas formas diferentes para tornar o ganho de performance da concorrência visível no trace.
Autor: Rafael Friederick — Unnamed-Lab
Mostrar, com algo que o aluno possa rodar, abrir no Grafana e tocar, três coisas:
- O que é um trace — uma árvore de spans que descreve uma requisição ponta a ponta.
- Como o tracing torna gargalos óbvios — quando você vê os spans no tempo, sequencial vs paralelo deixa de ser teoria.
- Quando vale paralelizar I/O com goroutines em Go — e quando não vale.
Não é uma aula de Go puro nem de SQL. É uma aula sobre observabilidade aplicada: o mesmo trabalho, executado de duas formas, e o trace contando a história.
A API expõe duas rotas que fazem exatamente o mesmo trabalho: três queries lentas no Postgres (cada uma é um pg_sleep parametrizado, simulando um I/O remoto).
| Rota | Como executa | Duração esperada |
|---|---|---|
GET /sync |
Sequencial — uma query depois da outra | ≈ N × QUERY_SLEEP_SECONDS |
GET /parallel |
Concorrente — errgroup + goroutines |
≈ 1 × QUERY_SLEEP_SECONDS |
Com QUERY_SLEEP_SECONDS=1 e 3 queries: /sync ≈ 3s, /parallel ≈ 1s.
No trace, o contraste é literal:
/sync [ q1 ][ q2 ][ q3 ] ← spans em série
/parallel [ q1 ]
[ q2 ] ← spans empilhados, sobrepostos no tempo
[ q3 ]
- Go 1.26 — Gin + pgx (sem ORM)
- PostgreSQL 18
- OpenTelemetry SDK (OTLP/HTTP) → Tempo → Grafana
- Prometheus (compose já provisionado para extensão futura)
Instrumentação:
- HTTP:
otelgin(span por request) - DB:
otelpgx(span por query, comdb.statement) - App:
otel.Tracermanual emRunSequential/RunParallelpara agrupar os filhos
Documentação:
- OpenAPI/Swagger gerados via
swaggo/swaga partir de annotations nos handlers —make swaggerregeneradocs/
A infra (Postgres, Tempo, Grafana, Prometheus) vive no monorepo pai — https://github.com/rafaelmfried/tracing. Este submódulo contém apenas a aplicação Go.
Para rodar a stack completa (esta API + Postgres + Tempo + Grafana):
# Do monorepo (um nível acima):
cd ..
make up go # sobe Postgres + Tempo + esta API
# OU:
make up all # sobe também Node, Grafana, PrometheusPara rodar só o binário Go localmente (precisa de um Postgres acessível):
# Do monorepo, suba a infra:
cd .. && make up infra && make up obs && cd go
# Depois, rode a API local:
make build-go
DATABASE_URL=postgres://tracing:tracing@localhost:5433/tracing?sslmode=disable \
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
./build/apiEndereços (defaults):
- API: http://localhost:8090
- Swagger UI: http://localhost:8090/swagger/index.html (raiz
/redireciona pra cá) - OpenAPI JSON: http://localhost:8090/swagger/doc.json
- Grafana: http://localhost:3000 (anônimo como Admin, dashboard "Tracing — Home" já provisionado)
- Tempo: http://localhost:3200
- Prometheus: http://localhost:9090
curl -s localhost:8080/sync | jq
curl -s localhost:8080/parallel | jqA resposta JSON inclui total_ms — o aluno já vê a diferença antes de abrir o Grafana. Use isso para introduzir a pergunta:
"Por que
/parallelé ~3× mais rápido se executa as mesmas três queries?"
- Grafana → Explore → datasource Tempo → aba Search.
- Service:
tracing-api. Filtrar porname=GET /syncename=GET /parallel. - Abrir um trace de cada lado, lado a lado.
O que mostrar:
- O waterfall dos spans. Em
/sync, cadaquery.*começa onde o anterior terminou. Em/parallel, todos começam praticamente juntos. - O span do
pgx(SELECT ...) comdb.statement— tracing dá observabilidade do SQL real, não só do código Go. - O span pai
queries.sequential/queries.parallelcom a duração total.
Goroutines não são gratuitas. Pontos para puxar com a turma:
- Pool de conexões —
/parallelsó ganha se houver conexões livres. Se o pool temMaxConns=1, o paralelismo desaparece (vireMaxConnseminternal/infra/db/postgres.goe mostre o trace de novo). - Propagação de contexto —
errgroup.WithContextcancela as goroutines irmãs quando uma falha. Mostre derrubando o Postgres no meio. - Ordem dos resultados —
RunParallelpreserva a ordem por índice, não por tempo de chegada. Importante quando a saída é lida por outro sistema. - Quando NÃO paralelizar — quando há dependência entre as queries (q2 precisa do resultado de q1), goroutines não ajudam. Tracing torna essa dependência visível se ela existir.
- Limites do paralelismo —
errgroup.SetLimit(N)para evitar saturar o pool/DB downstream.
- Mexer no
QUERY_SLEEP_SECONDS(no.env) para 0.2s e 5s. Comparartotal_mse o waterfall. - Adicionar uma quarta query que dependa do resultado da primeira — observar que
/parallelperde a vantagem nessa parte específica. - Reduzir
MaxConnsdo pool para 1 e observar que/parallelfica equivalente a/syncno trace. - Adicionar um span manual dentro de
runQueryantes dopool.QueryRowpara isolar "tempo de espera por conexão" vs "tempo de query no servidor". - Fazer 100 chamadas concorrentes em cada rota com
heyouk6e comparar p50/p95 — onde o paralelismo dentro do request começa a competir com o paralelismo entre requests?
.
├── cmd/api/main.go # entrypoint, wiring, graceful shutdown
├── internal/
│ ├── app/queries.go # RunSequential, RunParallel, runQuery
│ └── infra/
│ ├── db/postgres.go # pgxpool + otelpgx tracer
│ ├── http/handlers.go # Gin router, /sync e /parallel
│ └── observability/tracing.go # OTLP/HTTP exporter para Tempo
├── docs/ # OpenAPI/Swagger gerado por `make swagger`
├── docker/
│ └── Dockerfile # build da app Go (multi-stage)
└── Makefile # make test / build / swagger
- OpenTelemetry Go SDK
- Grafana Tempo
otelpgx— instrumentação OTEL parapgxgolang.org/x/sync/errgroup
© Rafael Friederick — Unnamed-Lab. Material livre para uso educacional, com atribuição.