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.
