← Voltar para o blog

OpenTelemetry com Go: Tracing Distribuído e Métricas na Prática

Aprenda a instrumentar aplicações Go com OpenTelemetry: tracing distribuído, métricas, exporters para Jaeger e Prometheus. Guia prático com exemplos de código.

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

AspectoOTel SDKPrometheus Clientzerolog/slog
Tracing✅ Completo
Métricas✅ Completo✅ Especializado
Logs✅ Em evolução✅ Especializado
Vendor lock-inNenhumPrometheus onlyNenhum
OverheadBaixoMuito baixoMuito 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.