Pular para o conteúdo

Trilha de Go · parte 32 de 37

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

Tamanho da fonte:
Largura da coluna:

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:

Anatomia de um Trace e Spans no OpenTelemetry
figura 1 · Anatomia de um Trace e Spans no OpenTelemetry

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, Ok ou Error.

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:

Pipeline de Telemetria OpenTelemetry (OTel)
figura 2 · Pipeline de Telemetria OpenTelemetry (OTel)

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:

  1. AlwaysSample() (100%): Use apenas em ambiente de desenvolvimento ou staging.
  2. TraceIDRatioBased(ratio) (Amostragem Baseada em Razão): Exemplo: 0.05 coleta exatamente 5% das transações distribuídas aleatoriamente de forma homogênea.
  3. 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.NewHandler para instrumentar automaticamente servidores HTTP.
  • Crie spans manuais em Use Cases críticos com tracer.Start(ctx, "Operation") e defer 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.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?