Logré que OpenTelemetry funcionara, pero ¿por qué fue tan complicado?
(iconsolutions.com)- La solicitud de un cliente para soporte de OpenTelemetry dejó en evidencia que la observabilidad existente basada en Prometheus, Jaeger y OpenTracing no era exactamente igual a los requisitos de métricas, logs y trazas de OTel
- Los logs y las métricas se migraron con relativa facilidad agregando un appender y configuración, pero el tracing distribuido fue mucho más difícil porque Spring y Akka debían compartir el contexto de tracing dentro de la misma JVM
- OTel y Lightbend Telemetry/OpenTracing usan distintas API de Tracer e implementaciones de SpanContext, por lo que una misma transacción se dividía en trazas separadas
- La solución fue injectar el contexto de OTel en un
Mapde Java, extraerlo como Jaeger SpanContext y luego activarlo manualmente conGlobalExtendedTracer.get().local().activateContext()de Lightbend - OTel ayuda a estandarizar la observabilidad, pero al mezclarlo con instrumentación existente basada en Akka y Jaeger puede hacer falta una conexión explícita de contexto
Las señales de observabilidad que OTel intenta unificar
- OpenTelemetry (OTel) es un framework y toolkit de observabilidad que busca estandarizar áreas de herramientas que antes estaban separadas en tres señales: métricas, logs y trazas
- La combinación tradicional estaba dividida por rol
- Prometheus: métricas
- Agregadores centrales como Logstash y Elasticsearch: logs
- OpenTracing: tracing distribuido
- OTel no solo ofrece especificaciones, sino también componentes centrales
- OpenTelemetry Protocol (OTLP): protocolo con el que las aplicaciones reportan datos de telemetry
- OpenTelemetry Collector: componente neutral frente a proveedores que recibe, procesa y exporta datos de telemetry
- SDK para más de 10 lenguajes: implementación de OTLP y exportación de telemetry
- Las aplicaciones envían señales al OTel Collector mediante instrumentación automática, API y SDK de lenguaje, y la infraestructura también puede enviar señales al Collector
El soporte existente de IPF y la solicitud del cliente
- IPF ya contaba con integración con ELK Stack, monitoreo y documentación de observabilidad, y los clientes también lo venían usando en entornos de producción
- Por las características del framework, no se puede saber en qué entorno se desplegarán las aplicaciones que usan la librería, así que era necesario no imponer un proveedor específico y ofrecer abstracciones y valores predeterminados redefinibles
- La recomendación predeterminada era enviar logs a agregadores como Elasticsearch o LogScale, y habilitar y dar soporte a Prometheus, que se convirtió prácticamente en el estándar para métricas
- Los clientes hicieron casi al mismo tiempo tres solicitudes de tracing basado en OTel, y al usar OTel para tracing surgió el flujo de migrar también logs y métricas
- Los logs y las métricas se resolvieron con relativa facilidad agregando un nuevo appender y configuración, pero el tracing quedó como un problema aparte
Por qué hace falta propagar contexto en tracing distribuido
- Para trazar una sola transacción en un sistema distribuido, hay que pasar entre sistemas información que conecte una llamada específica con la transacción; a esto se le llama propagación de contexto
- Una traza es una estructura padre que envuelve varias unidades de trabajo llamadas spans; puede haber varios spans y pueden estar anidados
- En un sitio de comercio electrónico, el flujo de presionar el botón “buy now” puede agruparse en una sola traza
- Un span en el que el frontend se comunica con el backend
- Spans hijos en los que el backend se comunica con servicios de pagos, envíos y gestión de pedidos
- Spans hijos adicionales en los que cada servicio downstream vuelve a comunicarse con otros sistemas
- Al usar OTel, cada servicio distribuido reporta su parte de la traza al OTel Collector, y OTel compone todo el flujo bajo un único trace ID
Cuatro estándares de propagación de contexto y la huella de OpenTracing
- Hay cuatro implementaciones de propagación de contexto que OTel debe soportar obligatoriamente
- El área de Akka en IPF ya soportaba OpenTracing, que está destinado a quedar obsoleto, y usaba el soporte de OpenTracing de Lightbend Telemetry
- Lightbend Telemetry 2.20.0 agregó soporte para logs/events y métricas de OpenTelemetry, pero faltaba el importante soporte de tracing
- En teoría, el tracing de OTel se parece a un rebranding de OpenTracing, y OpenTracing de Lightbend Telemetry también soporta los cuatro métodos de propagación, así que parecía que iba a funcionar, pero el resultado real fue distinto
La traza que se cortaba entre Spring y Akka
- IPF usa Spring y Akka en conjunto
- Spring Boot y Spring IoC: bootstrap de la aplicación, configuración y composición de dependencias
- Akka: event sourcing, scheduling, clustering, sharding, integración, etc.
- Un cliente puede iniciar un flujo de pagos desde un Spring REST controller o desde un método marcado con
@KafkaListener - Antes de OTel, las tres áreas funcionaban sin grandes conflictos
- Metrics: Spring y Akka exponían endpoints de Prometheus distintos
- Logs: ambos frameworks usaban SLF4J y Logback
- Traces: ambos usaban OpenTracing
- Al usar OTel, las métricas y los logs se transmitían correctamente porque Spring y Akka funcionaban de manera independiente, pero el tracing debía compartir el mismo trace context dentro de la JVM
- El problema real era que dentro de la aplicación el mismo concepto de tracing estaba representado por dos API distintas, y esas dos API no se comunicaban entre sí
- El comportamiento correcto habría sido que el cliente HTTP de Akka reutilizara el trace ID existente
123y solo generara un nuevo span ID para mostrarlo como una nueva unidad de trabajo de la misma traza; en la práctica, se creaban dos trazas sin relación
opentracing-shim y conflictos de implementación
- Del lado de OTel existe
opentracing-shim, que hace que elTracerde OTel parezca unTracerde OpenTracing - Sin embargo, Lightbend Telemetry usaba una implementación custom de Tracer, y por eso tanto el shim como Jaeger fallaban
- En los logs se mostraban los siguientes errores
Expected to have an OpenTelemetry Span but got cinnamon.opentracing.TraceLocal$ContextOnlySpanExpected to have a JaegerSpanContext but got io.opentelemetry.opentracingshim.SpanContextShim
- Jaeger fallaba al recibir el contexto del shim de OTel, y OTel fallaba al recibir el contexto de Lightbend
Investigación del Java Agent y del tracing interno de Lightbend
- Tanto la instrumentación de OTel como la de Lightbend Telemetry usan Java Agent para enganchar llamadas a métodos específicos de clases específicas y reportar actividad al tracer
- La instrumentación de Lightbend Telemetry no es open source, así que para entender cómo funcionaba fue necesario investigar el código decompilado del tracer
- Se creó un ejemplo mínimo reproducible para identificar en qué punto se cortaba el paso del trace context de OTel del lado de Spring hacia Akka
- El punto donde fallaba Jaeger estaba en este código de
JaegerTracer.java - El llamador era
OpenTracingAkkaPersistenceActorInstrumentation.class, una clase de instrumentación propietaria de Lightbend Telemetry - En el código decompilado, la condición clave era el estado de
var5- Si
var5, el OpenTracingSpanContext, no era null, el nuevo span se adjuntaba como hijo del span activo actual - Si
var5era null, se convertía en un span sin relación con la traza existente - Si
var5no era un Jaeger SpanContext, Jaeger fallaba
- Si
Conectar OTel y OpenTracing con una conversión manual
- El
SpanContextse obtenía desdethis.traceLocal.currentContext(), ytraceLocalse inicializaba desde elExtendedTracerde Lightbend - En la documentación de Lightbend había una forma de acceder globalmente a
ExtendedTracerGlobalExtendedTracer.get()
GlobalExtendedTracer.get()devuelve unExtendedTracer, ylocal()devuelve el mismoTraceLocalque usa la librería de instrumentación- Ese
TraceLocaltenía un métodoactivateContextque recibe unSpanContextde OpenTracing - El flujo de la solución tuvo cuatro pasos
- No usar el shim de OTel
- Injectar el Context actual de OpenTelemetry en un
Mapde Java - Usar los valores de ese
Mappara extraer un Jaeger SpanContext - Activar el Jaeger SpanContext en el
TraceLocalde Lightbend antes de entrar a Akka
- La operación usada de la API de propagators de OTel fue inject and extract operations
- El código clave sigue este flujo
- Crear un
HashMapvacío - Inyectar el contexto de OTel en el map con
GlobalOpenTelemetry.get().getPropagators().getTextMapPropagator().inject(...) - Crear un JaegerSpanContext con
new TextMapCodec(false).extract(new TextMapAdapter(otelContext)) - Activar el context antes de entrar a Akka con
GlobalExtendedTracer.get().local().activateContext(openTracingContext)
- Crear un
Comportamiento confirmado y recomendación operativa
- Después de la conexión manual, la traza continuó como se esperaba
- Todo el flujo, que empezaba desde la llamada REST de Spring
/submit, quedó conectado como una única traza - Se podían mezclar partes instrumentadas con la API de OTel y partes instrumentadas con la API de OpenTracing
- La traza se propagaba más allá del límite HTTP
- Todo el flujo, que empezaba desde la llamada REST de Spring
- En el ejemplo se usaron nombres distintos para diferenciar los dos tipos de instrumentación, pero en entornos reales de clientes se recomienda alinear
otel.service.nameycinnamon.applicationpara que desde afuera se vean como una sola aplicación
Por qué fue complicado y preocupaciones restantes
- Reescribir Lightbend Telemetry con la API de OTel podría ser un trabajo grande, porque habría que portar a la API de OTel mucha instrumentación que hoy está atada a la API de Jaeger
- Como el OTel Collector soporta la recolección de trazas en el formato legacy de Zipkin, Lightbend puede apoyarse en ese soporte legacy para ofrecer soporte completo de OTel
- La causa directa de la complejidad fue intentar combinar dos sistemas de instrumentación que usan librerías de tracing distintas
- El proyecto OTel intenta ordenar el área de observabilidad mediante esfuerzos de estandarización como las semantic conventions, y se lo evalúa como un proyecto FOSS útil, aunque algo complejo de entender al principio
- Todavía queda la preocupación de si Akka propaga correctamente el trace context entre threads dentro del actor model
- En pruebas de carga pequeñas funcionó como se esperaba
- Se dejó abierto un ticket relacionado del lado de Akka
1 comentarios
Opiniones de Hacker News
Durante todo el tiempo que estuve aprendiendo y portando Otel, sentí que había vuelto al mundo de Java. Cada vez que seguía el código línea por línea parecía EnterpriseFizzBuzz, no había ninguna posibilidad de descubrimiento, y hasta su terminología propia parecía hecha por gente bajo los efectos de algo.
En NodeJS, el uso de CPU era unas 4 veces mayor que con StatsD, así que al final hice mi propia agregación para bajar el consumo y también reducir la explosión de etiquetas. StatsD no tiene problema si varios procesos reportan las mismas etiquetas, pero OTEL las sobrescribe.
En carga pico, un CPU corría con 60~80% de uso, y antes de que algo cambiara ni siquiera podíamos escalar verticalmente. Para lenguajes que usan un proceso por core, OTEL parece activamente hostil, casi un chiste. Es mejor simplemente usar Prometheus y, en la práctica, tampoco hay otro competidor.
Intenté hacer una configuración simple de otel en .NET y, después de leer durante horas la documentación del proveedor que había elegido la organización sin entenderla, entré al Discord que administraba un colega. Parte de su modelo de negocio era “paga si quieres usar otel bien en un producto open source”, y de inmediato sentí que valía la pena sin importar el costo.
Antes que implementar OTEL, preferiría crear otra librería estable de eventos/pub-sub sin experiencia previa.
La configuración mínima en realidad también es bastante pequeña: alcanza con una UI web, una instancia de servidor y una DB que ya conozcas: https://skywalking.apache.org/docs/main/latest/en/setup/back...
Tiene algo parecido a Zabbix en el área de monitoreo. Ninguno de los dos va a impresionar a nadie, pero son suficientemente prácticos.
process.vpid. Se puede coordinar con algo como un objeto global para que ese valor sea único mientras la app esté viva.Luego basta con tener algo que lo sume y elimine ese atributo. Con statsd/delta, si se pierde un envío de señal se distorsionan todos los datos, pero con el enfoque acumulativo solo se pierde precisión.
Mi caso de uso no son procesos de larga duración que se puedan scrapear, sino métricas push-based provenientes de herramientas “batch”.
La razón por la que Otel parece complejo es que varios proveedores de observabilidad hicieron que implementar observabilidad fuera muy fácil con sus SDK, agentes y API propietarios. Otel intenta resolver ese problema, y creo que quienes lo hicieron lo están logrando muy bien.
También es digno de elogio que Grafana haya adoptado OpenTelemetry como un componente de primera clase del ecosistema.
Durante años impulsé el uso de Datadog, pero para una escala entre empresa mediana y grande el precio se volvió difícil de sostener. Con el tiempo, a medida que las API y SDK de OpenTelemetry se estabilizaron, lo adoptamos como estándar de observabilidad de aplicaciones.
Dicho eso, la documentación en general podría mejorar, y no es ideal que la documentación de onboarding sea distinta según el lenguaje.
Actualmente mi equipo usa un stack NodeJS/Typescript, e hicimos un paquete y un stack Grafana de ejemplo para poder empezar rápido con OpenTelemetry: https://github.com/zonneplan/open-telemetry-js
El mayor problema con todos los proveedores de APM es que, en el momento en que un agente mágico empieza a tener hooks del kernel, comienzan a pasar todo tipo de cosas que los desarrolladores no pueden explicar.
En una empresa anterior implementamos Dynatrace y, por suerte, la app ya tenía suficientes métricas integradas como para que el SRE líder la considerara un “modelo” de instrumentación. Pero apenas instalamos el agente de Dynatrace en el host de la app, aparecieron varios heisenbugs que requerían reiniciar nodos, y también medimos directamente una degradación de rendimiento.
Irónicamente, gracias a las métricas evitamos el sufrimiento, pero nadie sabía cómo arreglarlo. Lo peor fue un caso en que, durante una actualización de MSSQL, el failover contaminaba de forma extraña el pool de conexiones de ADO.NET.
Es una estructura que se vuelve compleja solo en la medida necesaria. Se pueden evitar las funciones mágicas y usar solo el subconjunto que, según el contexto, aporte más valor y sea más fácil de entender
En nuestro equipo es muy simple. Usamos una biblioteca que solo envía trazas, y las trazas son lo que más valor aporta para observar la aplicación, además de que pueden contener otros tipos de datos. Básicamente, es como usar un hashmap en lugar de cadenas y números de punto flotante
Usamos instrumentación manual en vez de instrumentación automática, decidimos intencionalmente qué observar y entendemos bien qué código emite spans. También tenemos convenciones de nombres que encajan con la estructura del código
Para el backend usamos un servicio barato de terceros junto con una instalación all-in-one de Jaeger para desarrollo local. Esta última solo requiere ejecutar un binario o un contenedor Docker, y no guarda los spans en disco. Principalmente sirve para tranquilizar al equipo y que no sientan que van a saturar el servicio de terceros
Para monitoreo de infraestructura ya tenemos una configuración existente y, en nuestro caso, no vemos mucho valor en recopilar todos los logs y métricas de infraestructura. Considero que las métricas y logs de OTEL todavía están en una etapa temprana, aunque los proveedores no lo digan así
Todavía estoy buscando un endpoint para enviar métricas simples y puntuales desde algunas partes de la infraestructura que aún no se pueden scrapear
Otel puede estar bien para un proyecto nuevo, pero activarlo en un servicio en producción que ya tiene telemetría se sintió como cambiarle las llantas a un auto en marcha
Creo que podría ser bueno en un proyecto greenfield que use solo otel y no tenga frameworks no-otel. Pero yo todavía no vivo en ese mundo
Uno de los mayores problemas fue la experiencia de desarrollo local. Quería soportar logs, trazas y métricas en local, pero no quería levantar un montón de imágenes Docker para eso. Quería ver en los logs cómo se veían las métricas, trazas, baggage y activity spans antes de desplegar
Hace poco el equipo de .NET lanzó .NET Aspire y está muy bueno. En un stack de desarrollo local facilita visualizar todo en un solo lugar, y funciona como un orquestador en código
Al desplegar en k8s, apuntas el endpoint de OTEL al DataDog Agent y todo simplemente funciona. Evitamos la biblioteca y el SDK de trazas personalizadas de DataDog y usamos solo OTEL
Ahora la experiencia de desarrollo quedó bastante buena
https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals...
https://docs.datadoghq.com/opentelemetry/#overview
Configurarlo por primera vez en una máquina local de desarrollo toma 5 minutos, y después basta con ejecutar
/path/to/openobserveen una pestaña de terminal aparte. Si quieres evitar la enorme complejidad de un binario único enlazado estáticamente, también ofrecen imágenes Docker para ejecución local y remota :PEs un backend OpenTelemetry todo en uno con gráficas bonitas, y en mis proyectos todavía no ha fallado de ninguna forma detectable
Si usas Aspire, agregas complejidad innecesaria al nivel de la app y quedas atado a un ecosistema estrecho. Para desarrollo local hay muchas alternativas probadas como docker compose, y Aspire tampoco es mucho más fácil que docker compose y variables de entorno
Si vas a hacer otel en Python, conviene usar el cliente de Logfire. Incluso si no usas el servicio de Logfire
Es open source y puede enviar a cualquier endpoint compatible con otel. Además, el cliente hecho por el equipo de pydantic es 10 veces mejor y más simple que la biblioteca oficial de otel
También hay una entrevista interesante donde Samuel Colvin explica cómo llegó hasta esto: https://www.bitecode.dev/p/samuel-colvin-on-logfire-mixing-p...
Por eso inicié un proyecto open source enfocado en hacer que adoptar OpenTelemetry sea tan fácil como un solo comando: https://github.com/odigos-io/odigos
Hoy en día, muchos frameworks web se encargan de la mayor parte de la instrumentación. Por ejemplo, si usas opentelemetry-js y alojas por tu cuenta algo como https://signoz.io, puedes tenerlo funcionando en menos de una hora y obtener muchos datos sin escribir código personalizado
OpenTelemetry creció a partir de los traces, pero es mucho mejor dejar las métricas y los logs a soluciones especializadas
Se siente como el problema de una “abstracción con fugas” o un “framework con fugas”. Si quieres meter todo bajo un mismo paraguas, una base de datos SQL también puede hacer todo eso a la vez. Eso no significa que debas hacerlo
https://cra.mr/the-problem-with-otel/
Aun así, cada vez que trato con OTel, sigue sin gustarme
Si lees hasta el final, te das cuenta de que la mayor parte del dolor se lo causaron ellos mismos. En el stack estándar de Python (mysql, flask, redis, requests, etc.) fue muy fácil. Con solo poner unos cuantos imports al inicio del servicio, todo se conectaba automáticamente y rastreaba todo sin mayor alboroto
aiohttpva por la versión 3.11.X, y aunque la instrumentación automática dice que soporta3.X[0], en la práctica el resultado depende de qué tan reciente sea elaiohttpque uses y del estado de la instrumentación automáticaCuando todo encaja, funciona como magia, pero ese margen es bastante estrecho
[0]: https://github.com/open-telemetry/opentelemetry-python-contr...
Los traces funcionan y también configuré el spanmetrics exporter; si consulto prometheus directamente, veo las spanmetrics. Pero haga lo que haga, no aparecen en la pestaña “monitor” de jaeger
Después de gastar 3 días en esto, mi jefe dijo: “¿y si simplemente hacemos instrumentación manual y mandamos todo a SQL Server, y armamos un dashboard en Grafana?”. Pero tampoco quiero hacer eso
Ni siquiera funciona para el caso de uso más simple. ¿Tengo que agregar Grafana encima?
Es complejo porque no fue diseñado para los ingenieros que lo implementan, sino para las empresas que venden software compatible con Otel
Más bien, creo que los backends tardaron bastante en adoptarlo
Los significados cambian, así que está bien, pero me parece curioso que ahora ya no quede una palabra para referirse a escribir software conforme a una especificación, y solo quede “desplegar software existente en un servidor”