Desarrollo backend

Distributed Tracing en microservicios: trazabilidad extremo a extremo con OpenTelemetry, Jaeger y Go

Ruslan Ismailov Publicado 14 min de lectura
D

Introducción: por qué es necesario el distributed tracing en 2026

Cuando un monolito se descompone en decenas de microservicios, la depuración se convierte en una tarea no trivial. Una solicitud de usuario atraviesa el API Gateway, varios servicios de negocio, una cola de mensajes, una caché Redis y PostgreSQL — y en algún punto de esa cadena aparece una latencia de 800 ms que reportan los clientes. Encontrar el cuello de botella sin herramientas de observabilidad es prácticamente imposible.

El distributed tracing es un mecanismo de trazabilidad extremo a extremo de las solicitudes a través de todos los componentes de un sistema distribuido. Cada solicitud recibe un trace_id único, y cada operación dentro del servicio genera un span con marcas de tiempo, atributos y relaciones. El resultado es una imagen completa: dónde se invierte el tiempo, qué servicios se invocan y dónde se producen errores.

En 2026, el estándar de facto es OpenTelemetry — un proyecto vendor-neutral de la CNCF que unificó OpenTracing y OpenCensus. Combinado con Jaeger como backend para almacenamiento y visualización de trazas, y Go como lenguaje de implementación de microservicios, esto proporciona un stack de observabilidad listo para producción.

Visión general de OpenTelemetry: estándar, SDK y exportadores

OpenTelemetry es un conjunto de APIs, SDKs y herramientas para la recolección de telemetría: trazas, métricas y logs. La arquitectura consta de varios componentes clave:

  • API — interfaces independientes de la implementación concreta. Las bibliotecas se instrumentan a través de la API sin vincularse al SDK.
  • SDK — implementación de la API con batching, sampling y procesamiento de datos.
  • Exporters — componentes que envían datos al backend: Jaeger, Zipkin, OTLP, Prometheus.
  • Collector — agente/proxy opcional que recibe datos de las aplicaciones y los enruta hacia uno o varios backends.
  • Instrumentation Libraries — integraciones listas para usar con net/http, gRPC, database/sql, Redis y otros.

Para Go, el paquete principal es go.opentelemetry.io/otel. El SDK de tracing es go.opentelemetry.io/otel/sdk/trace. El exportador hacia Jaeger mediante OTLP gRPC es go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc.

Instrumentación de servicios Go con otel-go

Inicialización del proveedor de trazas

Lo primero es configurar el TracerProvider — el objeto central que gestiona el ciclo de vida de las trazas. Normalmente esto se hace al arrancar la aplicación:

package telemetry

import (
    "context"
    "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.21.0"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

func InitTracer(ctx context.Context, serviceName, collectorAddr string) (func(), error) {
    conn, err := grpc.DialContext(ctx, collectorAddr,
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithBlock(),
    )
    if err != nil {
        return nil, err
    }

    exporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn))
    if err != nil {
        return nil, err
    }

    res, err := resource.New(ctx,
        resource.WithAttributes(
            semconv.ServiceName(serviceName),
            semconv.ServiceVersion("1.0.0"),
            semconv.DeploymentEnvironment("production"),
        ),
    )
    if err != nil {
        return nil, err
    }

    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter,
            sdktrace.WithBatchTimeout(5*time.Second),
            sdktrace.WithMaxExportBatchSize(512),
        ),
        sdktrace.WithResource(res),
        sdktrace.WithSampler(sdktrace.ParentBased(
            sdktrace.TraceIDRatioBased(0.1), // 10% de sampling en prod
        )),
    )

    otel.SetTracerProvider(tp)
    otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
        propagation.TraceContext{},
        propagation.Baggage{},
    ))

    shutdown := func() {
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        _ = tp.Shutdown(ctx)
    }

    return shutdown, nil
}

Creación de spans y propagación del contexto

El principio clave es que el contexto de Go (context.Context) es el portador de la información sobre la traza actual. Propágalo a través de todas las capas de la aplicación:

package service

import (
    "context"
    "fmt"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/codes"
)

var tracer = otel.Tracer("order-service")

type OrderService struct {
    repo   OrderRepository
    cache  CacheClient
}

func (s *OrderService) GetOrder(ctx context.Context, orderID string) (*Order, error) {
    ctx, span := tracer.Start(ctx, "OrderService.GetOrder")
    defer span.End()

    span.SetAttributes(
        attribute.String("order.id", orderID),
        attribute.String("component", "order-service"),
    )

    // Verificamos la caché
    order, err := s.cache.Get(ctx, "order:"+orderID)
    if err == nil {
        span.SetAttributes(attribute.Bool("cache.hit", true))
        return order, nil
    }

    span.SetAttributes(attribute.Bool("cache.hit", false))

    // Consultamos la BD
    order, err = s.repo.FindByID(ctx, orderID)
    if err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, err.Error())
        return nil, fmt.Errorf("repo.FindByID: %w", err)
    }

    return order, nil
}

Configuración de Jaeger como backend para la recolección de trazas

Jaeger es un sistema de trazabilidad open-source desarrollado por Uber y adoptado por la CNCF. En modo de desarrollo es conveniente utilizar la imagen all-in-one. A continuación se muestra la configuración de docker-compose.yml:

version: "3.9"

services:
  jaeger:
    image: jaegertracing/all-in-one:1.55
    environment:
      - COLLECTOR_OTLP_ENABLED=true
      - SPAN_STORAGE_TYPE=badger
      - BADGER_EPHEMERAL=false
      - BADGER_DIRECTORY_VALUE=/badger/data
      - BADGER_DIRECTORY_KEY=/badger/key
    volumes:
      - jaeger-data:/badger
    ports:
      - "16686:16686"   # UI
      - "4317:4317"     # OTLP gRPC
      - "4318:4318"     # OTLP HTTP
      - "14268:14268"   # Jaeger HTTP collector
    restart: unless-stopped

  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.95.0
    command: ["--config=/etc/otel-collector-config.yaml"]
    volumes:
      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
    ports:
      - "4319:4317"  # OTLP gRPC desde las aplicaciones
    depends_on:
      - jaeger

volumes:
  jaeger-data:

Configuración del OpenTelemetry Collector (otel-collector-config.yaml):

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 1s
    send_batch_size: 1024
  memory_limiter:
    limit_mib: 512
    spike_limit_mib: 128
    check_interval: 5s

exporters:
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true
  logging:
    loglevel: warn

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlp/jaeger, logging]

Para producción se recomienda utilizar Jaeger con Elasticsearch o Cassandra como almacenamiento backend en lugar de badger. Configure SPAN_STORAGE_TYPE=elasticsearch e indique la dirección del clúster.

Integración de la trazabilidad con endpoints REST API y gRPC

Middleware para REST API (net/http)

Para servidores HTTP se utiliza la biblioteca go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp:

package middleware

import (
    "net/http"

    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

func NewTracingMiddleware(serviceName string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return otelhttp.NewHandler(next, serviceName,
            otelhttp.WithMessageEvents(
                otelhttp.ReadEvents,
                otelhttp.WriteEvents,
            ),
        )
    }
}

// Cliente HTTP con trazabilidad
func NewTracedHTTPClient() *http.Client {
    return &http.Client{
        Transport: otelhttp.NewTransport(http.DefaultTransport),
    }
}

Interceptores para gRPC

Para los servicios gRPC utilizamos interceptors del paquete go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc:

package server

import (
    "google.golang.org/grpc"
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)

func NewGRPCServer() *grpc.Server {
    return grpc.NewServer(
        grpc.StatsHandler(otelgrpc.NewServerHandler(
            otelgrpc.WithMessageEvents(
                otelgrpc.ReceivedEvents,
                otelgrpc.SentEvents,
            ),
        )),
    )
}

func NewGRPCClientConn(target string) (*grpc.ClientConn, error) {
    return grpc.Dial(target,
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
    )
}

Correlación de trazas con logs y métricas

El máximo valor de la observabilidad se alcanza cuando las trazas, los logs y las métricas están interrelacionados. La clave es insertar el trace_id y el span_id en cada entrada de log:

package logger

import (
    "context"
    "log/slog"
    "os"

    "go.opentelemetry.io/otel/trace"
)

type TraceHandler struct {
    handler slog.Handler
}

func (h *TraceHandler) Handle(ctx context.Context, r slog.Record) error {
    span := trace.SpanFromContext(ctx)
    if span.IsRecording() {
        sc := span.SpanContext()
        r.AddAttrs(
            slog.String("trace_id", sc.TraceID().String()),
            slog.String("span_id", sc.SpanID().String()),
            slog.String("trace_flags", sc.TraceFlags().String()),
        )
    }
    return h.handler.Handle(ctx, r)
}

func NewLogger() *slog.Logger {
    base := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelInfo,
    })
    return slog.New(&TraceHandler{handler: base})
}

Con este enfoque, en Grafana o Kibana puedes usar el trace_id de Jaeger para encontrar al instante todos los logs relacionados con una solicitud específica. Para las métricas utiliza go.opentelemetry.io/otel/metric — agrega los atributos service.name y environment a cada métrica para que la correlación funcione mediante exemplars en Prometheus.

Ejemplo práctico: trazabilidad en una cadena de tres servicios Go

Consideremos un sistema formado por tres servicios: API Gateway, Order Service e Inventory Service. La solicitud de creación de un pedido recorre toda la cadena, pasando por Redis (caché) y PostgreSQL (almacenamiento).

Instrumentación de PostgreSQL mediante database/sql

package db

import (
    "context"
    "database/sql"

    "github.com/XSAM/otelsql"
    semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
    _ "github.com/lib/pq"
)

func NewPostgresDB(dsn string) (*sql.DB, error) {
    db, err := otelsql.Open("postgres", dsn,
        otelsql.WithAttributes(
            semconv.DBSystemPostgreSQL,
        ),
        otelsql.WithSpanOptions(otelsql.SpanOptions{
            Ping:                 true,
            RowsAffected:        true,
            DisableErrSkip:      true,
        }),
    )
    if err != nil {
        return nil, err
    }
    
    if err := otelsql.RegisterDBStatsMetrics(db,
        otelsql.WithAttributes(semconv.DBSystemPostgreSQL),
    ); err != nil {
        return nil, err
    }
    
    return db, nil
}

Instrumentación de Redis

package cache

import (
    "context"

    "github.com/redis/go-redis/extra/redisotel/v9"
    "github.com/redis/go-redis/v9"
)

func NewRedisClient(addr string) (*redis.Client, error) {
    rdb := redis.NewClient(&redis.Options{
        Addr: addr,
        DB:   0,
    })

    // Habilitamos trazabilidad y métricas de Redis
    if err := redisotel.InstrumentTracing(rdb,
        redisotel.WithDBStatement(true),
    ); err != nil {
        return nil, err
    }

    return rdb, nil
}

Manejador completo de pedidos con spans hijos

package handler

import (
    "context"
    "encoding/json"
    "net/http"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/codes"
    "go.opentelemetry.io/otel/trace"
)

var tracer = otel.Tracer("api-gateway")

type CreateOrderRequest struct {
    UserID    string   `json:"user_id"`
    ProductID string   `json:"product_id"`
    Quantity  int      `json:"quantity"`
}

func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    
    ctx, span := tracer.Start(ctx, "CreateOrder",
        trace.WithSpanKind(trace.SpanKindServer),
    )
    defer span.End()

    var req CreateOrderRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, "invalid request body")
        http.Error(w, "Bad Request", http.StatusBadRequest)
        return
    }

    span.SetAttributes(
        attribute.String("user.id", req.UserID),
        attribute.String("product.id", req.ProductID),
        attribute.Int("order.quantity", req.Quantity),
    )

    // Verificamos la disponibilidad en Inventory Service
    available, err := h.checkInventory(ctx, req.ProductID, req.Quantity)
    if err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, "inventory check failed")
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }
    span.SetAttributes(attribute.Bool("inventory.available", available))

    if !available {
        span.SetStatus(codes.Error, "out of stock")
        http.Error(w, "Product out of stock", http.StatusConflict)
        return
    }

    // Creamos el pedido en Order Service
    orderID, err := h.createOrder(ctx, req)
    if err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, "order creation failed")
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }

    span.SetAttributes(attribute.String("order.id", orderID))
    span.SetStatus(codes.Ok, "order created")

    json.NewEncoder(w).Encode(map[string]string{"order_id": orderID})
}

func (h *Handler) checkInventory(ctx context.Context, productID string, qty int) (bool, error) {
    ctx, span := tracer.Start(ctx, "checkInventory",
        trace.WithSpanKind(trace.SpanKindClient),
    )
    defer span.End()

    span.SetAttributes(
        attribute.String("rpc.service", "inventory-service"),
        attribute.String("product.id", productID),
    )

    // Llamada HTTP con propagación del contexto
    resp, err := h.inventoryClient.CheckStock(ctx, productID, qty)
    if err != nil {
        span.RecordError(err)
        return false, err
    }

    return resp.Available, nil
}

Errores habituales y cómo evitarlos

  • Pérdida de contexto. El error más frecuente es pasar context.Background() en lugar del ctx de la función llamante. Propaga siempre el contexto a través de todas las capas.
  • Ausencia de propagación en clientes HTTP. Al crear solicitudes HTTP manualmente, invoca otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header)); de lo contrario, los servicios hijos no recibirán el trace context.
  • Sampling excesivo en producción. Trazar el 100% de las solicitudes en un servicio de alta carga genera una sobrecarga considerable. Usa TraceIDRatioBased(0.01–0.1) e incrementa la tasa durante la investigación de incidentes.
  • Spans sin cerrar. Llama siempre a defer span.End() inmediatamente después de tracer.Start(). Los spans no cerrados no se exportan y provocan fugas de memoria.
  • Atributos demasiado detallados con PII. No registres datos personales de usuarios (correo electrónico, teléfono, números de tarjeta) en los atributos de los spans. Esto viola el RGPD y genera riesgos de seguridad.
  • Ignorar los errores del exportador. En producción, configura alertas sobre la métrica otelcol_exporter_send_failed_spans en el Collector para detectar a tiempo los problemas de entrega de trazas.
  • Un único tracer para toda la aplicación. Crea tracers con nombre mediante otel.Tracer("package-name") para cada paquete — esto simplifica el filtrado en Jaeger.

Consejos para producción

Algunas recomendaciones para entornos productivos:

  • Usa el OpenTelemetry Collector como capa intermedia entre las aplicaciones y Jaeger — esto permite cambiar el backend sin recompilar los servicios, añadir enriquecimiento de datos y tail-based sampling.
  • Configura Jaeger con Elasticsearch para almacenar trazas durante más de 48 horas. Indexa por service.name, span.kind y http.status_code.
  • Añade Span Events para eventos de negocio importantes dentro de un span: span.AddEvent("cache.miss", trace.WithAttributes(attribute.String("key", cacheKey))).
  • Integra Jaeger con Grafana mediante el plugin de datasource para tener un dashboard unificado con métricas, logs y trazas (Grafana Tempo como alternativa a Jaeger ofrece integración nativa).

Conclusión

El distributed tracing con OpenTelemetry, Jaeger y Go no es solo una herramienta de depuración, sino la base de una estrategia moderna de observabilidad. La implementación requiere un esfuerzo puntual: configurar el TracerProvider, añadir middleware para HTTP y gRPC, instrumentar PostgreSQL y Redis. Pero el retorno supera con creces el coste: el tiempo de diagnóstico de incidentes se reduce de horas a minutos.

Los principios clave para una implementación exitosa son: disciplina estricta en la propagación del contexto, sampling inteligente en producción, correlación de trazas con logs y métricas, y el uso del OpenTelemetry Collector como buffer entre las aplicaciones y el backend. Comienza instrumentando el camino crítico de tu aplicación y verás rápidamente el valor real de la trazabilidad extremo a extremo de las solicitudes.

Tecnologías

Etiquetas

Ruslan Ismailov

Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →