Observabilidade

Grafana

Referência completa de Grafana: dashboards, data sources, alertas, LogQL, provisioning e Grafana Alloy

Setup e Instalação

Docker Compose com Prometheus + Grafana

# docker-compose.yml — stack completa de observabilidade

version: '3.9'

volumes:
  grafana-data: {}
  prometheus-data: {}
  loki-data: {}

networks:
  observability:
    driver: bridge

services:

  grafana:
    image: grafana/grafana:10.2.3
    container_name: grafana
    restart: unless-stopped
    networks:
      - observability
    ports:
      - "3000:3000"
    environment:
      # Usuário/senha do admin
      GF_SECURITY_ADMIN_USER: admin
      GF_SECURITY_ADMIN_PASSWORD: admin
      # Desabilitar cadastro de novos usuários
      GF_USERS_ALLOW_SIGN_UP: "false"
      # Configuração de email para alertas
      GF_SMTP_ENABLED: "true"
      GF_SMTP_HOST: smtp.gmail.com:587
      GF_SMTP_USER: alertas@empresa.com
      GF_SMTP_PASSWORD: senha-aqui
      GF_SMTP_FROM_ADDRESS: alertas@empresa.com
      # Instalar plugins extras na inicialização
      GF_INSTALL_PLUGINS: grafana-piechart-panel,grafana-worldmap-panel
      # Desabilitar telemetria
      GF_ANALYTICS_REPORTING_ENABLED: "false"
      GF_ANALYTICS_CHECK_FOR_UPDATES: "false"
    volumes:
      - grafana-data:/var/lib/grafana
      # Provisioning automático de datasources e dashboards
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
      - ./grafana/dashboards:/var/lib/grafana/dashboards:ro
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/api/health || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 5

  prometheus:
    image: prom/prometheus:v2.48.0
    container_name: prometheus
    restart: unless-stopped
    networks:
      - observability
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=30d'
      - '--web.enable-lifecycle'

  loki:
    image: grafana/loki:2.9.3
    container_name: loki
    restart: unless-stopped
    networks:
      - observability
    ports:
      - "3100:3100"
    volumes:
      - ./loki/loki.yml:/etc/loki/local-config.yaml:ro
      - loki-data:/loki
    command: -config.file=/etc/loki/local-config.yaml

  promtail:
    image: grafana/promtail:2.9.3
    container_name: promtail
    restart: unless-stopped
    networks:
      - observability
    volumes:
      - ./promtail/promtail.yml:/etc/promtail/config.yml:ro
      - /var/log:/var/log:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
    command: -config.file=/etc/promtail/config.yml

  node-exporter:
    image: prom/node-exporter:v1.7.0
    container_name: node-exporter
    restart: unless-stopped
    networks:
      - observability
    pid: host
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'

Data Sources

Configuração via UI ou Provisioning

Prometheus:

# grafana/provisioning/datasources/prometheus.yml

apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    jsonData:
      httpMethod: POST
      timeInterval: 15s                # deve bater com o scrape_interval
      queryTimeout: 60s
      exemplarTraceIdDestinations:
        - name: TraceID
          datasourceUid: tempo         # link para traces no Tempo
    editable: false

Loki:

  - name: Loki
    type: loki
    access: proxy
    url: http://loki:3100
    jsonData:
      maxLines: 1000
      derivedFields:
        # Extrai TraceID dos logs e cria link para Jaeger/Tempo
        - matcherRegex: '"trace_id":"(\w+)"'
          name: TraceID
          url: '$${__value.raw}'
          datasourceUid: tempo
    editable: false

PostgreSQL:

  - name: PostgreSQL-Producao
    type: postgres
    url: postgres-server:5432
    user: grafana_ro           # usuário read-only dedicado
    secureJsonData:
      password: senha-secreta
    jsonData:
      database: producao
      sslmode: require
      maxOpenConns: 10
      maxIdleConns: 5
      connMaxLifetime: 14400
      postgresVersion: 1500    # PostgreSQL 15
      timescaledb: false
    editable: false

Elasticsearch:

  - name: Elasticsearch
    type: elasticsearch
    url: http://elasticsearch:9200
    jsonData:
      index: 'logs-*'
      timeField: '@timestamp'
      esVersion: '8.0.0'
      logMessageField: message
      logLevelField: log.level
      maxConcurrentShardRequests: 5
    editable: false

Panels — Tipos e Quando Usar

Time Series

Para dados contínuos ao longo do tempo. O tipo mais comum.

{
  "type": "timeseries",
  "title": "Taxa de Requisições por Segundo",
  "fieldConfig": {
    "defaults": {
      "unit": "reqps",
      "min": 0,
      "color": { "mode": "palette-classic" },
      "custom": {
        "lineWidth": 2,
        "fillOpacity": 10,
        "pointSize": 0
      }
    }
  },
  "options": {
    "tooltip": { "mode": "multi", "sort": "desc" },
    "legend": { "displayMode": "table", "placement": "bottom" }
  }
}

Quando usar:

  • Métricas de rate/irate
  • CPU, memória, latência ao longo do tempo
  • Qualquer série temporal contínua

Stat

Exibe um único valor grande com contexto.

Quando usar:

  • Uptime percentual
  • Total de erros no período
  • Valor atual de uma métrica crítica
  • KPIs do dashboard
{
  "type": "stat",
  "title": "Error Rate",
  "options": {
    "colorMode": "background",
    "graphMode": "area",
    "textMode": "value_and_name",
    "reduceOptions": {
      "calcs": ["lastNotNull"]
    }
  },
  "fieldConfig": {
    "defaults": {
      "unit": "percentunit",
      "thresholds": {
        "mode": "absolute",
        "steps": [
          { "color": "green", "value": null },
          { "color": "yellow", "value": 0.01 },
          { "color": "red", "value": 0.05 }
        ]
      }
    }
  }
}

Table

Para dados tabulares e comparações entre instâncias.

Quando usar:

  • Comparar múltiplos hosts (CPU, memória, disco)
  • Lista de alertas ativos
  • Top N endpoints por latência
  • Dados com múltiplas colunas

Transformações úteis para Table:

  • Merge — combinar resultados de múltiplas queries
  • Filter by value — filtrar linhas
  • Sort by — ordenar
  • Rename by regex — renomear campos
  • Calculate field — campos calculados

Bar Chart

Para comparar categorias (não tempo).

Quando usar:

  • Distribuição de erros por endpoint
  • Comparação de recursos entre serviços
  • Ranking de usuários mais ativos

Heatmap

Para visualizar distribuições ao longo do tempo.

Quando usar:

  • Distribuição de latências (histograms)
  • Horários de pico de tráfego
  • Densidade de eventos
# Query para heatmap de latência
sum(increase(http_request_duration_seconds_bucket[$__interval])) by (le)

Logs Panel

Para visualizar logs do Loki.

Quando usar:

  • Log Explorer
  • Correlacionar logs com métricas no mesmo dashboard

Traces Panel

Para visualizar traces do Tempo/Jaeger.

Quando usar:

  • Análise de latência de requests distribuídos
  • Debugging de microserviços

Queries Prometheus no Grafana

Variáveis de Tempo

O Grafana injeta automaticamente variáveis especiais nas queries:

# $__interval — intervalo calculado baseado no range do dashboard
# Evita over-sampling, ideal para dashboards
rate(http_requests_total[$__interval])

# $__rate_interval — similar, mas garante mínimo de 4x o scrape interval
# Mais seguro que $__interval para rate()
rate(http_requests_total[$__rate_interval])

# $__range — range total selecionado (ex: "1h")
increase(http_requests_total[$__range])

# $__from e $__to — timestamps Unix em milissegundos
# Usado em queries SQL: WHERE timestamp BETWEEN $__from AND $__to

Usando Template Variables

# Dashboard com variável $job
rate(http_requests_total{job="$job"}[$__rate_interval])

# Múltiplos valores selecionados (regex)
rate(http_requests_total{job=~"$job"}[$__rate_interval])

# Variável $instance com valores múltiplos
process_memory_bytes{instance=~"$instance"}

Editor de Query (Transform e Override)

// Transformação: Rename Fields usando Regex
{
  "id": "renameByRegex",
  "options": {
    "regex": "http_requests_total\\{.*job=\"([^\"]+)\".*\\}",
    "renamePattern": "$1"
  }
}

// Transformação: Calculate Field (adicionar coluna calculada)
{
  "id": "calculateField",
  "options": {
    "mode": "reduceRow",
    "reduce": { "reducer": "sum" },
    "alias": "total"
  }
}

// Override: Customizar uma série específica
{
  "matcher": { "id": "byName", "options": "error_rate" },
  "properties": [
    { "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } },
    { "id": "custom.lineWidth", "value": 3 }
  ]
}

Template Variables

Tipos de Variables

Query Variable — busca valores do datasource:

# Buscar todos os jobs disponíveis no Prometheus
type: query
datasource: Prometheus
query: label_values(up, job)
refresh: On Dashboard Load
multi: true
includeAll: true
allValue: ".*"    # valor para "All" em regex

# Buscar instâncias de um job específico
query: label_values(up{job="$job"}, instance)

# Buscar métricas que começam com "http"
query: metrics(http.*)

Custom Variable — lista fixa:

type: custom
query: "prod,staging,dev"
# Com labels customizados:
query: "Produção : prod, Homologação : staging, Dev : dev"

Interval Variable — para seleção de janela de tempo:

type: interval
values: "1m,5m,10m,30m,1h,6h,12h,1d"
auto: true
auto_count: 30    # divide o range em 30 intervalos
auto_min: 10s

Textbox Variable — input livre:

type: textbox
default: "api-backend"

Uso Avançado de Variables

# Variável com dependência (instâncias do job selecionado)
label_values(up{job="$job"}, instance)

# Usar variável em títulos de panels
"CPU Usage — $instance"

# Usar em links
"https://logs.empresa.com/search?job=$job"

# Repetir rows/panels por variável
# No JSON: "repeat": "instance", "repeatDirection": "h"

Alerting no Grafana

Regras de Alerta (Grafana Alerting)

# API: POST /api/v1/provisioning/alert-rules
{
  "title": "High Error Rate",
  "ruleGroup": "api-alerts",
  "folderUID": "prod-alerts",
  "noDataState": "NoData",    # comportamento quando sem dados: NoData, Alerting, OK
  "execErrState": "Error",
  "for": "5m",                # precisa ficar ativo por 5 minutos
  "labels": {
    "severity": "critical",
    "team": "backend"
  },
  "annotations": {
    "summary": "Error rate acima de 5%",
    "description": "Error rate atual: {{ $values.A.Value | printf \"%.2f\" }}%"
  },
  "data": [
    {
      "refId": "A",
      "queryType": "",
      "relativeTimeRange": { "from": 300, "to": 0 },
      "datasourceUid": "prometheus",
      "model": {
        "expr": "sum(rate(http_requests_total{status=~\"5..\"}[5m])) / sum(rate(http_requests_total[5m])) * 100",
        "intervalMs": 1000,
        "maxDataPoints": 43200
      }
    },
    {
      "refId": "B",
      "queryType": "expression",
      "model": {
        "type": "threshold",
        "refId": "B",
        "conditions": [
          {
            "evaluator": { "params": [5], "type": "gt" }
          }
        ]
      }
    }
  ]
}

Contact Points (Destinos de Notificação)

# grafana/provisioning/alerting/contact-points.yml

apiVersion: 1

contactPoints:
  - orgId: 1
    name: Slack-Critico
    receivers:
      - uid: slack-critico
        type: slack
        settings:
          url: https://hooks.slack.com/services/T00/B00/xxx
          channel: "#alertas-criticos"
          username: Grafana Alerts
          icon_emoji: ":rotating_light:"
          title: |
            {{ if eq .Status "firing" }}ALERTA{{ else }}RESOLVIDO{{ end }}: {{ .CommonLabels.alertname }}
          text: |
            {{ range .Alerts }}
            *Summary:* {{ .Annotations.summary }}
            *Description:* {{ .Annotations.description }}
            *Severity:* {{ .Labels.severity }}
            {{ end }}

  - orgId: 1
    name: Email-Oncall
    receivers:
      - uid: email-oncall
        type: email
        settings:
          addresses: oncall@empresa.com;backup@empresa.com
          subject: "[{{ .Status | toUpper }}] {{ .CommonLabels.alertname }}"
          message: |
            {{ range .Alerts }}
            Alert: {{ .Labels.alertname }}
            Summary: {{ .Annotations.summary }}
            {{ end }}

  - orgId: 1
    name: PagerDuty
    receivers:
      - uid: pagerduty
        type: pagerduty
        settings:
          integrationKey: <integration-key>
          severity: '{{ .CommonLabels.severity }}'
          class: '{{ .CommonLabels.alertname }}'
          component: grafana

Notification Policies

# grafana/provisioning/alerting/notification-policies.yml

apiVersion: 1

policies:
  - orgId: 1
    receiver: default-receiver
    group_by: [alertname, job]
    group_wait: 30s
    group_interval: 5m
    repeat_interval: 4h
    routes:
      # Alertas críticos → PagerDuty
      - receiver: PagerDuty
        matchers:
          - severity = critical
        repeat_interval: 1h

      # Alertas de banco de dados → canal específico
      - receiver: Slack-DBA
        matchers:
          - team = dba
        continue: false   # não propagar para rotas superiores

      # Alertas de staging → silenciados fora do horário
      - receiver: Slack-Dev
        matchers:
          - environment = staging
        mute_time_intervals:
          - outside-business-hours

Silences (via API)

# Criar silence por 4 horas (manutenção planejada)
curl -X POST http://grafana:3000/api/alertmanager/grafana/api/v2/silences \
  -H "Authorization: Bearer $GRAFANA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "matchers": [
      {"name": "instance", "value": "server-1", "isRegex": false},
      {"name": "environment", "value": "production", "isRegex": false}
    ],
    "startsAt": "2024-01-15T14:00:00Z",
    "endsAt": "2024-01-15T18:00:00Z",
    "createdBy": "ops-team",
    "comment": "Manutenção planejada servidor-1"
  }'

Grafana Loki

Configuração do Loki

# loki/loki.yml

auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9096

common:
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

schema_config:
  configs:
    - from: 2020-10-24
      store: boltdb-shipper
      object_store: filesystem
      schema: v11
      index:
        prefix: index_
        period: 24h

ruler:
  alertmanager_url: http://alertmanager:9093

limits_config:
  enforce_metric_name: false
  reject_old_samples: true
  reject_old_samples_max_age: 168h    # 7 dias
  max_entries_limit_per_query: 5000
  ingestion_rate_mb: 16
  ingestion_burst_size_mb: 32

Promtail — Coleta de Logs

# promtail/promtail.yml

server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:

  # Logs de arquivos do sistema
  - job_name: system
    static_configs:
      - targets:
          - localhost
        labels:
          job: system-logs
          __path__: /var/log/*.log

  # Logs de containers Docker
  - job_name: docker-containers
    docker_sd_configs:
      - host: unix:///var/run/docker.sock
        refresh_interval: 5s
        filters:
          - name: status
            values: ["running"]
    relabel_configs:
      # Label com nome do container
      - source_labels: [__meta_docker_container_name]
        regex: '/(.*)'
        target_label: container
      # Label com imagem do container
      - source_labels: [__meta_docker_container_image]
        target_label: image
    pipeline_stages:
      # Parse JSON logs
      - json:
          expressions:
            level: level
            message: message
            trace_id: trace_id
      # Extrair labels dos campos parseados
      - labels:
          level:
          trace_id:
      # Timestamp do log (não do promtail)
      - timestamp:
          source: time
          format: RFC3339Nano

  # Logs de aplicação Spring Boot (JSON)
  - job_name: api-backend
    static_configs:
      - targets:
          - localhost
        labels:
          job: api-backend
          env: production
          __path__: /var/log/api/*.log
    pipeline_stages:
      - json:
          expressions:
            level: level
            logger: logger_name
            thread: thread_name
            message: message
            trace_id: X-B3-TraceId
      - labels:
          level:
          logger:
      - output:
          source: message

LogQL — Linguagem de Query do Loki

Log Queries (retornam linhas de log)

# Seletor básico — streams de log por labels
{job="api-backend"}

# Múltiplos labels
{job="api-backend", env="production"}

# Operadores de label
{job="api-backend"}                   # igualdade
{job!="api-backend"}                  # diferença
{job=~"api.*|gateway"}                # regex
{job!~"test.*"}                       # negação regex

# Filtros de texto
{job="api-backend"} |= "ERROR"        # contém
{job="api-backend"} != "DEBUG"        # não contém
{job="api-backend"} |~ "ERROR|WARN"   # regex match
{job="api-backend"} !~ "healthcheck"  # regex negação

# Encadeamento de filtros
{job="api-backend"}
  |= "ERROR"
  != "health"
  |~ "timeout|connection refused"

# Parse JSON e filtrar por campos
{job="api-backend"}
  | json
  | level = "ERROR"
  | status_code >= 500

# Parse logfmt (key=value format)
{job="nginx"} | logfmt | method="POST" | status=500

# Parse com regex (extrai campos via grupos captura)
{job="nginx"}
  | regexp `(?P<method>\w+) (?P<path>[^ ]+) HTTP/[^ ]+" (?P<status>\d+)`
  | status >= 500

# Parse com padrão (mais legível que regex para logs estruturados)
{job="nginx"}
  | pattern `<ip> - <user> [<_>] "<method> <path> <_>" <status> <size>`
  | status >= 500

# Filtro por linha (expressão)
{job="api-backend"} | json | line_format "{{.message}}"

Metric Queries (retornam séries temporais)

# Contagem de linhas por segundo
rate({job="api-backend"}[5m])

# Total de linhas na janela
count_over_time({job="api-backend"}[5m])

# Contagem de erros por segundo
rate({job="api-backend"} |= "ERROR" [5m])

# Taxa de erros (%) por serviço
sum(rate({job="api-backend"} |= "ERROR" [5m])) by (container)
  / sum(rate({job="api-backend"} [5m])) by (container)

# Soma de campo numérico extraído
sum(
  sum_over_time(
    {job="api-backend"}
    | json
    | unwrap duration_ms [5m]
  )
) by (endpoint)

# P99 de latência a partir de logs
quantile_over_time(0.99,
  {job="api-backend"}
  | json
  | unwrap duration_ms
  | __error__=""
  [5m]
) by (endpoint)

# Bytes transferidos por segundo
rate({job="nginx"}
  | logfmt
  | unwrap bytes_sent [5m])

# Top 5 IPs com mais requisições
topk(5,
  sum by (remote_addr) (
    count_over_time(
      {job="nginx"} | logfmt [5m]
    )
  )
)

# Logs por nível de severidade
sum(count_over_time({job="api-backend"} | json [5m])) by (level)

Queries Avançadas

# Deduplicar linhas duplicadas (útil para logs que escrevem em múltiplos alvos)
{job="api-backend"} | decolorize | distinct

# Filtros de label após parse
{job="api-backend"}
  | json
  | duration > 1s          # campo duration como Duration
  | status_code >= 400
  | drop __error__          # remover label de erro de parse

# Format de saída customizado
{job="api-backend"}
  | json
  | line_format "[{{.level}}] {{.message}} (trace={{.trace_id}})"

# Label filter com expressões
{job="api-backend"}
  | json
  | label_format request_time=duration    # renomear label

Traces com Tempo e Jaeger

Configuração do Tempo

# tempo/tempo.yml

server:
  http_listen_port: 3200

distributor:
  receivers:
    jaeger:
      protocols:
        thrift_http:
          endpoint: 0.0.0.0:14268
        grpc:
          endpoint: 0.0.0.0:14250
    otlp:
      protocols:
        http:
          endpoint: 0.0.0.0:4318
        grpc:
          endpoint: 0.0.0.0:4317
    zipkin:
      endpoint: 0.0.0.0:9411

ingester:
  trace_idle_period: 10s
  max_block_bytes: 1_000_000
  max_block_duration: 5m

compactor:
  compaction:
    block_retention: 1h

storage:
  trace:
    backend: local
    block:
      bloom_filter_false_positive: .05
      index_downsample_bytes: 1000
      encoding: zstd
    local:
      path: /tmp/tempo/blocks
    wal:
      path: /tmp/tempo/wal

Datasource Tempo no Grafana

# grafana/provisioning/datasources/tempo.yml

datasources:
  - name: Tempo
    type: tempo
    access: proxy
    url: http://tempo:3200
    uid: tempo
    jsonData:
      httpMethod: GET
      serviceMap:
        datasourceUid: prometheus     # integração com métricas de serviço
      nodeGraph:
        enabled: true                 # habilitar visualização de grafo de serviços
      search:
        hide: false
      lokiSearch:
        datasourceUid: loki           # correlação com logs
      traceQuery:
        timeShiftEnabled: true
        spanStartTimeShift: 1h
        spanEndTimeShift: -1h

Provisioning — Dashboards como Código

Configuração de Provisioning

# grafana/provisioning/dashboards/dashboards.yml

apiVersion: 1

providers:
  - name: default
    orgId: 1
    type: file
    disableDeletion: false      # permitir deletar dashboards via UI
    updateIntervalSeconds: 30   # verificar novos arquivos a cada 30s
    allowUiUpdates: true        # permitir salvar mudanças via UI
    options:
      path: /var/lib/grafana/dashboards
      foldersFromFilesStructure: true  # criar pastas baseadas em subdiretórios

Dashboard JSON Básico

{
  "__inputs": [
    {
      "name": "DS_PROMETHEUS",
      "label": "Prometheus",
      "description": "",
      "type": "datasource",
      "pluginId": "prometheus"
    }
  ],
  "__requires": [
    {
      "type": "grafana",
      "id": "grafana",
      "name": "Grafana",
      "version": "10.0.0"
    }
  ],
  "id": null,
  "uid": "api-overview",
  "title": "API Overview",
  "tags": ["api", "production"],
  "timezone": "browser",
  "schemaVersion": 38,
  "version": 1,
  "refresh": "30s",
  "time": {
    "from": "now-1h",
    "to": "now"
  },
  "templating": {
    "list": [
      {
        "name": "job",
        "type": "query",
        "datasource": "${DS_PROMETHEUS}",
        "query": "label_values(up, job)",
        "refresh": 1,
        "multi": true,
        "includeAll": true
      }
    ]
  },
  "panels": [
    {
      "id": 1,
      "type": "timeseries",
      "title": "Request Rate",
      "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 },
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "expr": "sum by (job) (rate(http_requests_total{job=~\"$job\"}[$__rate_interval]))",
          "legendFormat": "{{job}}"
        }
      ]
    }
  ]
}

Plugins Úteis

# Instalar plugins via CLI
grafana-cli plugins install grafana-piechart-panel
grafana-cli plugins install grafana-worldmap-panel
grafana-cli plugins install grafana-clock-panel
grafana-cli plugins install grafana-polystat-panel
grafana-cli plugins install marcusolsson-json-datasource
grafana-cli plugins install volkovlabs-image-panel

# Via variável de ambiente no Docker
GF_INSTALL_PLUGINS: "grafana-piechart-panel,grafana-worldmap-panel,grafana-clock-panel"
PluginUso
grafana-piechart-panelGráficos de pizza para proporções
grafana-worldmap-panelMapa mundial com dados geográficos
grafana-clock-panelRelógio para contexto de timezone
marcusolsson-json-datasourceDatasource para APIs JSON REST
volkovlabs-image-panelExibir imagens dinâmicas
grafana-polystat-panelMúltiplos stats em hexágonos

Boas Práticas de Dashboard

Naming e Organização

Estrutura de pastas recomendada:
├── Infrastructure/
│   ├── Nodes Overview
│   ├── Node Detail
│   └── Network Overview
├── Applications/
│   ├── API Overview
│   ├── Service Detail
│   └── Database Overview
└── Business/
    ├── Sales Dashboard
    └── User Activity

Convenções de nomenclatura:

  • Títulos de dashboard: [Serviço] [Escopo] ex: “API — Visão Geral”, “PostgreSQL — Detalhes”
  • Títulos de panels: curtos e descritivos, incluir unidade se não óbvio
  • Tags: ambiente (prod/staging), time (backend/infra/dba), tipo (overview/detail)

Time Ranges e Refresh

// Configurações recomendadas por tipo de dashboard
{
  // Dashboard operacional (on-call)
  "time": { "from": "now-1h", "to": "now" },
  "refresh": "30s",

  // Dashboard de análise de capacidade
  "time": { "from": "now-7d", "to": "now" },
  "refresh": "5m",

  // Dashboard de tendências
  "time": { "from": "now-30d", "to": "now" },
  "refresh": "1h"
}

Thresholds Semânticos

// Padrão semáforo para SLOs
"thresholds": {
  "mode": "percentage",
  "steps": [
    { "color": "red", "value": null },    // 0-80% → vermelho
    { "color": "yellow", "value": 80 },   // 80-95% → amarelo
    { "color": "green", "value": 95 }     // >95% → verde
  ]
}

// Para métricas inversas (menor = melhor, ex: latência)
"thresholds": {
  "mode": "absolute",
  "steps": [
    { "color": "green", "value": null },
    { "color": "yellow", "value": 200 },  // 200ms
    { "color": "red", "value": 500 }      // 500ms
  ]
}
// Link de panel para dashboard de detalhes
"links": [
  {
    "title": "Ver detalhes",
    "type": "dashboard",
    "url": "/d/service-detail?var-service=${__field.name}",
    "targetBlank": false
  }
]

// Link de dashboard para logs correlacionados
{
  "title": "Logs deste serviço",
  "type": "link",
  "url": "/explore?orgId=1&left={\"datasource\":\"Loki\",\"queries\":[{\"expr\":\"{job=\\\"$job\\\"}\"}]}",
  "icon": "external link"
}

Dashboard as Code com Grafonnet

Grafonnet é uma biblioteca Jsonnet para gerar dashboards programaticamente.

# Instalar dependências
brew install jsonnet
jb init
jb install github.com/grafana/grafonnet/gen/grafonnet-latest@main
// dashboard.jsonnet
local grafana = import 'grafonnet-latest/main.libsonnet';
local dashboard = grafana.dashboard;
local panel = grafana.panel;
local query = grafana.query;

// Variáveis reutilizáveis
local prometheusDs = grafana.datasource.prometheus.new('DS_PROMETHEUS', 'Prometheus');

local jobVar = grafana.dashboard.variable.query.new('job')
  + grafana.dashboard.variable.query.withDatasource('prometheus', 'DS_PROMETHEUS')
  + grafana.dashboard.variable.query.withQuery('label_values(up, job)')
  + grafana.dashboard.variable.query.selectionOptions.withMulti()
  + grafana.dashboard.variable.query.selectionOptions.withIncludeAll();

// Panel de request rate
local requestRatePanel =
  panel.timeSeries.new('Request Rate')
  + panel.timeSeries.gridPos.withH(8)
  + panel.timeSeries.gridPos.withW(12)
  + panel.timeSeries.queryOptions.withTargets([
    query.prometheus.new(
      '$DS_PROMETHEUS',
      'sum by (job) (rate(http_requests_total{job=~"$job"}[$__rate_interval]))'
    )
    + query.prometheus.withLegendFormat('{{job}}'),
  ])
  + panel.timeSeries.standardOptions.withUnit('reqps')
  + panel.timeSeries.options.tooltip.withMode('multi');

// Dashboard final
dashboard.new('API Overview')
+ dashboard.withUid('api-overview')
+ dashboard.withTags(['api', 'production'])
+ dashboard.time.withFrom('now-1h')
+ dashboard.withRefresh('30s')
+ dashboard.withVariables([jobVar])
+ dashboard.withPanels([requestRatePanel])
# Gerar JSON do dashboard
jsonnet -J vendor dashboard.jsonnet > dashboard.json

# Importar no Grafana via API
curl -X POST http://grafana:3000/api/dashboards/db \
  -H "Authorization: Bearer $GRAFANA_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"dashboard\": $(cat dashboard.json), \"overwrite\": true}"

Grafana Alloy

Grafana Alloy é o sucessor do Grafana Agent — coleta telemetria (métricas, logs, traces) e encaminha para Prometheus/Loki/Tempo.

// alloy/config.alloy

// Coletar métricas do sistema
prometheus.exporter.unix "node" {
  // Equivalente ao node_exporter
}

prometheus.scrape "unix" {
  targets    = prometheus.exporter.unix.node.targets
  forward_to = [prometheus.remote_write.mimir.receiver]
}

// Coleta de logs via tail
loki.source.file "logs" {
  targets = [
    {__path__ = "/var/log/*.log", job = "system"},
  ]
  forward_to = [loki.write.grafana_loki.receiver]
}

// Coleta de logs Docker
loki.source.docker "containers" {
  host       = "unix:///var/run/docker.sock"
  targets    = discovery.docker.containers.targets
  forward_to = [loki.write.grafana_loki.receiver]
}

discovery.docker "containers" {
  host = "unix:///var/run/docker.sock"
}

// Receber traces via OTLP
otelcol.receiver.otlp "default" {
  grpc { endpoint = "0.0.0.0:4317" }
  http { endpoint = "0.0.0.0:4318" }

  output {
    traces  = [otelcol.exporter.otlp.tempo.input]
    metrics = [otelcol.exporter.prometheus.default.input]
    logs    = [otelcol.exporter.loki.default.input]
  }
}

// Pipeline de métricas: processar → enviar
otelcol.processor.batch "default" {
  output {
    metrics = [otelcol.exporter.prometheus.default.input]
    traces  = [otelcol.exporter.otlp.tempo.input]
  }
}

// Remote write para Prometheus/Mimir
prometheus.remote_write "mimir" {
  endpoint {
    url = "http://prometheus:9090/api/v1/write"
  }
}

// Enviar logs para Loki
loki.write "grafana_loki" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"
  }
}

// Enviar traces para Tempo
otelcol.exporter.otlp "tempo" {
  client {
    endpoint = "http://tempo:4317"
    tls { insecure = true }
  }
}
# Executar Alloy via Docker
docker run -d \
  --name alloy \
  -v /etc/alloy:/etc/alloy \
  -v /var/log:/var/log:ro \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -p 12345:12345 \
  grafana/alloy:latest \
  run /etc/alloy/config.alloy

Referência Rápida

API do Grafana

# Listar dashboards
curl -H "Authorization: Bearer $TOKEN" http://grafana:3000/api/search

# Criar/atualizar dashboard
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"dashboard": {...}, "overwrite": true}' \
  http://grafana:3000/api/dashboards/db

# Criar API key
curl -X POST -H "Content-Type: application/json" \
  -u admin:admin \
  -d '{"name":"deploy-key", "role": "Editor"}' \
  http://grafana:3000/api/auth/keys

# Health check
curl http://grafana:3000/api/health

# Reload provisioning
curl -X POST -u admin:admin \
  http://grafana:3000/api/admin/provisioning/dashboards/reload

Unidades Comuns no Grafana

Unit KeyDescrição
reqpsRequisições por segundo
percentunitPercentual (0.0 a 1.0)
percentPercentual (0 a 100)
bytesBytes (auto-escalado)
BpsBytes por segundo
msMilissegundos
sSegundos
shortNúmero curto (k, M)
noneSem unidade
dateTimeISOData/hora ISO 8601

Variáveis Especiais do Grafana

VariávelValor
$__fromInício do range (ms)
$__toFim do range (ms)
$__intervalIntervalo auto
$__rate_intervalIntervalo para rate()
$__rangeDuração total (ex: 1h)
$__orgID da organização
$__user.loginLogin do usuário
$__dashboardNome do dashboard
${variable:csv}Variable como CSV
${variable:regex}Variable como regex
${variable:pipe}Variable com pipe (OR)