Métricas com Prometheus client_golang

1. Introdução ao Prometheus e client_golang

Prometheus é um sistema de monitoramento e alerta de código aberto, parte da CNCF, que coleta métricas de sistemas alvo em intervalos regulares via HTTP. O ecossistema inclui armazenamento time-series, uma linguagem de consulta poderosa (PromQL) e integração com o Alertmanager para notificações.

O pacote client_golang é a biblioteca oficial para instrumentar aplicações Go. Com ele, você expõe métricas que o servidor Prometheus coleta. Os quatro tipos principais de métricas são:

  • Counter: valor que só aumenta (requisições, erros)
  • Gauge: valor que sobe e desce (memória, goroutines ativas)
  • Histogram: amostra observações em buckets configuráveis (latência)
  • Summary: calcula quantiles configuráveis (latência com percentis)

2. Configuração e registro de métricas

Instale o pacote com:

go get github.com/prometheus/client_golang/prometheus

Importe os pacotes necessários:

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

Você pode usar o registry global padrão (recomendado para a maioria dos casos) ou criar um registry customizado para isolamento:

// Registry global (padrão)
var (
    requestsTotal = prometheus.NewCounter(prometheus.CounterOpts{
        Name: "http_requests_total",
        Help: "Total number of HTTP requests.",
    })
)

func init() {
    prometheus.MustRegister(requestsTotal) // panic se falhar
}

// Registry customizado
var customRegistry = prometheus.NewRegistry()
var customCounter = prometheus.NewCounter(prometheus.CounterOpts{
    Name: "custom_requests_total",
    Help: "Custom counter.",
})

func init() {
    err := customRegistry.Register(customCounter)
    if err != nil {
        log.Fatalf("Falha ao registrar métrica: %v", err)
    }
}

Use MustRegister() para simplicidade em init() e Register() quando precisar tratar erros explicitamente.

3. Implementação dos tipos de métricas

Counter

var requestCounter = prometheus.NewCounter(prometheus.CounterOpts{
    Name: "app_requests_total",
    Help: "Total number of requests processed.",
})

func handleRequest() {
    requestCounter.Inc()   // incrementa em 1
    // requestCounter.Add(5) // incrementa em 5
}

Gauge

var activeGoroutines = prometheus.NewGauge(prometheus.GaugeOpts{
    Name: "app_active_goroutines",
    Help: "Current number of active goroutines.",
})

func worker() {
    activeGoroutines.Inc()
    defer activeGoroutines.Dec()
    // processa trabalho
}

// Ou defina diretamente:
activeGoroutines.Set(float64(runtime.NumGoroutine()))

Histogram e Summary

var requestDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
    Name:    "app_request_duration_seconds",
    Help:    "Request latency distributions.",
    Buckets: prometheus.DefBuckets, // [0.005, 0.01, 0.025, ..., 10]
})

var requestSummary = prometheus.NewSummary(prometheus.SummaryOpts{
    Name:       "app_request_summary_seconds",
    Help:       "Request latency summary.",
    Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
})

func handleWithTiming() {
    start := time.Now()
    defer func() {
        duration := time.Since(start).Seconds()
        requestDuration.Observe(duration)
        requestSummary.Observe(duration)
    }()
    // processa requisição
}

4. Exposição das métricas via HTTP

Use promhttp.Handler() para expor o registry global:

http.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(":8080", nil))

Para registry customizado:

http.Handle("/metrics", promhttp.HandlerFor(customRegistry, promhttp.HandlerOpts{}))

Middleware para coleta automática de métricas HTTP:

var httpRequestsTotal = prometheus.NewCounterVec(
    prometheus.CounterOpts{
        Name: "http_requests_total",
        Help: "Total number of HTTP requests.",
    },
    []string{"code", "method"},
)

func init() {
    prometheus.MustRegister(httpRequestsTotal)
}

func instrumentedHandler(handler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        rec := statusRecorder{ResponseWriter: w, status: http.StatusOK}
        handler.ServeHTTP(&rec, r)
        httpRequestsTotal.WithLabelValues(
            strconv.Itoa(rec.status),
            r.Method,
        ).Inc()
    })
}

type statusRecorder struct {
    http.ResponseWriter
    status int
}

func (r *statusRecorder) WriteHeader(code int) {
    r.status = code
    r.ResponseWriter.WriteHeader(code)
}

5. Métricas avançadas e labels

Labels permitem segmentar métricas por dimensões:

var requestByEndpoint = prometheus.NewCounterVec(
    prometheus.CounterOpts{
        Name: "http_requests_by_endpoint_total",
        Help: "Requests segmented by endpoint, method and status.",
    },
    []string{"endpoint", "method", "status"},
)

func trackRequest(endpoint, method string, status int) {
    requestByEndpoint.With(prometheus.Labels{
        "endpoint": endpoint,
        "method":   method,
        "status":   strconv.Itoa(status),
    }).Inc()
}

Boas práticas com labels:
- Evite alta cardinalidade (ex.: user_id, session_id) — pode explodir séries temporais
- Prefira labels com valores limitados (ex.: método HTTP, status code)
- Use prometheus.Labels para clareza

6. Integração com coleta e alertas (visão prática)

Configure o prometheus.yml para scrape:

scrape_configs:
  - job_name: 'go-app'
    static_configs:
      - targets: ['localhost:8080']

Exemplo de consulta PromQL:

# Taxa de requisições por segundo nos últimos 5 minutos
rate(http_requests_total[5m])

# Latência média (p50) a partir do histograma
histogram_quantile(0.5, rate(app_request_duration_seconds_bucket[5m]))

Para alertas no Alertmanager:

groups:
  - name: go-app
    rules:
      - alert: HighErrorRate
        expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "High error rate detected"

7. Boas práticas e considerações finais

Evite bloqueios na coleta:

Use GaugeFunc para métricas que podem ser caras de calcular:

var memAlloc = prometheus.NewGaugeFunc(
    prometheus.GaugeOpts{
        Name: "app_mem_alloc_bytes",
        Help: "Current memory allocation.",
    },
    func() float64 {
        var m runtime.MemStats
        runtime.ReadMemStats(&m)
        return float64(m.Alloc)
    },
)

Concorrência: client_golang é thread-safe por padrão. Não precisa de locks adicionais.

Checklist de instrumentação:
- Nomes seguem convenção: namespace_subsystem_unit_suffix (ex.: http_requests_total)
- Documente cada métrica com Help descritivo
- Teste com prometheus/testutil:

import "github.com/prometheus/client_golang/prometheus/testutil"

func TestCounter(t *testing.T) {
    counter := prometheus.NewCounter(prometheus.CounterOpts{Name: "test_counter"})
    counter.Inc()
    if testutil.ToFloat64(counter) != 1.0 {
        t.Error("Expected 1.0")
    }
}

A instrumentação com client_golang é direta e poderosa. Comece com métricas básicas (Counter para requisições, Gauge para recursos) e evolua para Histograms quando precisar de análise de latência. Lembre-se: métricas são tão úteis quanto a qualidade dos alertas que geram — invista tempo em PromQL e nas regras de alerta.

Referências