aula-15: APM com Grafana Tempo + OpenTelemetry
Nova aula cobrindo Application Performance Monitoring: - Grafana Tempo como backend de traces (single binary, sem DB) - OpenTelemetry auto-instrumentação (zero code changes) - Demo app com N+1 intencional pra demonstração - Conceito: limiar de Doherty (400ms) e perda silenciosa de UX - RED method (Rate, Errors, Duration) por rota - Correlação métrica→trace nativa no Grafana - Alertas: p95 > 400ms dispara aviso Filosofia: métricas dizem O QUE está errado, traces dizem POR QUE.
This commit is contained in:
@@ -25,6 +25,7 @@ App de demonstração: `node-bugado` - trava após N requests para demonstrar he
|
|||||||
| 12 | Victoria Metrics (Observabilidade) | Hetzner |
|
| 12 | Victoria Metrics (Observabilidade) | Hetzner |
|
||||||
| 13 | Container Factory (eStargz) | Hetzner |
|
| 13 | Container Factory (eStargz) | Hetzner |
|
||||||
| 14 | Istio Traffic Splitting | Hetzner |
|
| 14 | Istio Traffic Splitting | Hetzner |
|
||||||
|
| 15 | APM: Grafana Tempo + OpenTelemetry | Hetzner |
|
||||||
|
|
||||||
## Comandos Rápidos
|
## Comandos Rápidos
|
||||||
|
|
||||||
|
|||||||
371
aula-15/README.md
Normal file
371
aula-15/README.md
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
# Aula 15 - APM: De "está lento" para "aqui está o N+1" (Grafana Tempo + OpenTelemetry)
|
||||||
|
|
||||||
|
Métricas dizem **O QUE** está errado. Traces dizem **POR QUE**.
|
||||||
|
|
||||||
|
## O problema que esta aula resolve
|
||||||
|
|
||||||
|
Cenário real: uma aplicação começa rápida. Um agente de IA (ou um dev apressado) usa um ORM e mete um N+1. A base de dados cresce. A latência sobe de 50ms para 500ms ao longo de um mês.
|
||||||
|
|
||||||
|
O Victoria Metrics (aula-12) mostra que a latência subiu. Mas **não mostra por quê**. Você sabe que está lento — mas é o banco? A rede? Um serviço externo? Um loop de queries?
|
||||||
|
|
||||||
|
Sem traces, o debug é:
|
||||||
|
```
|
||||||
|
1. Olhar logs (milhares de linhas)
|
||||||
|
2. Adicionar console.log/dd() no código
|
||||||
|
3. Deploiar versão com debug
|
||||||
|
4. Esperar acontecer de novo
|
||||||
|
5. Repetir
|
||||||
|
```
|
||||||
|
|
||||||
|
Com traces, o debug é:
|
||||||
|
```
|
||||||
|
1. Abrir Grafana
|
||||||
|
2. Clicar no spike de latência
|
||||||
|
3. Ver o trace: 1 request HTTP → 147 queries SQL
|
||||||
|
4. Encontrar o N+1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diferença: horas/dias vs. 30 segundos.**
|
||||||
|
|
||||||
|
## O Limiar de Doherty
|
||||||
|
|
||||||
|
Em 1982, Walter Doherty e Ahrvind Thadani publicaram um estudo na IBM Systems Journal mostrando que quando o tempo de resposta de um sistema passa de **400ms**, o usuário perde o senso de "fluxo" — a interação deixa de parecer instantânea.
|
||||||
|
|
||||||
|
```
|
||||||
|
0-100ms → Instantâneo (o usuário nem percebe)
|
||||||
|
100-400ms → Rápido (o usuário nota mas tolera)
|
||||||
|
400ms-1s → Lento (perde o "flow state", perde engagement)
|
||||||
|
1s+ → Quebrado (o usuário sai)
|
||||||
|
```
|
||||||
|
|
||||||
|
O perigo do intervalo 100-400ms → 400ms-1s: o usuário **não reclama**. Ele simplesmente usa menos. A retenção cai silenciosamente. Nenhuma métrica de infraestrutura (CPU, RAM, disco) vai detectar isso — tudo parece normal.
|
||||||
|
|
||||||
|
**O que detecta: latência por rota medida com OpenTelemetry + alertas no Victoria Metrics.**
|
||||||
|
|
||||||
|
## O que instalamos
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Stack Completo │
|
||||||
|
│ │
|
||||||
|
│ App (Node.js / Laravel / qualquer linguagem) │
|
||||||
|
│ │ │
|
||||||
|
│ │ OpenTelemetry SDK (auto-instrumentação) │
|
||||||
|
│ │ │
|
||||||
|
│ ├──► Métricas ──► Victoria Metrics (aula-12, já instalado) │
|
||||||
|
│ │ └──► Grafana: dashboard RED │
|
||||||
|
│ │ │
|
||||||
|
│ └──► Traces ──► Grafana Tempo (esta aula) │
|
||||||
|
│ └──► Grafana: trace view │
|
||||||
|
│ │
|
||||||
|
│ Grafana conecta os dois: │
|
||||||
|
│ dashboard latência (VM) → clica → abre trace (Tempo) │
|
||||||
|
│ → vê cada SQL query │
|
||||||
|
│ → encontra o N+1 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Escolha do Tempo (e não Jaeger, SigNoz, etc.)
|
||||||
|
|
||||||
|
| | Jaeger | SigNoz | Grafana Tempo |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Storage** | Cassandra/Elasticsearch | ClickHouse | Local disk ou S3 (sem DB) |
|
||||||
|
| **RAM mínima** | ~512Mi+ | ~3Gi | ~256Mi |
|
||||||
|
| **UI** | Própria (mais uma URL) | Própria | **Grafana (já temos)** |
|
||||||
|
| **Query language** | Tags básicas | ClickHouse SQL | **TraceQL** |
|
||||||
|
| **Metrics↔Traces** | Manual | Próprio | **Nativo no Grafana** |
|
||||||
|
|
||||||
|
Tempo segue a mesma filosofia do workshop: **single binary, sem dependências extras, integra com o que já temos**.
|
||||||
|
|
||||||
|
## Conceitos
|
||||||
|
|
||||||
|
### O que é um Trace
|
||||||
|
|
||||||
|
Um trace é a **jornada completa** de um request pelo sistema:
|
||||||
|
|
||||||
|
```
|
||||||
|
Trace: GET /api/users (total: 487ms)
|
||||||
|
│
|
||||||
|
├── [Span] HTTP GET /api/users .................. 487ms
|
||||||
|
│ ├── [Span] middleware.auth .................. 2ms
|
||||||
|
│ ├── [Span] UserController.index ............ 480ms
|
||||||
|
│ │ ├── [Span] SELECT * FROM users ......... 3ms ← 1 query
|
||||||
|
│ │ ├── [Span] SELECT * FROM posts WHERE ... 3ms ← N+1 começa aqui
|
||||||
|
│ │ ├── [Span] SELECT * FROM posts WHERE ... 3ms
|
||||||
|
│ │ ├── [Span] SELECT * FROM posts WHERE ... 3ms
|
||||||
|
│ │ ├── ... (x147 vezes)
|
||||||
|
│ │ └── [Span] SELECT * FROM posts WHERE ... 3ms ← 147 queries
|
||||||
|
│ └── [Span] JSON serialize .................. 5ms
|
||||||
|
└── total SQL time: 441ms / 147 queries
|
||||||
|
```
|
||||||
|
|
||||||
|
Cada caixa é um **span**. O trace é a árvore completa.
|
||||||
|
|
||||||
|
### RED Method (métricas por rota)
|
||||||
|
|
||||||
|
O padrão ouro de monitoramento de aplicações:
|
||||||
|
|
||||||
|
| Métrica | O que mede | Alerta quando |
|
||||||
|
|---------|-----------|---------------|
|
||||||
|
| **R**ate | Requests por segundo por rota | Drop súbito (app caiu?) |
|
||||||
|
| **E**rrors | % de respostas 5xx por rota | > 1% (algo quebrou) |
|
||||||
|
| **D**uration | Latência p50, p95, p99 por rota | p95 > 400ms (limiar de Doherty) |
|
||||||
|
|
||||||
|
OpenTelemetry gera essas métricas automaticamente. Victoria Metrics armazena. Grafana mostra.
|
||||||
|
|
||||||
|
### Diferença: Métricas vs Traces
|
||||||
|
|
||||||
|
| | Métricas (Victoria Metrics) | Traces (Tempo) |
|
||||||
|
|---|---|---|
|
||||||
|
| **Granularidade** | Agregado (p99 de todos os requests) | Individual (cada request) |
|
||||||
|
| **Custo** | Baixo (números comprimidos) | Alto (cada span é um evento) |
|
||||||
|
| **Responde** | "A latência da rota /api/users está alta" | "Este request específico fez 147 queries SQL" |
|
||||||
|
| **Analogia** | Termômetro (temperatura do corpo) | Raio-X (o que está causando a febre) |
|
||||||
|
|
||||||
|
**Use métricas pra detectar. Use traces pra diagnosticar.**
|
||||||
|
|
||||||
|
## Pré-requisitos
|
||||||
|
|
||||||
|
- Cluster Kubernetes Hetzner (aula-08)
|
||||||
|
- Victoria Metrics + Grafana (aula-12)
|
||||||
|
- kubectl e helm instalados
|
||||||
|
|
||||||
|
## Estrutura
|
||||||
|
|
||||||
|
```
|
||||||
|
aula-15/
|
||||||
|
├── README.md
|
||||||
|
├── setup.sh # Instala Tempo + OTel Collector
|
||||||
|
├── cleanup.sh
|
||||||
|
│
|
||||||
|
├── tempo-values.yaml # Config do Grafana Tempo
|
||||||
|
├── otel-collector-values.yaml # Config do OpenTelemetry Collector
|
||||||
|
│
|
||||||
|
├── demo-app/ # App Node.js com N+1 intencional
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ ├── app.js # Servidor com rotas rápidas e lentas
|
||||||
|
│ ├── package.json
|
||||||
|
│ └── k8s/
|
||||||
|
│ ├── deployment.yaml
|
||||||
|
│ ├── service.yaml
|
||||||
|
│ └── ingress.yaml
|
||||||
|
│
|
||||||
|
├── dashboards/
|
||||||
|
│ └── red-dashboard.json # Dashboard RED pra Grafana
|
||||||
|
│
|
||||||
|
└── alerts/
|
||||||
|
└── latency-alerts.yaml # VMRule: alerta quando p95 > 400ms
|
||||||
|
```
|
||||||
|
|
||||||
|
## Instalação
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd aula-15
|
||||||
|
export KUBECONFIG=$(pwd)/../aula-08/kubeconfig
|
||||||
|
./setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
O script instala:
|
||||||
|
1. **Grafana Tempo** — backend de traces (namespace: monitoring)
|
||||||
|
2. **OpenTelemetry Collector** — recebe traces das apps e envia pro Tempo
|
||||||
|
3. **Demo app** — aplicação Node.js com N+1 intencional
|
||||||
|
4. **Dashboard RED** — latência/rate/errors por rota
|
||||||
|
5. **Alerta de latência** — dispara quando p95 > 400ms (Doherty)
|
||||||
|
|
||||||
|
## Arquitetura
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Namespace: demo │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────┐ │
|
||||||
|
│ │ demo-app (Node.js) │ │
|
||||||
|
│ │ @opentelemetry/auto-instrument │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ GET /fast → 1 SQL query │ │
|
||||||
|
│ │ GET /slow → N+1 (100+ SQL) │ │
|
||||||
|
│ └──────────┬───────────────────────┘ │
|
||||||
|
│ │ OTLP (gRPC :4317) │
|
||||||
|
└─────────────┼────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Namespace: monitoring │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────────────┐ │
|
||||||
|
│ │ OTel Collector │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Recebe OTLP ──────┬──► Grafana Tempo (traces) │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Gera métricas ─────┴──► Victoria Metrics (RED metrics) │
|
||||||
|
│ └────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────────────┐ │
|
||||||
|
│ │ Grafana │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Dashboard RED ◄──── Victoria Metrics │
|
||||||
|
│ │ (latência p95) │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ click spike │ │
|
||||||
|
│ │ ▼ │ │
|
||||||
|
│ │ Trace view ◄─────── Tempo │
|
||||||
|
│ │ (147 SQL spans) │ │
|
||||||
|
│ └────────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Por que o OTel Collector?
|
||||||
|
|
||||||
|
A app poderia enviar direto pro Tempo. Mas o Collector faz duas coisas cruciais:
|
||||||
|
|
||||||
|
1. **Deriva métricas dos traces** — gera automaticamente `http_server_request_duration` (histograma por rota) sem instrumentar a app pra métricas separadamente
|
||||||
|
2. **Desacopla** — a app envia pra um endpoint fixo (`otel-collector:4317`), o backend pode mudar sem tocar na app
|
||||||
|
|
||||||
|
## A Demo App: node-bugado-n1
|
||||||
|
|
||||||
|
Uma aplicação Node.js simples com duas rotas:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// GET /fast — 1 query, resposta em ~5ms
|
||||||
|
app.get('/fast', async (req, res) => {
|
||||||
|
const users = await db.query('SELECT * FROM users LIMIT 10');
|
||||||
|
res.json(users);
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /slow — N+1, resposta em ~500ms
|
||||||
|
app.get('/slow', async (req, res) => {
|
||||||
|
const users = await db.query('SELECT * FROM users');
|
||||||
|
// N+1: uma query por usuário pra buscar posts
|
||||||
|
for (const user of users) {
|
||||||
|
user.posts = await db.query('SELECT * FROM posts WHERE user_id = ?', [user.id]);
|
||||||
|
}
|
||||||
|
res.json(users);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
O OpenTelemetry auto-instrumento captura **cada query SQL** como um span — sem mudar o código.
|
||||||
|
|
||||||
|
## Fluxo da demonstração
|
||||||
|
|
||||||
|
### 1. Gerar tráfego
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Rota rápida (1 query)
|
||||||
|
for i in $(seq 1 100); do curl -s https://demo.kube.quest/fast > /dev/null; done
|
||||||
|
|
||||||
|
# Rota lenta (N+1)
|
||||||
|
for i in $(seq 1 100); do curl -s https://demo.kube.quest/slow > /dev/null; done
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Ver no dashboard RED (Grafana)
|
||||||
|
|
||||||
|
Abrir `https://grafana.kube.quest` → Dashboard "RED - Application":
|
||||||
|
|
||||||
|
- **Rate**: `/fast` e `/slow` com ~100 requests cada
|
||||||
|
- **Errors**: 0% (não é erro — é lentidão)
|
||||||
|
- **Duration**: `/fast` p95 = 5ms, `/slow` p95 = 500ms
|
||||||
|
|
||||||
|
O alerta "Doherty Threshold" dispara pra `/slow`.
|
||||||
|
|
||||||
|
### 3. Clicar no spike → ver o trace
|
||||||
|
|
||||||
|
No gráfico de latência, clicar num ponto da rota `/slow`:
|
||||||
|
- Grafana abre o trace no Tempo
|
||||||
|
- Trace mostra: 1 span HTTP → 1 span Controller → **100 spans SQL**
|
||||||
|
- Cada span SQL: `SELECT * FROM posts WHERE user_id = ?`
|
||||||
|
- **N+1 encontrado em 30 segundos**
|
||||||
|
|
||||||
|
### 4. Comparar com `/fast`
|
||||||
|
|
||||||
|
Clicar num ponto da rota `/fast`:
|
||||||
|
- Trace mostra: 1 span HTTP → 1 span Controller → **1 span SQL**
|
||||||
|
- `SELECT * FROM users LIMIT 10`
|
||||||
|
- Latência: 5ms
|
||||||
|
|
||||||
|
**A diferença é visual e imediata.**
|
||||||
|
|
||||||
|
## Alertas
|
||||||
|
|
||||||
|
### Doherty Threshold (latência > 400ms)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# alerts/latency-alerts.yaml
|
||||||
|
apiVersion: operator.victoriametrics.com/v1beta1
|
||||||
|
kind: VMRule
|
||||||
|
metadata:
|
||||||
|
name: latency-alerts
|
||||||
|
namespace: monitoring
|
||||||
|
spec:
|
||||||
|
groups:
|
||||||
|
- name: application.latency
|
||||||
|
rules:
|
||||||
|
- alert: DohertyThresholdExceeded
|
||||||
|
expr: |
|
||||||
|
histogram_quantile(0.95,
|
||||||
|
sum(rate(http_server_request_duration_seconds_bucket[5m])) by (le, http_route)
|
||||||
|
) > 0.4
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "Rota {{ $labels.http_route }} acima do limiar de Doherty"
|
||||||
|
description: "p95 = {{ $value | humanizeDuration }}. Acima de 400ms, usuários perdem o senso de fluxo."
|
||||||
|
|
||||||
|
- alert: HighErrorRate
|
||||||
|
expr: |
|
||||||
|
sum(rate(http_server_request_duration_seconds_count{http_status_code=~"5.."}[5m])) by (http_route)
|
||||||
|
/ sum(rate(http_server_request_duration_seconds_count[5m])) by (http_route)
|
||||||
|
> 0.01
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
summary: "Rota {{ $labels.http_route }} com {{ $value | humanizePercentage }} de erros"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quanto custa em recursos
|
||||||
|
|
||||||
|
| Componente | Memory Request | Memory Limit |
|
||||||
|
|------------|----------------|--------------|
|
||||||
|
| Grafana Tempo | 256Mi | 512Mi |
|
||||||
|
| OTel Collector | 128Mi | 256Mi |
|
||||||
|
| Demo app | 64Mi | 128Mi |
|
||||||
|
| **Total adicional** | **~448Mi** | **~896Mi** |
|
||||||
|
|
||||||
|
Tempo armazena traces em disco (10Gi PVC). Retenção: 7 dias por padrão.
|
||||||
|
|
||||||
|
## Lições do Workshop
|
||||||
|
|
||||||
|
1. **Métricas ≠ Traces** — métricas são o termômetro, traces são o raio-X. Use os dois.
|
||||||
|
2. **O limiar de Doherty (400ms)** é onde a UX degrada silenciosamente. Meça latência por rota.
|
||||||
|
3. **N+1 é invisível sem traces** — a app não dá erro, o CPU não sobe, o disco não enche. Só o usuário sofre.
|
||||||
|
4. **OpenTelemetry é o padrão** — instrumenta uma vez, manda pra qualquer backend (Tempo, Jaeger, Datadog).
|
||||||
|
5. **OTel Collector como hub** — deriva métricas RED dos traces automaticamente. Uma instrumentação, dois sinais.
|
||||||
|
6. **Grafana conecta tudo** — métricas (Victoria Metrics) + traces (Tempo) na mesma UI, com correlação nativa.
|
||||||
|
|
||||||
|
## O que NÃO cobrimos (e por quê)
|
||||||
|
|
||||||
|
| Tema | Por que não | Quando faz sentido |
|
||||||
|
|------|------------|-------------------|
|
||||||
|
| **Logs** (Loki/VictoriaLogs) | Logs são texto desestruturado — traces são melhores pra debug | Compliance, auditoria, erros não-HTTP |
|
||||||
|
| **Distributed tracing multi-serviço** | Workshop tem apps simples | Quando tiver 5+ microserviços |
|
||||||
|
| **Continuous Profiling** (Pyroscope) | Nível muito avançado | Quando traces não bastam (CPU profiling) |
|
||||||
|
| **Real User Monitoring (RUM)** | Precisa de frontend instrumentado | SPA com métricas de UX |
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./cleanup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Remove Tempo, OTel Collector e demo app. Victoria Metrics (aula-12) é mantido.
|
||||||
|
|
||||||
|
## Referências
|
||||||
|
|
||||||
|
- [Grafana Tempo Docs](https://grafana.com/docs/tempo/latest/)
|
||||||
|
- [OpenTelemetry Docs](https://opentelemetry.io/docs/)
|
||||||
|
- [OpenTelemetry Node.js Auto-Instrumentation](https://opentelemetry.io/docs/languages/js/automatic/)
|
||||||
|
- [RED Method (Tom Wilkie)](https://grafana.com/blog/2018/08/02/the-red-method-how-to-instrument-your-services/)
|
||||||
|
- [Doherty Threshold (IBM, 1982)](https://jlelliotton.blogspot.com/p/the-economic-value-of-rapid-response.html)
|
||||||
|
- [TraceQL (Grafana)](https://grafana.com/docs/tempo/latest/traceql/)
|
||||||
|
- [Span Metrics via OTel Collector](https://opentelemetry.io/docs/collector/transforming-telemetry/)
|
||||||
Reference in New Issue
Block a user