Observabilidade
LidoNesta aula, exploramos o conceito de observabilidade em sistemas distribuídos, focando em métricas, tracing distribuído, logs e OpenTelemetry. Aprenderemos a instrumentar aplicações Go para coletar esses sinais e entender como eles se complementam para diagnosticar problemas em produção.
Observabilidade é a capacidade de entender o estado interno de um sistema a partir de seus outputs externos. Em sistemas distribuídos modernos, onde múltiplos serviços interagem, a observabilidade se torna essencial para diagnosticar problemas, otimizar performance e garantir confiabilidade. Nesta aula, vamos explorar os três pilares da observabilidade — métricas, tracing distribuído e logs — e como o OpenTelemetry unifica a coleta desses sinais em aplicações Go.
Vamos começar com uma visão geral de cada pilar, depois mergulhar em exemplos práticos de instrumentação em Go, e finalizar com uma introdução ao OpenTelemetry, o padrão atual para observabilidade.
Métricas
Métricas são medições numéricas coletadas ao longo do tempo, como contadores, gauges e histogramas. Elas fornecem uma visão agregada do comportamento do sistema, permitindo monitorar tendências, detectar anomalias e alertar sobre condições anormais. Em Go, a biblioteca padrão expvar e a biblioteca prometheus/client_golang são as mais comuns para expor métricas.
O expvar é uma solução simples que publica variáveis via HTTP em formato JSON. Já o Prometheus oferece um modelo de dados mais rico e integração com o ecossistema de monitoramento. Vamos ver um exemplo com expvar primeiro:
package main
import (
"expvar"
"net/http"
)
var (
requests = expvar.NewInt("requests_total")
errors = expvar.NewInt("errors_total")
)
func handler(w http.ResponseWriter, r *http.Request) {
requests.Add(1)
// ... lógica do handler
if r.URL.Path == "/error" {
errors.Add(1)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write([]byte("OK"))
}
func main() {
http.HandleFunc("/", handler)
http.Handle("/debug/vars", expvar.Handler())
http.ListenAndServe(":8080", nil)
}Com o Prometheus, definimos métricas com tipos específicos e registramos em um registry. O servidor HTTP expõe as métricas no formato de texto do Prometheus. Exemplo:
package main
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
httpRequests = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests.",
},
[]string{"method", "path"},
)
)
func init() {
prometheus.MustRegister(httpRequests)
}
func handler(w http.ResponseWriter, r *http.Request) {
httpRequests.WithLabelValues(r.Method, r.URL.Path).Inc()
w.Write([]byte("OK"))
}
func main() {
http.HandleFunc("/", handler)
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":8080", nil)
}Métricas são fundamentais para monitoramento em tempo real e alertas. Elas ajudam a responder perguntas como "quantas requisições por segundo?", "qual é a latência média?" e "quantos erros ocorreram?".
Tracing distribuído (visão geral)
Tracing distribuído rastreia o fluxo de uma requisição à medida que ela atravessa múltiplos serviços. Ele captura informações como tempo gasto em cada serviço, operações executadas e erros encontrados, permitindo reconstruir o caminho completo de uma chamada. Isso é crucial para identificar gargalos de performance e falhas em arquiteturas de microserviços.
Cada trace é composto por spans, que representam uma unidade de trabalho. Um span contém um ID, um trace ID (que agrupa todos os spans de uma requisição), um nome, timestamps e atributos. Em Go, bibliotecas como go.opentelemetry.io/otel e github.com/opentracing/opentracing-go são usadas para criar spans. Vamos ver um exemplo com OpenTelemetry:
package main
import (
"context"
"fmt"
"log"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
func main() {
// Configurar um tracer provider simples (apenas para exemplo)
tp := otel.TracerProvider()
otel.SetTracerProvider(tp)
tracer := tp.Tracer("example")
ctx, span := tracer.Start(context.Background(), "main-operation")
defer span.End()
// Simular trabalho
doWork(ctx)
fmt.Println("Done")
}
func doWork(ctx context.Context) {
_, span := otel.Tracer("example").Start(ctx, "doWork-operation")
defer span.End()
// Simula uma chamada a um serviço externo
log.Println("Working...")
}Tracing distribuído é essencial para entender dependências entre serviços, medir latências por serviço e identificar erros em cascata. Ele complementa as métricas, fornecendo contexto detalhado sobre requisições individuais.
Logs
Logs são registros textuais de eventos que ocorrem no sistema. Eles são a forma mais tradicional de observabilidade e fornecem informações detalhadas sobre o que aconteceu em um determinado momento. Em Go, a biblioteca padrão log é básica, mas bibliotecas como logrus e zap oferecem logs estruturados, que são mais fáceis de analisar.
Logs estruturados incluem campos chave-valor, permitindo filtragem e agregação eficientes. Exemplo com logrus:
package main
import (
"github.com/sirupsen/logrus"
)
func main() {
log := logrus.New()
log.SetFormatter(&logrus.JSONFormatter{})
log.WithFields(logrus.Fields{
"event": "user_login",
"user_id": 12345,
"ip": "192.168.1.1",
}).Info("User logged in")
}Boas práticas para logs incluem: usar níveis apropriados (debug, info, warn, error), incluir IDs de correlação (como trace IDs) para relacionar logs a traces, e evitar logs excessivos que podem impactar performance.
OpenTelemetry (visão geral)
OpenTelemetry é um conjunto de APIs, SDKs e ferramentas que padroniza a coleta de métricas, traces e logs. Ele permite que você instrumente seu código uma vez e exporte os dados para diferentes backends (Prometheus, Jaeger, Zipkin, etc.) sem alterar o código. Em Go, o OpenTelemetry é a escolha recomendada para observabilidade moderna.
Para usar OpenTelemetry em Go, você precisa adicionar as dependências e configurar um provider. Exemplo básico de configuração:
package main
import (
"context"
"fmt"
"log"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/sdk/trace"
)
func main() {
// Criar um exporter que imprime no stdout
exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
if err != nil {
log.Fatal(err)
}
// Criar um tracer provider com o exporter
tp := trace.NewTracerProvider(
trace.WithBatcher(exporter),
)
defer tp.Shutdown(context.Background())
otel.SetTracerProvider(tp)
tracer := otel.Tracer("example")
ctx, span := tracer.Start(context.Background(), "main")
defer span.End()
fmt.Println("Hello, OpenTelemetry!")
}OpenTelemetry também fornece instrumentações automáticas para bibliotecas comuns (como HTTP, gRPC, bancos de dados), reduzindo o esforço manual. A integração com logs pode ser feita através de um context de correlação, onde o trace ID é incluído nos logs.
Boas práticas e observações finais
Para uma observabilidade eficaz, é importante combinar os três pilares: use métricas para monitoramento agregado e alertas, traces para investigação de requisições individuais, e logs para detalhes de eventos. Sempre que possível, use OpenTelemetry para unificar a coleta e evitar vendor lock-in. Além disso, estabeleça IDs de correlação (como trace IDs) e inclua-os nos logs para conectar os dados.
Lembre-se de que observabilidade é uma prática contínua: comece com instrumentação básica e evolua conforme necessário. Monitore a saúde do sistema, mas também investigue problemas com profundidade usando traces e logs.
Referências
- Documentação oficial do pacote expvar
- Guia oficial do Prometheus para aplicações Go
- Documentação oficial do OpenTelemetry para Go
- Repositório oficial do Logrus
- Documentação do Zap (log estruturado)
- Conceitos de sinais no OpenTelemetry
- Visão geral de tracing distribuído (OpenTracing)
Exercícios
- Exercício 1: Crie um programa Go que exponha uma métrica do tipo contador usando
expvarpara contar o número de vezes que uma função é chamada, e exponha em/debug/vars.✓ Resposta:package main import ( "expvar" "fmt" "net/http" ) var calls = expvar.NewInt("function_calls_total") func myFunction() { calls.Add(1) fmt.Println("Function called") } func main() { http.HandleFunc("/call", func(w http.ResponseWriter, r *http.Request) { myFunction() w.Write([]byte("OK")) }) http.Handle("/debug/vars", expvar.Handler()) http.ListenAndServe(":8080", nil) } - Exercício 2: Usando a biblioteca
prometheus/client_golang, registre um histograma para medir a duração de uma operação e exponha em/metrics.✓ Resposta:package main import ( "net/http" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) var ( operationDuration = prometheus.NewHistogram(prometheus.HistogramOpts{ Name: "operation_duration_seconds", Help: "Duration of operation in seconds.", Buckets: prometheus.DefBuckets, }) ) func init() { prometheus.MustRegister(operationDuration) } func doOperation() { start := time.Now() defer func() { operationDuration.Observe(time.Since(start).Seconds()) }() // Simula trabalho time.Sleep(100 * time.Millisecond) } func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { doOperation() w.Write([]byte("OK")) }) http.Handle("/metrics", promhttp.Handler()) http.ListenAndServe(":8080", nil) } - Exercício 3: Escreva um programa Go que use OpenTelemetry para criar um trace com dois spans (pai e filho) e exporte para o console.✓ Resposta:
package main import ( "context" "fmt" "log" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" "go.opentelemetry.io/otel/sdk/trace" ) func main() { exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint()) if err != nil { log.Fatal(err) } tp := trace.NewTracerProvider(trace.WithBatcher(exporter)) defer tp.Shutdown(context.Background()) otel.SetTracerProvider(tp) tracer := otel.Tracer("example") ctx, parent := tracer.Start(context.Background(), "parent") defer parent.End() _, child := tracer.Start(ctx, "child") defer child.End() fmt.Println("Trace created") } - Exercício 4: Implemente um logger estruturado usando
logrusque registre um evento de login com campos como usuário e IP, em formato JSON.✓ Resposta:package main import ( "github.com/sirupsen/logrus" ) func main() { log := logrus.New() log.SetFormatter(&logrus.JSONFormatter{}) log.WithFields(logrus.Fields{ "event": "user_login", "user_id": 12345, "ip": "192.168.1.1", }).Info("User logged in") } - Exercício 5: Explique como você correlacionaria logs com traces usando OpenTelemetry. Descreva os passos e mostre um exemplo de código que adiciona o trace ID a um log.✓ Resposta: Para correlacionar logs com traces, você deve extrair o trace ID do contexto e incluí-lo nos campos do log. Com OpenTelemetry, você pode obter o span atual do contexto e usar
SpanContext()para pegar o trace ID. Exemplo:package main import ( "context" "github.com/sirupsen/logrus" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" ) func main() { log := logrus.New() log.SetFormatter(&logrus.JSONFormatter{}) tracer := otel.Tracer("example") ctx, span := tracer.Start(context.Background(), "operation") defer span.End() // Extrair trace ID do contexto spanCtx := trace.SpanContextFromContext(ctx) traceID := spanCtx.TraceID().String() log.WithFields(logrus.Fields{ "trace_id": traceID, "event": "operation_start", }).Info("Operation started") }