Tracing Distribuído e OpenTelemetry em Go: Spans, Contexto e OTLP
Como instrumentar microsserviços Go com tracing distribuído, spans, propagação de contexto e exportação via OTLP com OpenTelemetry.
Publicada em · 5 min de leitura
Tracing Distribuído e OpenTelemetry em Go: Spans, Contexto e OTLP
Em arquiteturas monolíticas simples, diagnosticar erros ou gargalos de performance é direto: basta consultar o log de erros no servidor. Em sistemas distribuídos e microsserviços, no entanto, uma única ação do usuário (como clicar em “Finalizar Compra”) pode atravessar um API Gateway, passar por 5 serviços diferentes via HTTP ou gRPC, consultar 2 bancos relacionais, gravar uma mensagem no Kafka e fazer upload de um recibo no AWS S3.
Se essa transação demorar 1,8 segundos ou falhar silenciosamente no meio do caminho, logs isolados não conseguem correlacionar onde o tempo foi gasto.
O OpenTelemetry (OTel), mantido pela Cloud Native Computing Foundation (CNCF), é o padrão universal da indústria para telemetria neutra de fornecedor. Com ele, você instrumenta seu código Go uma única vez e pode exportar métricas e traces para qualquer plataforma de observabilidade, como Grafana Tempo, Jaeger, Datadog, New Relic ou Dynatrace.
1. Anatomia de um Trace Distribuído
Um Trace representa a trajetória completa de uma requisição através de toda a malha de serviços. Cada etapa individual cronometrada dessa jornada é chamada de Span:
Elementos de um Span:
- Trace ID: Identificador hexadecimal de 16 bytes compartilhado por todos os spans da mesma requisição.
- Span ID: Identificador exclusivo de 8 bytes daquela operação específica.
- Parent Span ID: O identificador do span pai que originou esta chamada (estabelece a árvore causal).
- Nome da Operação: Rótulo descritivo (ex:
POST /api/v1/orders,pgx.QueryRow,s3.Upload). - Timestamps: Horário exato de início e fim (medindo latência em nanossegundos).
- Attributes: Metadados estruturados de contexto (ex:
customer.id = "cust_123",http.status_code = 201). - Events: Anotações com carimbo de tempo ocorridas durante o span (ex:
CacheMiss,SerializationCompleted). - Status:
Unset,OkouError.
2. A Arquitetura do Pipeline OTel
O Go SDK do OpenTelemetry envia dados de telemetria utilizando o protocolo aberto OTLP (OpenTelemetry Protocol) via gRPC ou HTTP:
A melhor prática corporativa é desacoplar o microsserviço dos backends finais: a aplicação envia para um OpenTelemetry Collector local (sidecar ou daemonset), e o Collector é quem cuida de autenticação, amostragem, roteamento e retentativas para o Grafana Tempo ou Datadog.
3. Inicializando o TracerProvider no Go
Configurando o SDK oficial (go.opentelemetry.io/otel e go.opentelemetry.io/otel/sdk):
package telemetry
import (
"context"
"fmt"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// InitTracer inicializa o pipeline de traces OTLP e registra os propagadores globais
func InitTracer(ctx context.Context, serviceName, collectorAddr string) (func(context.Context) error, error) {
// 1. Cria conexão gRPC com o OTel Collector
conn, err := grpc.DialContext(ctx, collectorAddr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
)
if err != nil {
return nil, fmt.Errorf("falha ao conectar ao OTel Collector: %w", err)
}
// 2. Cria o exportador OTLP via gRPC
exporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn))
if err != nil {
return nil, fmt.Errorf("falha ao criar OTLP trace exporter: %w", err)
}
// 3. Define os metadados do recurso (Service Name, Versão, Ambiente)
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceNameKey.String(serviceName),
semconv.ServiceVersionKey.String("1.0.0"),
semconv.DeploymentEnvironmentKey.String("production"),
),
)
if err != nil {
return nil, fmt.Errorf("falha ao criar OTel Resource: %w", err)
}
// 4. Configura o TracerProvider com Amostragem (ex: 10% em produção)
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter), // Exporta em lote em segundo plano
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.10))),
)
// 5. Registra o TracerProvider e o Propagador de Contexto W3C Trace Context globalmente
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
// Retorna a função de shutdown para flush no encerramento da aplicação
return tp.Shutdown, nil
}
4. Instrumentação Automática de Servidores HTTP (otelhttp)
O pacote go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp intercepta requisições HTTP, extrai o header W3C traceparent vindo de clientes ou microsserviços upstream e inicia o Root Span automaticamente:
package main
import (
"net/http"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func SetupRouter() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("POST /api/v1/orders", handleCreateOrder)
// Envolve o roteador com o middleware oficial do OpenTelemetry
return otelhttp.NewHandler(mux, "http-server",
otelhttp.WithMessageEvents(otelhttp.ReadEvents, otelhttp.WriteEvents),
)
}
5. Instrumentação Manual: Criando Spans em Casos de Uso
Nos seus Use Cases e Repositórios da Clean Architecture em Go, utilize o tracer para medir operações de negócio e anexar metadados:
package usecase
import (
"context"
"errors"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
var tracer = otel.Tracer("meu-projeto/internal/usecase")
func (uc *CreateOrderUseCase) Execute(ctx context.Context, input CreateOrderInput) (*CreateOrderOutput, error) {
// 1. Inicia um novo span filho herdado do context
ctx, span := tracer.Start(ctx, "CreateOrderUseCase.Execute",
trace.WithSpanKind(trace.SpanKindInternal),
)
defer span.End() // Finaliza o span no retorno da função
// 2. Anexa atributos de negócio
span.SetAttributes(
attribute.String("customer.id", input.UserID),
attribute.Float64("order.amount", input.TotalAmount),
)
// 3. Executa regra de negócio
order, err := uc.repo.Save(ctx, input)
if err != nil {
// 4. Registra erro formal no Span
span.RecordError(err)
span.SetStatus(codes.Error, "falha ao persistir pedido")
return nil, err
}
// 5. Adiciona um evento pontual na timeline do trace
span.AddEvent("OrderCreatedSuccessfully", trace.WithAttributes(
attribute.String("order.id", order.ID),
))
span.SetStatus(codes.Ok, "sucesso")
return order, nil
}
6. Propagação de Contexto Entre Microsserviços (HTTP Client)
Ao chamar outro microsserviço via HTTP, os cabeçalhos de rastreamento devem ser propagados para que o microsserviço de destino continue o mesmo Trace:
func CallBillingService(ctx context.Context, orderID string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://billing-service/charge", nil)
if err != nil {
return err
}
// Injeta automaticamente os headers 'traceparent' e 'tracestate' na requisição
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
O header traceparent enviado terá o formato padrão W3C:
00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
(versão - trace_id - span_id - flags).
7. Integração Perfeita com Logs Estruturados (slog)
A melhoria mais valiosa na experiência de observabilidade do time de engenharia é clicar em uma linha de log no Grafana ou Datadog e abrir instantaneamente o Trace gráfico correspondente.
Basta extrair o TraceID do span ativo no middleware ou no slog.Handler customizado criado em Logging Estruturado com slog:
package logging
import (
"context"
"log/slog"
"go.opentelemetry.io/otel/trace"
)
type OTelSlogHandler struct {
slog.Handler
}
func (h *OTelSlogHandler) Handle(ctx context.Context, r slog.Record) error {
// Se houver um span ativo no contexto, extrai o TraceID e SpanID
span := trace.SpanFromContext(ctx)
if span.SpanContext().IsValid() {
r.AddAttrs(
slog.String("trace_id", span.SpanContext().TraceID().String()),
slog.String("span_id", span.SpanContext().SpanID().String()),
)
}
return h.Handler.Handle(ctx, r)
}
8. Estratégias de Amostragem (Sampling) em Alta Escala
Em serviços com 50.000 requisições por segundo, rastrear 100% de todas as chamadas geraria terabytes de telemetria por hora e contas impagáveis de armazenamento:
AlwaysSample()(100%): Use apenas em ambiente de desenvolvimento ou staging.TraceIDRatioBased(ratio)(Amostragem Baseada em Razão): Exemplo:0.05coleta exatamente 5% das transações distribuídas aleatoriamente de forma homogênea.ParentBased(sampler)(Recomendado): Se um serviço upstream (ex: API Gateway) decidiu amostrar uma requisição, todos os microsserviços downstream respeitam a decisão e também gravam os seus spans, garantindo que você nunca tenha traces incompletos.
9. Resumo Conceitual
- O OpenTelemetry padroniza a coleta e exportação de traces, logs e métricas sob a governança da CNCF.
- Um Trace é composto por uma árvore de Spans que medem latências individuais de cada operação.
- A propagação entre microsserviços via rede obedece ao padrão W3C Trace Context (
traceparent). - Use
otelhttp.NewHandlerpara instrumentar automaticamente servidores HTTP. - Crie spans manuais em Use Cases críticos com
tracer.Start(ctx, "Operation")edefer span.End(). - Correlacione traces diretamente com o Logging Estruturado com slog injetando
span.SpanContext().TraceID(). - Defina amostradores como
ParentBased(TraceIDRatioBased(0.05))para manter custos e overhead de rede sob controle estrito em produção.