Trazado OpenTelemetry en Go para visibilidad de extremo a extremo de APIs
Trazado OpenTelemetry en Go explicado con pasos prácticos para correlacionar trazas, métricas y logs a través de peticiones HTTP, trabajos en segundo plano y llamadas a terceros.

Qué significa el trazado de extremo a extremo para una API en Go
Una traza es la línea de tiempo de una petición mientras se mueve por tu sistema. Empieza cuando llega una llamada a la API y termina cuando envías la respuesta.
Dentro de una traza hay spans. Un span es un paso medido en el tiempo, como “parsear la petición”, “ejecutar SQL” o “llamar al proveedor de pagos”. Los spans también pueden llevar detalles útiles, como un código de estado HTTP, un identificador de usuario no sensible o cuántas filas devolvió una consulta.
“Extremo a extremo” significa que la traza no se corta en tu primer handler. Sigue la petición por los lugares donde suelen esconderse los problemas: middleware, consultas a la base de datos, llamadas a caché, trabajos en background, APIs de terceros (pagos, email, mapas) y otros servicios internos.
El trazado es especialmente valioso cuando los problemas son intermitentes. Si una de cada 200 peticiones es lenta, los logs suelen verse idénticos en los casos rápidos y lentos. Una traza deja la diferencia clara: una petición pasó 800 ms esperando una llamada externa, reintentó dos veces y luego disparó un trabajo en background.
También es difícil conectar logs entre servicios. Puedes tener una línea de log en la API, otra en un worker y nada en medio. Con trazado, esos eventos comparten el mismo trace ID, así puedes seguir la cadena sin adivinar.
Trazas, métricas y logs: cómo encajan
Trazas, métricas y logs responden preguntas distintas.
Las trazas muestran qué pasó en una petición real. Te dicen dónde se fue el tiempo entre tu handler, las llamadas a la base de datos, búsquedas en caché y peticiones a terceros.
Las métricas muestran la tendencia. Son la mejor herramienta para alertas porque son estables y baratas de agregar: percentiles de latencia, tasa de peticiones, tasa de errores, profundidad de colas y saturación.
Los logs son el “por qué” en texto claro: fallos de validación, entradas inesperadas, casos límite y decisiones que tomó tu código.
La verdadera ventaja es la correlación. Cuando el mismo trace ID aparece en spans y en logs estructurados, puedes saltar de un log de error a la traza exacta y ver de inmediato qué dependencia se ralentizó o qué paso falló.
Un modelo mental simple
Usa cada señal para lo que mejor hace:
- Las métricas te dicen que algo está mal.
- Las trazas muestran dónde se fue el tiempo en una petición.
- Los logs explican qué decidió tu código y por qué.
Ejemplo: tu endpoint POST /checkout empieza a hacer timeouts. Las métricas muestran que el p95 de latencia sube. Una traza muestra que la mayor parte del tiempo está dentro de una llamada al proveedor de pagos. Un log correlacionado dentro de ese span muestra reintentos por un 502, lo que te dirige a revisar la configuración de backoff o a un incidente upstream.
Antes de añadir código: nombres, muestreo y qué registrar
Un poco de planificación al principio hace que las trazas sean buscables después. Sin ella, seguirás recopilando datos, pero preguntas básicas se vuelven difíciles: “¿Fue esto en staging o en prod?” “¿Qué servicio empezó el problema?”
Empieza con identidad consistente. Elige un service.name claro para cada API Go (por ejemplo, checkout-api) y un único campo de entorno como deployment.environment=dev|staging|prod. Mantén estos valores estables. Si los nombres cambian a mitad de semana, los gráficos y búsquedas parecerán sistemas distintos.
Luego decide el muestreo. Trazar cada petición está bien en desarrollo, pero suele ser demasiado caro en producción. Un enfoque común es muestrear un pequeño porcentaje del tráfico normal y conservar las trazas de errores y peticiones lentas. Si ya sabes que ciertos endpoints tienen alto volumen (health checks, polling), trázalos menos o no los traces.
Finalmente, acuerda qué etiquetas añadirás a los spans y qué nunca recogerás. Mantén una lista corta de atributos permitidos que te ayuden a conectar eventos entre servicios y escribe reglas simples de privacidad.
Las buenas etiquetas suelen incluir IDs estables e información de la petición en términos generales (plantilla de ruta, método, código de estado). Evita totalmente datos sensibles: contraseñas, datos de pago, emails completos, tokens de autenticación y cuerpos de petición crudos. Si debes incluir valores relacionados con usuarios, aplíales hash o redacta antes de añadirlos.
Paso a paso: añade trazado OpenTelemetry a una API HTTP en Go
Configurarás un proveedor de tracer una sola vez en el arranque. Esto decide a dónde van los spans y qué atributos de recurso se adjuntan a cada span.
1) Inicializa OpenTelemetry
Asegúrate de establecer service.name. Sin él, las trazas de distintos servicios se mezclarán y los gráficos serán difíciles de leer.
// main.go (startup)
exp, _ := stdouttrace.New(stdouttrace.WithPrettyPrint())
res, _ := resource.New(context.Background(),
resource.WithAttributes(
semconv.ServiceName("checkout-api"),
),
)
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(res),
)
otel.SetTracerProvider(tp)
Esa es la base para el trazado OpenTelemetry en Go. A continuación, necesitas un span por cada petición entrante.
2) Añade middleware HTTP y captura campos clave
Usa middleware HTTP que inicie automáticamente un span y registre el código de estado y la duración. Nombra el span usando la plantilla de ruta (como /users/:id), no la URL cruda, o acabarás con miles de rutas únicas.
Apunta a una línea base limpia: un span de servidor por petición, nombres de span basados en la ruta, código HTTP capturado, fallos del handler reflejados como errores en el span y la duración visible en el visualizador de trazas.
3) Haz que los fallos sean evidentes
Cuando algo falle, devuelve un error y marca el span actual como fallido. Eso hace que la traza destaque incluso antes de mirar los logs.
En handlers, puedes hacer:
span := trace.SpanFromContext(r.Context())
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
4) Verifica los trace IDs localmente
Ejecuta la API y llama a un endpoint. Registra el trace ID desde el contexto de la petición una vez para confirmar que cambia por petición. Si siempre está vacío, tu middleware no está usando el mismo context que recibe el handler.
Llevar el contexto a través de la BD y llamadas a terceros
La visibilidad extremo a extremo se rompe en el momento en que pierdes context.Context. El contexto entrante de la petición debe ser el hilo que pasas a cada llamada a la BD, petición HTTP y helper. Si lo reemplazas con context.Background() o olvidas pasarlo, tu traza se convierte en trabajos separados e independientes.
Para HTTP saliente, usa un transporte instrumentado para que cada Do(req) se convierta en un span hijo bajo la petición actual. Reenvía las cabeceras de trazado W3C en las peticiones salientes para que los servicios downstream puedan adjuntar sus spans a la misma traza.
Las llamadas a la base de datos necesitan el mismo tratamiento. Usa un driver instrumentado o envuelve las llamadas con spans alrededor de QueryContext y ExecContext. Registra solo detalles seguros. Quieres encontrar consultas lentas sin filtrar datos.
Atributos útiles y de bajo riesgo incluyen un nombre de operación (por ejemplo, SELECT user_by_id), nombre de tabla o modelo, recuento de filas (solo el número), duración, número de reintentos y un tipo de error en términos generales (timeout, canceled, constraint).
Los timeouts son parte de la historia, no solo fallos. Establécelos con context.WithTimeout para DB y llamadas a terceros, y deja que las cancelaciones se propaguen. Cuando una llamada se cancela, marca el span como error y añade una razón breve como deadline_exceeded.
Trazar trabajos en background y colas
El trabajo en background es donde las trazas suelen detenerse. Una petición HTTP termina y luego un worker procesa un mensaje más tarde en otra máquina sin contexto compartido. Si no haces nada, obtienes dos historias: la traza de la API y una traza del job que parece empezar de la nada.
La solución es sencilla: cuando encolas un job, captura el contexto de trazado actual y guárdalo en los metadatos del job (payload, cabeceras o atributos, según la cola). Cuando el worker arranque, extrae ese contexto y empieza un nuevo span como hijo de la petición original.
Propaga el contexto de forma segura
Solo copia el contexto de trazado, no datos de usuario.
- Inyecta solo identificadores de trazado y banderas de muestreo (estilo W3C traceparent).
- Manténlo separado de los campos de negocio (por ejemplo, un campo dedicado "otel" o "trace").
- Trátalo como entrada no confiable cuando lo leas (valida el formato, maneja datos faltantes).
- Evita poner tokens, emails o cuerpos de petición en los metadatos del job.
Spans a añadir (sin convertir trazas en ruido)
Las trazas legibles suelen tener unos pocos spans significativos, no docenas de pequeños. Crea spans alrededor de los límites y puntos de espera. Un buen punto de partida es un span enqueue en el handler de la API y un span job.run en el worker.
Añade una pequeña cantidad de contexto: número de intento, nombre de la cola, tipo de job y tamaño del payload (no el contenido). Si hay reintentos, regístralos como spans o eventos separados para ver los retrasos por backoff.
Las tareas programadas también necesitan un padre. Si no hay una petición entrante, crea un span raíz nuevo para cada ejecución y etiquétalo con un nombre de schedule.
Correlacionar logs con trazas (y mantener los logs seguros)
Las trazas te dicen dónde se fue el tiempo. Los logs te dicen qué pasó y por qué. La forma más simple de conectarlos es añadir trace_id y span_id a cada entrada de log como campos estructurados.
En Go, captura el span activo desde context.Context y enriquece tu logger una vez por petición (o job). Entonces cada línea de log apunta a una traza específica.
span := trace.SpanFromContext(ctx)
sc := span.SpanContext()
logger := baseLogger.With(
"trace_id", sc.TraceID().String(),
"span_id", sc.SpanID().String(),
)
logger.Info("charge_started", "order_id", orderID)
Eso es suficiente para saltar de una entrada de log a el span exacto que se estaba ejecutando cuando ocurrió. También hace obvio cuando falta contexto: trace_id estará vacío.
Mantén los logs útiles sin filtrar PII
Los logs suelen vivir más tiempo y viajar más lejos que las trazas, así que sé más estricto. Prefiere identificadores estables y resultados: user_id, order_id, payment_provider, status y error_code. Si debes loggear entrada de usuario, redacta primero y limita longitudes.
Haz que los errores sean fáciles de agrupar
Usa nombres de evento y tipos de error consistentes para poder contarlos y buscarlos. Si el texto cambia cada vez, el mismo problema parecerá muchos distintos.
Añade métricas que realmente te ayuden a encontrar problemas
Las métricas son tu sistema de alerta temprana. En una configuración que ya usa OpenTelemetry en Go, las métricas deben responder: ¿con qué frecuencia?, ¿qué tan grave? y ¿desde cuándo?.
Empieza con un conjunto pequeño que funcione para casi cualquier API: conteo de peticiones, conteo de errores (por clase de estado), percentiles de latencia (p50, p95, p99), peticiones en vuelo y latencia de dependencias para tu BD y llamadas clave a terceros.
Para mantener las métricas alineadas con las trazas, usa las mismas plantillas de ruta y nombres. Si tus spans usan /users/{id}, tus métricas deberían también. Entonces cuando un gráfico muestre “p95 para /checkout subió”, puedes saltar directamente a las trazas filtradas por esa ruta.
Ten cuidado con las etiquetas (atributos). Una etiqueta mala puede explotar los costes y volver inútiles los dashboards. Plantilla de ruta, método, clase de estado y nombre de servicio suelen ser seguros. IDs de usuario, emails, URLs completas y mensajes de error crudos normalmente no lo son.
Añade unas pocas métricas personalizadas para eventos críticos del negocio (por ejemplo, checkout iniciado/completado, fallos de pago por grupo de códigos, jobs en background exitosos vs reintentos). Mantén el conjunto pequeño y elimina lo que nunca usas.
Exportar la telemetría y desplegar con seguridad
Exportar es donde OpenTelemetry se hace real. Tu servicio tiene que enviar spans, métricas y logs a un destino fiable sin ralentizar las peticiones.
Para desarrollo local, manténlo simple. Un exporter a consola (o OTLP a un collector local) te permite ver trazas rápidamente y validar nombres de spans y atributos. En producción, prefiere OTLP a un agente o al OpenTelemetry Collector cerca del servicio. Te da un único punto para manejar reintentos, enrutamiento y filtrado.
El batching importa. Envía la telemetría en lotes con un intervalo corto, con timeouts estrictos para que una red atascada no bloquee tu app. La telemetría no debe estar en el camino crítico. Si el exporter no puede seguir el ritmo, debe descartar datos en lugar de consumir memoria.
El muestreo mantiene los costes predecibles. Empieza con muestreo head-based (por ejemplo, 1-10% de peticiones), luego añade reglas simples: siempre muestrea errores y siempre muestrea peticiones lentas por encima de un umbral. Si tienes jobs en background de alto volumen, muestrea esos a tasas más bajas.
Despliega en pasos pequeños: desarrollo con 100% de muestreo, staging con tráfico realista y muestreo menor, y producción con muestreo conservador y alertas por fallos del exporter.
Errores comunes que arruinan la visibilidad extremo a extremo
La visibilidad extremo a extremo falla casi siempre por razones sencillas: los datos existen, pero no se conectan.
Los problemas que rompen el trazado distribuido en Go suelen ser estos:
- Perder el contexto entre capas. Un handler crea un span, pero una llamada a BD, cliente HTTP o goroutine usa
context.Background()en vez del contexto de la petición. - Devolver errores sin marcar spans. Si no registras el error y no pones el estado del span, las trazas parecen “verdes” incluso cuando los usuarios ven 500s.
- Instrumentarlo todo. Si cada helper se convierte en un span, las trazas se vuelven ruido y cuestan más.
- Añadir atributos de alta cardinalidad. URLs completas con IDs, emails, valores SQL crudos, cuerpos de petición o mensajes de error crudos pueden crear millones de valores únicos.
- Juzgar el rendimiento por promedios. Los incidentes aparecen en percentiles (p95/p99) y en la tasa de errores, no en la latencia media.
Una comprobación rápida es escoger una petición real y seguirla a través de los límites. Si no puedes ver un trace ID fluyendo por la petición entrante, la consulta a BD, la llamada a terceros y el worker asíncrono, aún no tienes visibilidad extremo a extremo.
Una lista práctica de “hecho”
Estás cerca cuando puedes ir desde el informe de un usuario hasta la petición exacta y seguirla por cada salto.
- Elige una línea de log de la API y localiza la traza exacta por
trace_id. Confirma que logs más profundos de la misma petición (BD, cliente HTTP, worker) llevan el mismo contexto de traza. - Abre la traza y verifica el anidamiento: un span de servidor HTTP arriba, con spans hijos para consultas BD y APIs de terceros. Una lista plana suele significar que se perdió contexto.
- Dispara un job en background desde una petición API (por ejemplo, enviar un recibo por email) y confirma que el span del worker se conecta de vuelta a la petición.
- Revisa métricas básicas: conteo de peticiones, tasa de errores y percentiles de latencia. Confirma que puedes filtrar por ruta u operación.
- Escanea atributos y logs por seguridad: no debe haber contraseñas, tokens, números completos de tarjeta de crédito ni datos personales crudos.
Una prueba simple es simular un checkout lento donde el proveedor de pagos se retrasa. Debes ver una traza con un span externo claramente etiquetado y un pico en la métrica p95 de latencia para la ruta de checkout.
Si generas backends Go (por ejemplo, con AppMaster), ayuda convertir esta lista en parte de tu rutina de despliegue para que nuevos endpoints y workers sigan siendo trazables a medida que la app crece. AppMaster genera servicios Go reales, así puedes estandarizar una única configuración de OpenTelemetry y mantenerla en servicios y trabajos en background.
Ejemplo: depurar un checkout lento entre servicios
Un cliente dice: “El checkout se queda colgado a veces.” No puedes reproducirlo a demanda, y ahí es donde el trazado OpenTelemetry en Go resulta útil.
Empieza con las métricas para entender la forma del problema. Mira la tasa de peticiones, la tasa de errores y el p95 o p99 de latencia para el endpoint de checkout. Si la ralentización ocurre en ráfagas cortas y solo en una porción de peticiones, suele apuntar a una dependencia, colas o comportamiento de reintentos más que a CPU.
Después, abre una traza lenta del mismo intervalo. Una traza suele ser suficiente. Un checkout sano puede estar entre 300 y 600 ms end-to-end. Uno malo puede estar entre 8 y 12 segundos, con la mayor parte del tiempo en un solo span.
Un patrón común es: el handler API es rápido, el trabajo de BD está mayormente bien, luego un span del proveedor de pagos muestra reintentos con backoff y una llamada downstream espera detrás de un lock o cola. La respuesta puede incluso devolver 200, así que las alertas basadas solo en errores nunca saltan.
Los logs correlacionados te dicen entonces el camino exacto en texto claro: “retrying Stripe charge: timeout”, seguido de “db tx aborted: serialization failure”, seguido de “retry checkout flow”. Eso indica claramente que varios problemas pequeños se combinan en una mala experiencia de usuario.
Una vez encuentres el cuello de botella, la consistencia es lo que mantiene todo legible con el tiempo. Estandariza nombres de spans, atributos (hash de user ID seguro, order ID, nombre de dependencia) y reglas de muestreo entre servicios para que todos lean las trazas de la misma manera.


