Tracing distribuído com OpenTelemetry
1. Introdução ao Tracing Distribuído e OpenTelemetry
Em arquiteturas de microsserviços, uma única requisição do usuário pode atravessar dezenas de serviços distintos. Sem tracing distribuído, diagnosticar problemas de latência ou falhas em cascata torna-se uma tarefa quase impossível. O tracing distribuído permite rastrear o caminho completo de uma requisição através de múltiplos serviços, fornecendo visibilidade sobre cada etapa do processamento.
O OpenTelemetry surge como o padrão unificado para observabilidade, combinando tracing, métricas e logs em uma única API. Diferentemente de soluções proprietárias, o OpenTelemetry é agnóstico a fornecedores, permitindo que você colete dados e os exporte para qualquer backend compatível (Jaeger, Zipkin, Grafana Tempo, etc.).
Nesta série, já exploramos rate limiting, circuit breaker e métricas. O tracing distribuído complementa essas ferramentas: enquanto métricas mostram o que está acontecendo (ex.: aumento de latência), o tracing revela onde e por que está acontecendo, permitindo identificar gargalos específicos em cada serviço.
2. Configuração Inicial do OpenTelemetry em Go
Para começar, instale as dependências necessárias:
go get go.opentelemetry.io/otel
go get go.opentemetry.io/otel/sdk
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
A configuração básica envolve a criação de um TracerProvider com um exportador. Aqui está um exemplo usando o protocolo OTLP via HTTP:
package main
import (
"context"
"log"
"time"
"go.opentelemetry.io/otel"
"go.opentemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
)
func initTracer() (*sdktrace.TracerProvider, error) {
ctx := context.Background()
exporter, err := otlptrace.New(ctx, otlptracehttp.NewClient(
otlptracehttp.WithEndpoint("localhost:4318"),
otlptracehttp.WithInsecure(),
))
if err != nil {
return nil, err
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("meu-servico"),
attribute.String("environment", "production"),
)),
sdktrace.WithSampler(sdktrace.AlwaysSample()),
)
otel.SetTracerProvider(tp)
return tp, nil
}
3. Criação e Gerenciamento de Spans
Spans representam unidades de trabalho dentro de um trace. Com o TracerProvider configurado, podemos criar spans facilmente:
func processRequest(ctx context.Context, userID string) error {
tracer := otel.Tracer("meu-servico")
ctx, span := tracer.Start(ctx, "processRequest")
defer span.End()
// Adicionando atributos
span.SetAttributes(
attribute.String("user.id", userID),
attribute.Int("request.size", 1024),
)
// Adicionando eventos
span.AddEvent("cache.check")
// Simulando processamento
if err := heavyComputation(ctx); err != nil {
span.SetStatus(codes.Error, err.Error())
span.RecordError(err)
return err
}
span.SetStatus(codes.Ok, "request processed successfully")
return nil
}
4. Propagação de Contexto entre Serviços
A propagação de contexto é crucial para conectar spans entre serviços. O OpenTelemetry utiliza o formato W3C TraceContext para propagar informações através de cabeçalhos HTTP.
Para propagação manual em chamadas HTTP:
import (
"go.opentelemetry.io/otel/propagation"
"net/http"
)
func makeOutgoingRequest(ctx context.Context, url string) error {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
// Injeta contexto nos cabeçalhos HTTP
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))
client := &http.Client{}
_, err := client.Do(req)
return err
}
Para extração automática em servidores HTTP, use o middleware otelhttp:
import (
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func main() {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// O contexto já contém o span extraído dos cabeçalhos
ctx := r.Context()
// ... processamento
})
wrappedHandler := otelhttp.NewHandler(handler, "meu-endpoint")
http.Handle("/api/users", wrappedHandler)
http.ListenAndServe(":8080", nil)
}
Para gRPC, utilize os interceptors do pacote otelgrpc:
import (
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"google.golang.org/grpc"
)
func startGRPCServer() {
s := grpc.NewServer(
grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor()),
)
// registrar serviços...
}
5. Instrumentação Automática de Bibliotecas Comuns
O ecossistema OpenTelemetry oferece instrumentação automática para bibliotecas populares.
Para clientes HTTP:
import (
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func createInstrumentedClient() *http.Client {
return &http.Client{
Transport: otelhttp.NewTransport(
http.DefaultTransport,
otelhttp.WithClientTrace(func(ctx context.Context) *httptrace.ClientTrace {
return otelhttptrace.NewClientTrace(ctx)
}),
),
}
}
Para Redis com go-redis:
import (
"github.com/redis/go-redis/v9"
"go.opentelemetry.io/contrib/instrumentation/github.com/redis/go-redis/otelredis"
)
func createInstrumentedRedis() *redis.Client {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
rdb.AddHook(otelredis.NewHook(redisOptions))
return rdb
}
Para o framework Gin:
import (
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.Use(otelgin.Middleware("meu-servico-gin"))
r.GET("/users", func(c *gin.Context) {
// O span já está disponível no contexto
span := trace.SpanFromContext(c.Request.Context())
span.AddEvent("processing users request")
c.JSON(200, gin.H{"message": "ok"})
})
r.Run(":8080")
}
6. Coleta e Exportação de Dados de Trace
A configuração do exportador Jaeger é semelhante ao OTLP:
import (
"go.opentelemetry.io/otel/exporters/jaeger"
)
func initJaegerExporter() (*jaeger.Exporter, error) {
return jaeger.New(jaeger.WithCollectorEndpoint(
jaeger.WithEndpoint("http://localhost:14268/api/traces"),
))
}
Para amostragem inteligente, configure um Sampler personalizado:
tp := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sdktrace.ParentBased(
sdktrace.TraceIDRatioBased(0.1), // 10% das requisições
)),
)
7. Boas Práticas e Exemplo Completo
Aqui está um exemplo completo de dois microsserviços instrumentados:
// Serviço API
func main() {
tp, _ := initTracer()
defer tp.Shutdown(context.Background())
handler := otelhttp.NewHandler(
http.HandlerFunc(apiHandler),
"api.request",
otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
return fmt.Sprintf("%s %s", r.Method, r.URL.Path)
}),
)
http.Handle("/process", handler)
http.ListenAndServe(":8080", nil)
}
func apiHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tracer := otel.Tracer("api-service")
_, span := tracer.Start(ctx, "validate_input")
span.SetAttributes(attribute.String("input.type", "json"))
time.Sleep(10 * time.Millisecond)
span.End()
// Chamada para worker service
callWorkerService(ctx)
}
// Serviço Worker
func workerHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tracer := otel.Tracer("worker-service")
ctx, span := tracer.Start(ctx, "process_task")
defer span.End()
span.SetAttributes(
attribute.String("task.type", "heavy_computation"),
attribute.Int("task.priority", 1),
)
// Simula processamento
time.Sleep(50 * time.Millisecond)
span.AddEvent("task.completed")
}
Boas práticas essenciais:
- Use
SpanKindapropriado:Serverpara handlers,Clientpara chamadas externas,Internalpara operações internas - Nomeie spans consistentemente usando
{entidade}.{ação}(ex.:user.create,payment.process) - Sempre feche spans com
defer span.End()para evitar vazamentos - Propague contexto explicitamente em operações assíncronas e goroutines
O tracing distribuído com OpenTelemetry em Go oferece uma base sólida para observabilidade em microsserviços. Combinado com métricas (Prometheus) e circuit breakers, você terá visibilidade completa sobre a saúde e performance do seu sistema.
Referências
- OpenTelemetry Go Documentation — Documentação oficial da implementação Go do OpenTelemetry, incluindo guias de configuração e exemplos
- OpenTelemetry Go Contrib Repository — Repositório oficial com instrumentações para bibliotecas populares como Gin, gRPC e Redis
- Jaeger Documentation — Documentação oficial do Jaeger, um dos backends mais populares para visualização de traces
- W3C Trace Context Specification — Especificação oficial do formato de propagação de contexto utilizado pelo OpenTelemetry
- Grafana Tempo Documentation — Documentação do Grafana Tempo, alternativa moderna para armazenamento e consulta de traces
- OpenTelemetry Sampling Guide — Guia sobre estratégias de amostragem para balancear cobertura e desempenho