OpenTelemetry en Go: traces, métricas, exporters y Jaeger para observabilidad

OpenTelemetry (OTel) es el estándar de facto para instrumentar aplicaciones y recopilar señales de observabilidad: traces, métricas y logs. En Go el SDK oficial se instala con go.opentelemetry.io/otel y tiene exporters para Jaeger, Prometheus, Datadog y cualquier backend compatible con OTLP.

Configurar el TracerProvider con Jaeger vía OTLP

package main

import (
    "context"
    "log"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "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) func() {
    exporter, err := otlptracegrpc.New(ctx,
        otlptracegrpc.WithEndpoint("localhost:4317"),
        otlptracegrpc.WithInsecure(),
    )
    if err != nil {
        log.Fatalf("error al crear exporter: %v", err)
    }

    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter),
        sdktrace.WithResource(resource.NewWithAttributes(
            semconv.SchemaURL,
            semconv.ServiceName("mi-servicio"),
            semconv.ServiceVersion("1.0.0"),
        )),
        sdktrace.WithSampler(sdktrace.TraceIDRatioBased(0.1)), // 10% en prod
    )

    otel.SetTracerProvider(tp)
    return func() { tp.Shutdown(ctx) }
}

func main() {
    ctx := context.Background()
    shutdown := initTracer(ctx)
    defer shutdown()
    // ...
}

Crear y anidar spans con atributos

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

var tracer = otel.Tracer("mi-servicio")

func procesarPedido(ctx context.Context, pedidoID string) error {
    ctx, span := tracer.Start(ctx, "procesarPedido",
        trace.WithAttributes(
            attribute.String("pedido.id", pedidoID),
            attribute.String("pedido.region", "EU"),
        ),
    )
    defer span.End()

    if err := validarPedido(ctx, pedidoID); err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, "validación fallida")
        return err
    }

    span.AddEvent("pedido validado")
    return cobrarPedido(ctx, pedidoID)
}

func validarPedido(ctx context.Context, id string) error {
    _, span := tracer.Start(ctx, "validarPedido")
    defer span.End()
    span.SetAttributes(attribute.String("pedido.id", id))
    // lógica de validación
    return nil
}

Propagación de contexto entre servicios HTTP

El paquete go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp inyecta y extrae el contexto de tracing automáticamente en las cabeceras HTTP:

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

// Servidor: envuelve el handler con otelhttp
mux := http.NewServeMux()
mux.HandleFunc("/api/pedidos", handlerPedidos)
handler := otelhttp.NewHandler(mux, "servidor-pedidos")
http.ListenAndServe(":8080", handler)

// Cliente: envuelve el Transport para propagar el contexto
httpClient := &http.Client{
    Transport: otelhttp.NewTransport(http.DefaultTransport),
}
req, _ := http.NewRequestWithContext(ctx, "GET", "http://servicio-b/datos", nil)
resp, err := httpClient.Do(req)

Métricas con MeterProvider

import (
    "go.opentelemetry.io/otel/metric"
    sdkmetric "go.opentelemetry.io/otel/sdk/metric"
)

func initMeter() metric.Meter {
    exp, _ := otlpmetricgrpc.New(context.Background(),
        otlpmetricgrpc.WithEndpoint("localhost:4317"),
        otlpmetricgrpc.WithInsecure(),
    )
    mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(
        sdkmetric.NewPeriodicReader(exp),
    ))
    otel.SetMeterProvider(mp)
    return mp.Meter("mi-servicio")
}

meter := initMeter()
contador, _ := meter.Int64Counter("pedidos.procesados",
    metric.WithDescription("Total de pedidos procesados"),
)
duracion, _ := meter.Float64Histogram("pedido.duracion_ms",
    metric.WithUnit("ms"),
)

// En el handler:
start := time.Now()
// procesar pedido
contador.Add(ctx, 1, metric.WithAttributes(attribute.String("estado", "ok")))
duracion.Record(ctx, float64(time.Since(start).Milliseconds()))

Sampling para producción

Registrar el 100% de los traces en producción es muy caro. Hay varias estrategias:

// Porcentaje fijo (10%)
sdktrace.TraceIDRatioBased(0.1)

// ParentBased: si el padre trazó, el hijo también (recomendado)
sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))

// Siempre traza (solo para desarrollo)
sdktrace.AlwaysSample()

// Nunca traza
sdktrace.NeverSample()

El sampler ParentBased es el más habitual en producción: respeta la decisión del servicio upstream para mantener la coherencia de los traces distribuidos.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP