2. Dashboard RED
Dados que você não consegue ler em dez segundos durante um incidente não são observabilidade, são armazenamento. Este passo constrói um dashboard para todos os serviços do BoxOffice, provisionado como código, organizado em torno de dois métodos da aula de sinais:
| Método | Para | Sinais | Pergunta que responde |
|---|---|---|---|
| RED (Tom Wilkie) | serviços movidos a requisições | Rate (taxa), Errors (erros), Duration (duração) | "Meus usuários estão sendo bem atendidos?" |
| USE (Brendan Gregg) | recursos | Utilization (utilização), Saturation (saturação), Errors (erros) | "Algum recurso é o gargalo?" |
O RED diz que algo está errado para os usuários; o USE diz onde a capacidade acabou. A linha de cima do dashboard é RED por serviço; a de baixo é USE para as JVMs e seus pools de conexão.
Dashboards como código
O Grafana carrega dashboards de arquivos listados por um provedor. Os dois arquivos ficam no seu repositório, então o dashboard é revisado, versionado e idêntico em todos os notebooks:
apiVersion: 1
providers:
- name: BoxOffice
folder: BoxOffice
type: file
options:
path: /otel-lgtm/boxoffice-dashboards
Você salvou esse provedor no passo 1; os volumes dele o montam na pasta de provisionamento do Grafana e infra/grafana/dashboards/ no caminho para o qual ele aponta. Salve o dashboard abaixo como infra/grafana/dashboards/boxoffice-red.json e reinicie: docker compose restart otel-lgtm. Ele aparece em Dashboards → BoxOffice → BoxOffice — RED.
infra/grafana/dashboards/boxoffice-red.json (clique para expandir)
{
"uid": "boxoffice-red",
"title": "BoxOffice — RED",
"tags": [
"boxoffice"
],
"timezone": "browser",
"schemaVersion": 39,
"refresh": "10s",
"time": {
"from": "now-15m",
"to": "now"
},
"templating": {
"list": [
{
"name": "service",
"label": "service",
"type": "query",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"query": {
"query": "label_values(http_server_request_duration_seconds_count, service_name)",
"refId": "service"
},
"definition": "label_values(http_server_request_duration_seconds_count, service_name)",
"multi": true,
"includeAll": true,
"current": {
"text": "All",
"value": "$__all"
},
"refresh": 2
}
]
},
"panels": [
{
"id": 1,
"type": "row",
"title": "RED: Rate, Errors, Duration",
"gridPos": {
"x": 0,
"y": 0,
"w": 24,
"h": 1
},
"collapsed": false,
"panels": []
},
{
"id": 2,
"type": "timeseries",
"title": "Rate (requests/s)",
"description": "R: how much work arrives",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"x": 0,
"y": 1,
"w": 8,
"h": 8
},
"fieldConfig": {
"defaults": {
"unit": "reqps"
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "sum by (service_name) (rate(http_server_request_duration_seconds_count{service_name=~\"$service\"}[$__rate_interval]))",
"legendFormat": "{{service_name}}"
}
]
},
{
"id": 3,
"type": "timeseries",
"title": "Errors (% of requests answered 5xx)",
"description": "E: the share of that work that fails",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"x": 8,
"y": 1,
"w": 8,
"h": 8
},
"fieldConfig": {
"defaults": {
"unit": "percent"
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "100 * sum by (service_name) (rate(http_server_request_duration_seconds_count{service_name=~\"$service\", http_response_status_code=~\"5..\"}[$__rate_interval])) / sum by (service_name) (rate(http_server_request_duration_seconds_count{service_name=~\"$service\"}[$__rate_interval]))",
"legendFormat": "{{service_name}}"
}
]
},
{
"id": 4,
"type": "timeseries",
"title": "Duration p95 (s)",
"description": "D: how long the slowest 5% wait",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"x": 16,
"y": 1,
"w": 8,
"h": 8
},
"fieldConfig": {
"defaults": {
"unit": "s"
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "histogram_quantile(0.95, sum by (service_name, le) (rate(http_server_request_duration_seconds_bucket{service_name=~\"$service\"}[$__rate_interval])))",
"legendFormat": "{{service_name}}"
}
]
},
{
"id": 5,
"type": "timeseries",
"title": "Duration p50 / p95 / p99 (all selected services)",
"description": "The gap between p50 and p99 is the tail",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"x": 0,
"y": 9,
"w": 12,
"h": 8
},
"fieldConfig": {
"defaults": {
"unit": "s"
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "histogram_quantile(0.5, sum by (le) (rate(http_server_request_duration_seconds_bucket{service_name=~\"$service\"}[$__rate_interval])))",
"legendFormat": "p50"
},
{
"refId": "B",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "histogram_quantile(0.95, sum by (le) (rate(http_server_request_duration_seconds_bucket{service_name=~\"$service\"}[$__rate_interval])))",
"legendFormat": "p95"
},
{
"refId": "C",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "histogram_quantile(0.99, sum by (le) (rate(http_server_request_duration_seconds_bucket{service_name=~\"$service\"}[$__rate_interval])))",
"legendFormat": "p99"
}
]
},
{
"id": 6,
"type": "table",
"title": "Slowest routes (p95 over 5 min)",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"x": 12,
"y": 9,
"w": 12,
"h": 8
},
"fieldConfig": {
"defaults": {
"unit": "s"
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"instant": true,
"format": "table",
"expr": "topk(10, histogram_quantile(0.95, sum by (service_name, http_request_method, http_route, le) (rate(http_server_request_duration_seconds_bucket{service_name=~\"$service\"}[5m]))))"
}
],
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true
}
}
}
]
},
{
"id": 7,
"type": "row",
"title": "USE: Utilization, Saturation (JVM and pools)",
"gridPos": {
"x": 0,
"y": 17,
"w": 24,
"h": 1
},
"collapsed": false,
"panels": []
},
{
"id": 8,
"type": "timeseries",
"title": "CPU utilization (of the container's CPUs)",
"description": "",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"x": 0,
"y": 18,
"w": 8,
"h": 8
},
"fieldConfig": {
"defaults": {
"unit": "percentunit"
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "avg by (service_name) (jvm_cpu_recent_utilization_ratio{service_name=~\"$service\"})",
"legendFormat": "{{service_name}}"
}
]
},
{
"id": 9,
"type": "timeseries",
"title": "Heap used",
"description": "",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"x": 8,
"y": 18,
"w": 8,
"h": 8
},
"fieldConfig": {
"defaults": {
"unit": "bytes"
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "sum by (service_name) (jvm_memory_used_bytes{service_name=~\"$service\", jvm_memory_type=\"heap\"})",
"legendFormat": "{{service_name}}"
}
]
},
{
"id": 10,
"type": "timeseries",
"title": "DB connections in use / waiting (saturation)",
"description": "Waiting > 0 means requests queue for a connection: the pool is saturated",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"x": 16,
"y": 18,
"w": 8,
"h": 8
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "sum by (service_name) (db_client_connections_usage{service_name=~\"$service\", state=\"used\"})",
"legendFormat": "{{service_name}} used"
},
{
"refId": "B",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "sum by (service_name) (db_client_connections_max{service_name=~\"$service\"})",
"legendFormat": "{{service_name}} max"
},
{
"refId": "C",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "sum by (service_name) (db_client_connections_pending_requests{service_name=~\"$service\"})",
"legendFormat": "{{service_name}} waiting"
}
]
}
]
}
Lendo os painéis
Cada painel é uma consulta PromQL sobre o histograma que o agente exporta. Aprenda as quatro formas; todo dashboard RED que você vai ver na vida é uma variação delas.
| Painel | Forma da consulta | Leia como |
|---|---|---|
| Rate | sum by (service_name) (rate(…_count[$__rate_interval])) | requisições por segundo, por serviço |
| Errors | rate(…_count{status=~"5.."}) / rate(…_count) | a fração de requisições que falham (não a contagem) |
| Duration p95 | histogram_quantile(0.95, sum by (service_name, le) (rate(…_bucket[…]))) | 95% das requisições foram mais rápidas que isto |
| Rotas mais lentas | topk(10, histogram_quantile(0.95, sum by (…, http_route, le) …)) | onde olhar primeiro |
| Conexões do banco | db_client_connections_usage{state="used"} vs _max, _pending_requests | o pool está saturado quando waiting > 0 |
Três hábitos fazem a diferença:
- Agregue antes do quantil. O
histogram_quantileprecisa receber asum by (le)das taxas dos buckets: dá para somar buckets entre instâncias, não dá para tirar a média de percentis (a média de três p95 não é o p95 de nada). - Erros como fração. 50 erros por segundo é um desastre a 60 req/s e ruído a 60 000 req/s.
- Percentis vindos de buckets são estimativas. O
histogram_quantileinterpola dentro de um bucket; com os limites padrão do agente (…, 0,25, 0,5, 0,75, 1, 2,5 s…), um p99 de "0,6 s" quer dizer "algo entre 0,5 e 0,75 s".
Exemplars: do pico ao trace que o causou
O Prometheus do otel-lgtm guarda exemplars: alguns trace ids de amostra presos aos buckets do histograma. No painel Duration, ative Exemplars nas opções da consulta; os losangos no gráfico são requisições reais, e clicar em um abre o seu trace no Tempo. É o caminho mais curto de "o p99 subiu" para "este comando SQL está lento".
Exercício: veja um gargalo aparecer
- Rode o teste de estresse em escada do lab 2 contra a borda — ele sobe a taxa de chegada em degraus até
PEAKe depois desce:k6 run -e BASE_URL=http://localhost -e PEAK=600 load/stress.js. - Observe Duration p95, CPU utilization e DB connections lado a lado. A curva de latência dobra onde o primeiro recurso satura. Quando
waitingsai do zero, as requisições fazem fila por uma conexão (Lei de Little: com o pool cheio, concorrência extra vira tempo de espera); quando a CPU de um serviço chega perto de 100% da CPU do seu contêiner, elas fazem fila pelo processador. Numa execução de teste deste lab ogateway(limitado a uma CPU) saturou primeiro: cerca de 95% de CPU a cerca de 320 req/s, p95 acima de 2 s e504depois do seu timeout de 5 s, enquanto o pool docatalognunca teve uma requisição esperando. - Abra um exemplar no topo da curva: o trace mostra para onde foi o tempo — um grande intervalo antes do span SQL é tempo esperando uma conexão, não executando SQL; um span longo do
gatewayem volta de um span curto docatalogé tempo na fila do gateway.
Escreva nos seus resultados qual recurso saturou primeiro e a taxa de requisições em que isso aconteceu.