Capítulo 17: Observabilidade com Prometheus Metrics e Logs com Zap
Especialização em Backend com Go & Gin • Gin Framework & Go 1.22+ • GORM, Clean Architecture, Concorrência e Alta Performance
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Cliente HTTP / Frontend"] --> B["API Gateway / Router"]
B --> C["Controller / Handler"]
C --> D["Service Layer (Regras de Negócio)"]
D --> E["Repository / ORM (Persistência)"]
E --> F["Banco de Dados / Cache"]
subgraph ARQ["Arquitetura do Capítulo"]
G["Conceito: Observabilidade com Prometheus Metrics e Logs com Zap"]
H["Segurança, Validação e Resiliência"]
I["Alta Performance e Escalabilidade"]
end
D --> ARQ
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style F fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
🏛️ 1. Fundamentos Técnicos de Observabilidade com Prometheus Metrics e Logs com Zap
“Observabilidade” não é sinônimo de “logar tudo” — é a capacidade de responder, sem precisar reimplantar código, três perguntas depois que algo dá errado em produção: o que aconteceu (logs estruturados), quanto está acontecendo (métricas) e onde na cadeia de chamadas o tempo foi gasto (tracing). Os três pilares são complementares, não substituíveis entre si: um contador Prometheus diz que a taxa de erro 500 subiu para 8%, mas não diz qual requisição falhou nem por quê — para isso existe o log estruturado correlacionado por request_id.
Logs estruturados de alto desempenho com Zap
O pacote log da biblioteca padrão formata mensagens como texto livre — ótimo para humanos lendo um terminal, péssimo para máquinas fazendo parsing em um agregador como Loki ou Elasticsearch. go.uber.org/zap resolve isso emitindo JSON estruturado por padrão, e faz isso com uma arquitetura pensada para o hot path: em seu modo zap.Logger (não o SugaredLogger, que sacrifica performance por ergonomia com interface{}), os campos são passados como zap.Field tipados (zap.String, zap.Int, zap.Duration, zap.Error), o que permite serializar sem refletir tipos em tempo de execução e sem alocar no heap para a maioria dos casos comuns — a diferença chega a ser de 10-20x menos alocações por chamada de log comparado a fmt.Sprintf + log.Println sob carga.
logger, _ := zap.NewProduction() // JSON, nível Info+, timestamp ISO8601, caller
defer logger.Sync() // OBRIGATÓRIO: drena o buffer antes de sair
logger.Info("requisição processada",
zap.String("metodo", "POST"),
zap.String("rota", "/pedidos"),
zap.Int("status", 201),
zap.Duration("latencia", 42*time.Millisecond),
)
// {"level":"info","ts":...,"caller":"...","msg":"requisição processada","metodo":"POST","rota":"/pedidos","status":201,"latencia":"42ms"}
O defer logger.Sync() é fácil de esquecer e caro de esquecer: sem ele, logs ainda no buffer interno do Zap podem se perder se o processo terminar antes do flush. Em um middleware Gin, o padrão é criar o logger uma única vez na inicialização (nunca por requisição — isso anularia o ganho de performance) e usar logger.With(...) para derivar um logger filho já carregando campos fixos como request_id, evitando repeti-los em cada chamada.
Métricas com Prometheus: contador, histograma e gauge não são intercambiáveis
Prometheus opera em modelo pull: o servidor Prometheus faz scraping periódico (tipicamente a cada 15s) de um endpoint /metrics que expõe o estado atual das métricas em texto plano (formato OpenMetrics), em vez da aplicação empurrar cada evento individualmente. Isso implica que a aplicação só precisa manter contadores/acumuladores em memória — o cliente prometheus/client_golang cuida da serialização.
Os três tipos fundamentais têm semânticas distintas e usá-los errado quebra os dashboards:
- Counter (
NewCounterVec) só cresce — serve para “total de requisições”, “total de erros”. Nunca decrementa; se sua métrica pode cair (ex.: “conexões abertas agora”), não é Counter. - Gauge — valor que sobe e desce livremente, como “goroutines ativas” ou “tamanho da fila”.
- Histogram (
NewHistogramVec) — distribui observações em buckets de valor (ex.: latência ≤5ms, ≤10ms, ≤25ms…), permitindo calcular percentis (p50, p95, p99) no lado do Prometheus viahistogram_quantile(), algo que uma média simples nunca revela (uma média de 50ms pode esconder 1% das requisições levando 3 segundos).
var (
reqTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{Name: "http_requests_total", Help: "Total de requisições HTTP"},
[]string{"method", "path", "status"},
)
reqDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Duração das requisições HTTP em segundos",
Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5},
},
[]string{"method", "path"},
)
)
func init() {
prometheus.MustRegister(reqTotal, reqDuration)
}
// MiddlewarePrometheus instrumenta toda requisição sem exigir código repetido em cada handler.
func MiddlewarePrometheus() gin.HandlerFunc {
return func(c *gin.Context) {
inicio := time.Now()
c.Next() // executa o handler real primeiro
status := strconv.Itoa(c.Writer.Status())
reqTotal.WithLabelValues(c.Request.Method, c.FullPath(), status).Inc()
reqDuration.WithLabelValues(c.Request.Method, c.FullPath()).Observe(time.Since(inicio).Seconds())
}
}
Um cuidado real de produção: usar c.FullPath() (o padrão de rota registrado, ex. /users/:id) como label, nunca c.Request.URL.Path (a URL literal, ex. /users/482, /users/483…). Cada combinação única de valores de label vira uma série temporal própria no Prometheus — usar o ID literal do usuário como label causa “cardinalidade explosiva”, multiplicando o uso de memória do Prometheus por milhões de séries órfãs.
Tracing distribuído: a peça que falta entre logs e métricas
Em uma arquitetura de microsserviços, uma única requisição do cliente pode atravessar 5 serviços internos. Logs e métricas isolados por serviço não respondem “qual desses 5 saltos causou os 800ms de latência?”. OpenTelemetry resolve propagando um TraceID (identifica a requisição fim a fim) e SpanID (identifica cada segmento/salto) via cabeçalhos HTTP padronizados (W3C Trace Context, traceparent), permitindo reconstruir a árvore de chamadas completa em ferramentas como Jaeger.
tr := otel.Tracer("gin-server")
ctx, span := tr.Start(c.Request.Context(), "processar-pedido")
defer span.End()
// repassar ctx para chamadas downstream propaga o mesmo TraceID
Na prática, os três pilares se cruzam: um alerta dispara porque reqDuration p99 (métrica) ultrapassou o SLO; o time abre os logs Zap filtrados por request_id na janela do pico (log) e, se o gargalo estiver em um serviço downstream, segue o TraceID correspondente até a raiz do problema (trace).
🔗 Recursos Pedagógicos do Capítulo 17
| Recurso Didático | Finalidade | Link de Acesso |
|---|---|---|
| 📊 Slides de Aula | Apresentação visual interativa com Dark Mode e suporte a teclado | Ver Slides |
| 🧠 Quiz Formativo | Teste interativo de fixação com feedback imediato por alternativa | Fazer Quiz |
| 💻 Exemplos de Código | Demonstrações funcionais com código executável | Ver Exemplos |
| 🧩 Exercícios em 4 Níveis | Lista progressiva de fixação com gabarito em bloco colapsável | Resolver Exercícios |
| ⬅️ Capítulo Anterior | 📚 Sumário de Tópicos | Próximo Capítulo ➡️ |