OpenTelemetry (OTel) se consolidou como o padrão da indústria para observabilidade. Mantido pela CNCF, ele unifica tracing distribuído, métricas e logs em uma única API — e o SDK para Go é um dos mais maduros do ecossistema. Se você já trabalha com logging estruturado via slog, adicionar tracing e métricas com OTel é o próximo passo natural para ter visibilidade completa das suas aplicações em produção.
Por que OpenTelemetry?
Antes do OpenTelemetry, instrumentar uma aplicação significava escolher entre OpenTracing, OpenCensus e ferramentas proprietárias. OTel unificou tudo em um único framework vendor-neutral. Você instrumenta uma vez e exporta para qualquer backend: Jaeger, Zipkin, Prometheus, Grafana Tempo, Datadog ou New Relic.
Para aplicações Go em produção — especialmente microsserviços com gRPC ou APIs REST — tracing distribuído não é luxo, é necessidade. Sem ele, debugar latência entre serviços vira adivinhação.
Instalando o SDK
O OpenTelemetry Go é distribuído em múltiplos módulos. Para tracing e métricas, você precisa do SDK base e dos exporters:
go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/sdk
go get go.opentelemetry.io/otel/sdk/metric
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
go get go.opentelemetry.io/otel/exporters/prometheus
Se você usa Go modules, essas dependências se integram ao seu go.mod normalmente.
Configurando o Provider de Traces
O primeiro passo é configurar o TracerProvider, que gerencia o ciclo de vida dos spans e a exportação para o backend:
package main
import (
"context"
"log"
"time"
"go.opentelemetry.io/otel"
"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.26.0"
)
func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
exporter, err := otlptracehttp.New(ctx,
otlptracehttp.WithEndpoint("localhost:4318"),
otlptracehttp.WithInsecure(),
)
if err != nil {
return nil, err
}
res := resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("meu-servico-go"),
semconv.ServiceVersionKey.String("1.0.0"),
)
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter,
sdktrace.WithBatchTimeout(5*time.Second),
),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.AlwaysSample()),
)
otel.SetTracerProvider(tp)
return tp, nil
}
O WithBatcher agrupa spans antes de enviá-los, reduzindo overhead de rede. O Resource identifica seu serviço nos dashboards — essencial quando você tem dezenas de microsserviços.
Criando Spans
Com o provider configurado, criar spans é direto:
package main
import (
"context"
"fmt"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
)
var tracer = otel.Tracer("meu-servico-go")
func processarPedido(ctx context.Context, pedidoID string) error {
ctx, span := tracer.Start(ctx, "processar-pedido")
defer span.End()
span.SetAttributes(
attribute.String("pedido.id", pedidoID),
attribute.String("pedido.tipo", "compra"),
)
// Span filho para validação
if err := validarPedido(ctx, pedidoID); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "validação falhou")
return err
}
// Span filho para persistência
if err := salvarPedido(ctx, pedidoID); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "persistência falhou")
return err
}
span.SetStatus(codes.Ok, "pedido processado")
return nil
}
func validarPedido(ctx context.Context, pedidoID string) error {
ctx, span := tracer.Start(ctx, "validar-pedido")
defer span.End()
// Simula validação
time.Sleep(10 * time.Millisecond)
span.AddEvent("validação concluída",
attribute.String("pedido.id", pedidoID),
)
return nil
}
func salvarPedido(ctx context.Context, pedidoID string) error {
_, span := tracer.Start(ctx, "salvar-pedido")
defer span.End()
// Simula escrita no banco
time.Sleep(25 * time.Millisecond)
return nil
}
Cada chamada a tracer.Start cria um span filho automaticamente, desde que o context.Context seja propagado corretamente. É por isso que usar context em Go de forma consistente é tão importante — ele carrega o trace ID por toda a cadeia de chamadas.
Instrumentando HTTP com Middleware
Para capturar automaticamente todas as requisições HTTP, use o middleware do OTel:
import (
"net/http"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/pedidos", pedidosHandler)
mux.HandleFunc("/health", healthHandler)
// Wrapping com OTel — cada request gera um span
handler := otelhttp.NewHandler(mux, "meu-servico-http",
otelhttp.WithMessageEvents(
otelhttp.ReadEvents,
otelhttp.WriteEvents,
),
)
http.ListenAndServe(":8080", handler)
}
O middleware cria spans para cada requisição, registra método HTTP, status code, duração e tamanho do body — tudo automaticamente. Para o lado cliente, use otelhttp.NewTransport no http.Client:
client := &http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
// Requisições feitas com este client propagam o trace context
resp, err := client.Get("https://api-externa.com/dados")
Métricas com OpenTelemetry
Além de tracing, OTel oferece uma API completa para métricas. Veja como configurar o provider e criar instrumentos:
import (
"context"
"go.opentelemetry.io/otel/metric"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/exporters/prometheus"
)
func initMetrics() (*sdkmetric.MeterProvider, error) {
exporter, err := prometheus.New()
if err != nil {
return nil, err
}
mp := sdkmetric.NewMeterProvider(
sdkmetric.WithReader(exporter),
)
return mp, nil
}
// Usando métricas no código
var meter = otel.Meter("meu-servico-go")
func registrarMetricas() {
// Contador de pedidos
pedidosTotal, _ := meter.Int64Counter("pedidos_total",
metric.WithDescription("Total de pedidos processados"),
metric.WithUnit("{pedido}"),
)
// Histograma de latência
latencia, _ := meter.Float64Histogram("pedido_latencia_ms",
metric.WithDescription("Latência do processamento de pedidos"),
metric.WithUnit("ms"),
metric.WithExplicitBucketBoundaries(5, 10, 25, 50, 100, 250, 500),
)
// Gauge para pedidos em fila
filaGauge, _ := meter.Int64UpDownCounter("pedidos_fila",
metric.WithDescription("Pedidos aguardando processamento"),
)
// Uso
ctx := context.Background()
pedidosTotal.Add(ctx, 1,
metric.WithAttributes(attribute.String("tipo", "compra")),
)
latencia.Record(ctx, 42.5)
filaGauge.Add(ctx, 1) // pedido entrou na fila
filaGauge.Add(ctx, -1) // pedido saiu da fila
}
Com o exporter Prometheus, essas métricas ficam disponíveis no endpoint /metrics para scraping. Se você já usa Prometheus com Go, o OTel se integra perfeitamente ao seu setup existente.
Auto-Instrumentação com eBPF
Uma das novidades mais empolgantes do OTel para Go é a auto-instrumentação via eBPF. Diferente de linguagens com runtime (Java, Python), Go compila para binário estático — então a instrumentação automática usa eBPF para injetar tracing em processos rodando sem alterar o código-fonte:
# Instalar o agente de auto-instrumentação
go install go.opentelemetry.io/auto/cmd/instrumentor@latest
# Executar o agente apontando para o PID do processo
sudo instrumentor -pid $(pidof meu-servico)
O agente detecta automaticamente chamadas a net/http, database/sql, google.golang.org/grpc e outros pacotes populares, gerando spans sem uma linha de instrumentação manual. Desde o Go SDK v1.36.0, o Auto SDK é importado automaticamente como dependência indireta da API padrão.
Propagação de Contexto entre Serviços
Para que traces funcionem entre múltiplos serviços, o trace context precisa ser propagado nos headers HTTP. O OTel configura isso automaticamente via o padrão W3C Trace Context:
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)
func init() {
otel.SetTextMapPropagator(
propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
),
)
}
Com isso configurado, quando o serviço A chama o serviço B via HTTP, o trace ID é propagado no header traceparent. O serviço B continua o mesmo trace, criando spans filhos — e no Jaeger ou Grafana Tempo você vê a timeline completa da requisição cruzando todos os serviços.
Inicializando Tudo no main
Juntando tracing e métricas na função main:
func main() {
ctx := context.Background()
// Inicializa tracing
tp, err := initTracer(ctx)
if err != nil {
log.Fatal(err)
}
defer tp.Shutdown(ctx)
// Inicializa métricas
mp, err := initMetrics()
if err != nil {
log.Fatal(err)
}
defer mp.Shutdown(ctx)
otel.SetMeterProvider(mp)
// Servidor HTTP com instrumentação
mux := http.NewServeMux()
mux.HandleFunc("/pedidos", pedidosHandler)
handler := otelhttp.NewHandler(mux, "api")
log.Println("Servidor rodando em :8080")
log.Fatal(http.ListenAndServe(":8080", handler))
}
O defer Shutdown garante que spans e métricas pendentes sejam exportados antes do processo encerrar — importante em ambientes de containers onde pods podem ser terminados a qualquer momento.
OTel vs Prometheus Client vs zerolog: Quando Usar Cada Um
| Aspecto | OTel SDK | Prometheus Client | zerolog/slog |
|---|---|---|---|
| Tracing | ✅ Completo | ❌ | ❌ |
| Métricas | ✅ Completo | ✅ Especializado | ❌ |
| Logs | ✅ Em evolução | ❌ | ✅ Especializado |
| Vendor lock-in | Nenhum | Prometheus only | Nenhum |
| Overhead | Baixo | Muito baixo | Muito baixo |
Na prática, muitos projetos combinam OTel para tracing + Prometheus para métricas + slog para logs. Com o bridge do OTel para Prometheus, essa combinação funciona sem conflitos.
Próximos Passos
OpenTelemetry transforma a forma como você entende suas aplicações Go em produção. Comece pelo tracing HTTP — é o que dá retorno mais rápido. Depois adicione métricas de negócio (pedidos por minuto, latência por endpoint) e evolua para tracing em bancos de dados e filas.
Para uma visão completa de observabilidade incluindo alerting, veja nosso tutorial de observabilidade em Go. Se você trabalha com padrões de concorrência, instrumentar goroutines com spans ajuda a identificar gargalos que não aparecem em profiling tradicional.
Se você também trabalha com stacks fora de Go, veja como Python lida com observabilidade para comparar abordagens entre linguagens.
FAQ
Qual a diferença entre OpenTelemetry e OpenTracing? OpenTelemetry é o sucessor do OpenTracing e do OpenCensus, unificando tracing, métricas e logs em um único framework. OpenTracing está em modo de manutenção — todo projeto novo deve usar OTel.
OTel adiciona muito overhead à minha aplicação?
O overhead é mínimo com sampling configurado corretamente. Em produção, use ParentBasedSampler com taxa de 10-50% em vez de AlwaysSample. O batching de spans também reduz o impacto na rede.
Preciso instrumentar todo o código manualmente?
Não. O middleware HTTP cobre a maioria dos casos automaticamente. Para Go, a auto-instrumentação via eBPF pode instrumentar net/http, database/sql e gRPC sem alterar código. Instrumentação manual é recomendada apenas para lógica de negócio específica.
Posso usar OTel com Jaeger? Sim. Jaeger aceita dados via OTLP nativamente desde a versão 1.35. Configure o exporter OTLP apontando para o endpoint do Jaeger e os traces aparecem no dashboard sem configuração adicional.