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 SpanKind apropriado: Server para handlers, Client para chamadas externas, Internal para 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