Trazabilidad de Extremo a Extremo con OpenTelemetry
August 31, 2022
Ya sea que implementes o no microservicios (y probablemente no deberías), tu sistema está compuesto muy probablemente por múltiples componentes. El sistema más sencillo probablemente esté formado por un proxy inverso, una aplicación y una base de datos. En este caso, el monitoreo no solo es una buena idea; es un requisito. Cuanto mayor sea el número de componentes por los que puede fluir una solicitud, más fuerte será el requisito.
Sin embargo, el monitoreo es solo el comienzo del viaje. Cuando las solicitudes comienzan a fallar en masa, necesitas una vista agregada en todos los componentes. Esto se llama tracing, y es uno de los pilares de la observabilidad; los otros dos son métricas y logs.
En esta publicación, me centraré únicamente en los traces y describiré cómo puedes comenzar tu viaje hacia la observabilidad.
La especificación W3C Trace Context
Una solución de tracing debe proporcionar un formato estándar para trabajar en pilas tecnológicas heterogéneas. Dicho formato debe adherirse a una especificación, ya sea formal o de facto.
Es importante entender que una especificación rara vez aparece de la nada. En general, el mercado ya tiene un par de implementaciones distintas. La mayoría de las veces, una nueva especificación conduce a una implementación adicional, como describe el famoso cómic de XKCD:
Sin embargo, a veces ocurre un milagro: el mercado se adhiere a la nueva especificación. Aquí, Trace Context es una especificación del W3C, y parece haber logrado el objetivo:
Esta especificación define encabezados HTTP estándar y un formato de valor para propagar información de contexto que permite escenarios de tracing distribuido. La especificación estandariza cómo se envía y modifica la información de contexto entre servicios. La información de contexto identifica de manera única solicitudes individuales en un sistema distribuido y también define un medio para agregar y propagar información de contexto específica del proveedor. -- https://www.w3.org/TR/trace-context/
Dos conceptos críticos emergen del documento:
- Un trace sigue el camino de una solicitud que abarca múltiples componentes
- Un span está vinculado a un solo componente y se relaciona con otro span mediante una relación padre-hijo

En el momento de escribir esto, la especificación es una recomendación del W3C, que es la etapa final.
Trace Context ya tiene muchas implementaciones. Una de ellas es OpenTelemetry.
OpenTelemetry como el estándar de oro
Cuanto más cerca estés de la parte operativa de TI, mayores serán las posibilidades de que hayas oído hablar de OpenTelemetry:
OpenTelemetry es una colección de herramientas, APIs y SDKs. Úsalo para instrumentar, generar, recopilar y exportar datos de telemetría (métricas, logs y traces) para ayudarte a analizar el rendimiento y comportamiento de tu software. OpenTelemetry está generalmente disponible en varios lenguajes y es adecuado para su uso. -- https://opentelemetry.io/
OpenTelemetry es un proyecto gestionado por la CNCF. Antes de OpenTelemetry existían dos proyectos:
- OpenTracing, enfocado en traces como su nombre lo indica
- OpenCensus, cuyo objetivo era gestionar métricas y traces
Ambos proyectos se fusionaron y agregaron logs. OpenTelemetry ahora ofrece un conjunto de "capas" enfocadas en la observabilidad:
- APIs de instrumentación en varios lenguajes
- Implementaciones canónicas, nuevamente en diferentes lenguajes
- Componentes de infraestructura como recolectores
- Formatos de interoperabilidad, como el Trace Context del W3C
Nota que, aunque OpenTelemetry es una implementación de Trace Context, hace más. Trace Context se limita a HTTP, mientras que OpenTelemetry permite que los spans crucen componentes no web, como Kafka. Esto está fuera del alcance de esta publicación.
El caso de uso
Mi caso de uso favorito es una tienda de comercio electrónico, así que no lo cambiemos. En este caso, la tienda está diseñada alrededor de microservicios, cada uno accesible a través de una API REST y protegido detrás de un API Gateway. Para simplificar la arquitectura para esta publicación, usaré solo dos microservicios: catalog
gestiona los productos, y pricing
maneja el precio de los productos.
Cuando un usuario llega a la aplicación, la página de inicio obtiene todos los productos, obtiene sus respectivos precios y los muestra.
Para hacer las cosas más interesantes, catalog
es una aplicación Spring Boot codificada en Kotlin, mientras que pricing
es una aplicación Flask en Python.
El tracing debería permitirnos seguir el camino de una solicitud a través del gateway, ambos microservicios y, si es posible, las bases de datos.
Traces en el gateway
El punto de entrada es la parte más emocionante del tracing, ya que debería generar el ID del trace: en este caso, el punto de entrada es el gateway. Usaré Apache APISIX para implementar la demo:
Apache APISIX proporciona ricas características de gestión de tráfico como Balanceo de Carga, Upstream Dinámico, Lanzamiento Canario, Circuit Breaking, Autenticación, Observabilidad, etc. -- https://apisix.apache.org/
Apache APISIX se basa en una arquitectura de plugins y ofrece un plugin de OpenTelemetry:
El plugin
opentelemetry
se puede usar para reportar datos de tracing según la especificación de OpenTelemetry. El plugin solo admite OLTP codificado en binario sobre HTTP. -- https://apisix.apache.org/docs/apisix/plugins/opentelemetry/
Configuremos el plugin opentelemetry
:
apisix:
enable_admin: false #1
config_center: yaml #1
plugins:
- opentelemetry #2
plugin_attr:
opentelemetry:
resource:
service.name: APISIX #3
collector:
address: jaeger:4318 #4
- Ejecuta Apache APISIX en modo independiente para facilitar la demo. Es una buena práctica en producción de todos modos.
- Configura
opentelemetry
como un plugin global. - Establece el nombre del servicio. Es el nombre que aparecerá en el componente de visualización de traces.
- Envía los traces al servicio
jaeger
. La siguiente sección lo describirá.
Queremos rastrear cada ruta, por lo que en lugar de agregar el plugin a cada ruta, debemos configurar el plugin como global:
global_rules:
- id: 1
plugins:
opentelemetry:
sampler:
name: always_on #1
- El tracing tiene un impacto en el rendimiento. Cuanto más rastreamos, más impactamos. Por lo tanto, debemos equilibrar cuidadosamente el impacto en el rendimiento frente a los beneficios de la observabilidad. Sin embargo, para la demo, queremos rastrear cada solicitud.
Recolección, almacenamiento y visualización de traces
Aunque Trace Context es una especificación del W3C y OpenTelemetry es un estándar de facto, existen muchas soluciones en el mercado para recopilar, almacenar y visualizar traces. Cada solución puede proporcionar las tres capacidades o solo parte de ellas. Por ejemplo, la pila Elastic maneja el almacenamiento y la visualización, pero debes confiar en otra cosa para la recolección. Por otro lado, Jaeger y Zipkin proporcionan una suite completa para cumplir con las tres capacidades.
Jaeger y Zipkin son anteriores a OpenTelemetry, por lo que cada uno tiene su formato de transporte de traces. Sin embargo, ambos proporcionan integración con el formato de OpenTelemetry.
En el alcance de esta publicación, la solución exacta no es relevante, ya que solo necesitamos las capacidades. Elegí Jaeger porque proporciona una imagen Docker todo en uno: cada capacidad tiene su componente, pero todos están integrados en la misma imagen, lo que facilita mucho la configuración.
Los puertos relevantes de la imagen son los siguientes:
Puerto | Protocolo | Componente | Función |
---|---|---|---|
16686 | HTTP | query | sirve el frontend |
4317 | HTTP | collector | acepta OpenTelemetry Protocol (OTLP) sobre gRPC, si está habilitado |
4318 | HTTP | collector | acepta OpenTelemetry Protocol (OTLP) sobre HTTP, si está habilitado |
La parte de Docker Compose se ve así:
services:
jaeger:
image: jaegertracing/all-in-one:1.37 #1
environment:
- COLLECTOR_OTLP_ENABLED=true #2
ports:
- "16686:16686" #3
- Usa la imagen
all-in-one
. - Muy importante: habilita el colector en formato OpenTelemetry.
- Expone el puerto de la interfaz de usuario.
Ahora que hemos configurado la infraestructura, podemos centrarnos en habilitar los traces en nuestras aplicaciones.
Traces en aplicaciones Flask
El servicio pricing
es una aplicación simple de Flask. Ofrece un solo endpoint para obtener el precio de un solo producto desde la base de datos.
@app.route('/price/<product_str>') #1-2
def price(product_str: str) -> Dict[str, object]:
product_id = int(product_str)
price: Price = Price.query.get(product_id) #3
if price is None:
return jsonify({'error': 'Product not found'}), 404
else:
low: float = price.value - price.jitter #4
high: float = price.value + price.jitter #4
return {
'product_id': product_id,
'price': round(uniform(low, high), 2) #4
}
- Endpoint
- La ruta requiere el ID del producto.
- Obtiene datos de la base de datos usando SQLAlchemy.
- Los motores de precios reales nunca devuelven el mismo precio con el tiempo; aleatoricemos un poco el precio por diversión.
Advertencia: Obtener un solo precio por llamada es altamente ineficiente. Requiere tantas llamadas como productos, pero hace que el trace sea más interesante. En la vida real, la ruta debería poder aceptar múltiples IDs de productos y obtener todos los precios asociados en una sola solicitud-respuesta.
Ahora es el momento de instrumentar la aplicación. Hay dos opciones disponibles: instrumentación automática e instrumentación manual. La automática es de bajo esfuerzo y una victoria rápida; la manual requiere tiempo de desarrollo enfocado. Recomendaría comenzar con la automática y solo agregar la manual si es necesario.
Necesitamos agregar un par de paquetes de Python:
opentelemetry-distro[otlp]==0.33b0
opentelemetry-instrumentation
opentelemetry-instrumentation-flask
Necesitamos configurar un par de parámetros:
pricing:
build: ./pricing
environment:
OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317 #1
OTEL_RESOURCE_ATTRIBUTES: service.name=pricing #2
OTEL_METRICS_EXPORTER: none #3
OTEL_LOGS_EXPORTER: none #3
- Envía los traces a Jaeger.
- Establece el nombre del servicio. Es el nombre que aparecerá en el componente de visualización de traces.
- No estamos interesados ni en logs ni en métricas.
Ahora, en lugar de usar el comando estándar flask run
, lo envolvemos:
opentelemetry-instrument flask run
Solo con esto, ya recopilamos spans de llamadas a métodos y rutas de Flask.
Podemos agregar manualmente spans adicionales si es necesario, por ejemplo:
from opentelemetry import trace
@app.route('/price/<product_str>')
def price(product_str: str) -> Dict[str, object]:
product_id = int(product_str)
with tracer.start_as_current_span("SELECT * FROM PRICE WHERE ID=:id", attributes={":id": product_id}) as span: #1
price: Price = Price.query.get(product_id)
# ...
- Agrega un span adicional con la etiqueta y el atributo configurados.
Traces en aplicaciones Spring Boot
El servicio catalog
es una aplicación Reactiva de Spring Boot desarrollada en Kotlin. Ofrece dos endpoints:
- Uno para obtener un solo producto.
- Otro para obtener todos los productos.
Ambos primero buscan en la base de datos de productos y luego consultan el servicio pricing
anterior para obtener el precio.
Al igual que en Python, podemos aprovechar la instrumentación automática y manual. Comencemos con lo más fácil, la instrumentación automática. En la JVM, lo logramos a través de un agente:
java -javaagent:opentelemetry-javaagent.jar -jar catalog.jar
Al igual que en Python, crea spans para cada llamada a métodos y punto de entrada HTTP. También instrumenta llamadas JDBC, pero tenemos una pila Reactiva y, por lo tanto, usamos R2DBC. Para el registro, hay un problema en GitHub abierto para agregar soporte.
Necesitamos configurar el comportamiento predeterminado:
catalog:
build: ./catalog
environment:
APP_PRICING_ENDPOINT: http://pricing:5000/price
OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317 #1
OTEL_RESOURCE_ATTRIBUTES: service.name=orders #2
OTEL_METRICS_EXPORTER: none #3
OTEL_LOGS_EXPORTER: none #3
- Envía los traces a Jaeger.
- Establece el nombre del servicio. Es el nombre que aparecerá en el componente de visualización de traces.
- No estamos interesados ni en logs ni en métricas.
Al igual que en Python, podemos mejorar el juego agregando instrumentación manual. Hay dos opciones disponibles, programática y basada en anotaciones. La primera es un poco complicada a menos que introduzcamos Spring Cloud Sleuth. Agreguemos anotaciones.
Necesitamos una dependencia adicional:
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-instrumentation-annotations</artifactId>
<version>1.17.0-alpha</version>
</dependency>
Ten cuidado, el artefacto fue recientemente reubicado desde io.opentelemetry:opentelemetry-extension-annotations
.
Ahora podemos anotar nuestro código:
@WithSpan("ProductHandler.fetch") //1
suspend fun fetch(@SpanAttribute("id") id: Long): Result<Product> { //2
val product = repository.findById(id)
return if (product == null) Result.failure(IllegalArgumentException("Product $id not found"))
else Result.success(product)
}
- Agrega un span adicional con la etiqueta configurada.
- Usa el parámetro como un atributo, con la clave establecida en
id
y el valor el valor en tiempo de ejecución del parámetro.
El resultado
Ahora podemos jugar con nuestra demo simple para ver el resultado:
curl localhost:9080/products
curl localhost:9080/products/1
Las respuestas no son interesantes, pero veamos la interfaz de usuario de Jaeger. Encontramos ambos traces, uno por llamada:
Podemos profundizar en los spans de un solo trace:
Nota que podemos inferir el flujo de secuencia sin el diagrama UML anterior. Incluso mejor, la secuencia muestra las llamadas internas a un componente.
Cada span contiene atributos que la instrumentación automática agregó y los que agregamos manualmente:
Conclusión
En esta publicación, he mostrado el tracing siguiendo una solicitud a través de un API gateway, dos aplicaciones basadas en diferentes pilas tecnológicas y sus respectivas bases de datos. Solo he rozado la superficie del tracing: en el mundo real, el tracing probablemente involucraría componentes no relacionados con HTTP, como Kafka y colas de mensajes.
Aún así, la mayoría de los sistemas dependen de HTTP de una forma u otra. Aunque no es trivial de configurar, tampoco es demasiado difícil. Rastrear solicitudes HTTP a través de componentes es un buen comienzo en tu viaje hacia la observabilidad de tu sistema.
El código fuente completo para esta publicación se puede encontrar en GitHub.
Para ir más allá: