En el anterior post vimos qué es OpenTelemetry y cómo nos ayuda a entender qué pasa en nuestro sistema en términos de trazabilidad distribuida. En este post vamos a dar un paso más y vamos a ver cómo podemos tener un centro de observabilidad completo, y también conseguir esa correlación entre logs y trazabilidad usando OpenTelemetry y Grafana.

Escenario inicial

Al igual que en el post anterior, partimos del siguiente escenario:

Los cuatro servicios heterogéneos de lo que partimos.

Hay cuatro servicios heterogéneos. El flujo empieza con la invocación a “service A”, que recibe un valor inicial. Después, cada servicio incrementa ese valor en 10 unidades. A diferencia del caso anterior, hemos incluido casos a propósito en los que cada servicio lanza una excepción si recibe un valor concreto. Esto lo hemos hecho para disponer de casos de error durante la ejecución.

Arquitectura de observabilidad

Como hemos comentado, partiremos de la base del post sobre trazabilidad distribuida y nuestro objetivo será dar un paso más allá: disponer de un centro de observabilidad todo en uno, en el que podamos ver tanto información de las trazas de los servicios como de la información de métricas y de logs.

Por tanto, vamos a añadir algunas piezas a la arquitectura para poder gestionar la información de las métricas y de logs. La arquitectura, a nivel general, queda de la siguiente forma:

Arquitectura a nivel general.

A continuación vamos a ver con más detalle cada punto.

Trazabilidad

Si vemos el diagrama, la gestión de la trazabilidad difiere con el post anterior únicamente en la herramienta utilizada como backend de trazabilidad: cambiamos Jaeger por Tempo (más Grafana).

Esta es una de las ventajas que tenemos al usar el agente y el colector de OpenTelemetry, ya que no hemos modificado los servicios. Simplemente, hemos cambiado el backend de trazabilidad sin afectar a los servicios. Si vamos al fichero “otel-collector-config.yaml” veremos lo siguiente:

receivers:
 otlp:
   protocols:
     grpc:
       endpoint: otel-collector:4317

…
…

exporters:
 otlp:
   endpoint: tempo:4317
   tls:
     insecure: true

…

El receiver no cambia, pero, como hemos comentado, se ha eliminado el exporter relativo a Jaeger y se ha cambiado por el exporter de OLTP que envía la información a Tempo. En las aplicaciones no hemos realizado ningún cambio.

Métricas

Este punto es nuevo respecto al post anterior. En este caso, optamos por Prometheus como backend de métricas. Podríamos haber optado por recuperar la información de métricas directamente desde Prometheus haciendo pull a los servicios, pero como el post tiene carácter divulgativo, hemos preferido usar el colector de OpenTelemetry como punto de desacople.

Usar el colector nos permitiría enviar las métricas a varios backends (no solo a Prometheus) e incluso realizar operaciones de transformación o filtrado antes de enviarlas. He de decir que, en el momento de escribir este post, el colector oficial solo soporta dos procesadores oficialmente, pero en el proyecto contrib dispones de muchos más.

De nuevo, si vamos al fichero “otel-collector-config.yaml” veremos lo siguiente:

receivers:
..
..
 prometheus:
   config:
     scrape_configs:
     - job_name: 'service-a'
       scrape_interval: 2s
       metrics_path: '/metrics/prometheus'
       static_configs:
         - targets: [ 'service-a:8080' ]
     - job_name: 'service-b'
       scrape_interval: 2s
       metrics_path: '/actuator/prometheus'
       static_configs:
         - targets: [ 'service-b:8081' ]
     - job_name: 'service-c'
       scrape_interval: 2s
      metrics_path: '/q/metrics'
       static_configs:
         - targets: [ 'service-c:8082' ]
     - job_name: 'service-d'
       scrape_interval: 2s
       metrics_path: '/q/metrics'
       static_configs:
         - targets: [ 'service-d:8083' ]

…
…

exporters:
…
 prometheusremotewrite:
    endpoint: http://prometheus:9090/api/v1/write
    tls:
      insecure: true

…

Hemos añadido la configuración Prometheus en la parte destinada a los receivers, ya que el colector de OpenTelemetry lo soporta. En esta configuración decimos que, según el intervalo definido, se hagan peticiones a los endpoints de métricas de cada servicio para recuperar la información. Después, definimos el exporter de push a Prometheus. Para que funcione, es necesario activar en Prometheus esta característica (“--web.enable-remote-write-receiver”).

Hemos habilitado los endpoints de métricas en las aplicaciones. En el caso del servicio en NodeJS, nos apoyamos en prom-client y express-prom-bundle. En el caso de Spring Boot, incluimos la dependencia de Micrometer:

<dependency>
   <groupId>io.micrometer</groupId>
   <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

En el caso de Quarkus, de forma similar a Spring, incluimos la dependencia de Micrometer:

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-micrometer-registry-prometheus</artifactId>
</dependency>

Para ver más detalles, no dudes en consultar el repositorio asociado al post.

Logs

En el caso de los logs, nos vamos a apoyar en Fluent Bit. Fluent Bit es un subproyecto de Fluentd y lo usaremos para “recoger” los logs de los diferentes servicios y enviarlos a un agregador de logs. En nuestro caso, el agregador de logs es Grafana Loki.

En este caso no usamos el colector de OpenTelemetry, sino que nos apoyamos en la herramienta de gestión de contenedores. Utilizaremos Docker y aprovecharemos su capacidad para gestionar los logs de los contenedores. Por si no lo sabes, Docker dispone de un driver para Fluentd que, para la PoC que mostramos en este post, nos viene como anillo al dedo. Simplemente, incluimos las siguientes líneas en la definición de los servicios en docker-compose.yml. Por ejemplo, para el servicio A:

service-a:
 …
 …
 logging:
   driver: fluentd
   options:
     fluentd-async-connect: "true"
     fluentd-address: "localhost:24224"

Estamos diciendo que envíe la información de los logs a la dirección configurada (localhost:24224). En esa dirección está escuchando Fluent Bit, que recoge la información y la manda al agregador. Esto lo configuramos en el fichero “fluent-bit.conf”:

[INPUT]
    Name        forward
    Listen      0.0.0.0
    Port        24224
[Output]
    Name grafana-loki
    Match *
    Url ${LOKI_URL}
    RemoveKeys source,container_id
    Labels {job="fluent-bit"}
    LabelKeys container_name
    BatchWait 1s
    BatchSize 1001024
    LineFormat json
    LogLevel info

Visualización

Por último, necesitamos un dashboard donde poder consultar la información de manera visual. Para ello utilizamos Grafana.

En Grafana, hemos definido un datasource por cada elemento que queremos gestionar: trazas, métricas y logs. En el fichero “resources/grafana/datasource.yml” encontrarás los detalles de la configuración de estos datasources.

Sí que vamos a destacar estas líneas en la definición del datasource de Loki:

jsonData:
 derivedFields:
   - datasourceUid: tempo
     matcherRegex: "traceId=(\\w+)"
     name: TraceID
     url: ${__value.raw}

Estas líneas nos van a permitir disponer de la capacidad de ir directamente desde una línea de log a la información completa de las trazas. Con esa configuración le decimos que busque la expresión “traceId” dentro de los mensajes de logs y que permita acceder a Tempo pasando el valor de ese “traceId”.

Ejecutando la PoC

Para ejecutar y jugar con la PoC, simplemente tenemos que clonar el repositorio y lanzar el siguiente comando:

sh build.sh

Una vez que el script haya finalizado, veremos algo similar a la siguiente imagen:

Script finalizado.

Para que la PoC tenga sentido, el script lanza en segundo plano otro script que genera llamadas a los servicios. De esta forma tendremos ya información en el dashboard.

Ya podemos acceder al dashboard de Grafana:

Dashboard de Grafana.

Es un dashboard muy sencillo en el que podemos ver indicadores sobre:

Si queremos generar más peticiones, ejecutamos este script pasando como parámetro el número de peticiones que queremos generar (por ejemplo, 50):

sh load.sh 50

Ahora vamos a detallar un poco lo que podemos ver en la PoC:

Correlación entre logs y trazabilidad

Un punto interesante que hemos comentado antes es la posibilidad de enlazar logs y trazabilidad. Por ejemplo, si vamos al panel de trazas con error y hacemos clic en cualquier valor de la columna “TraceId”:

Panel de trazas con error.

Accederemos directamente a Tempo para ver la información concreta de esa traza:

En Tempo vemos la información concreta de esa traza.

Gráfico de Trazabilidad

Otra característica de gran impacto visual es el gráfico de trazabilidad. Para ello, pulsamos sobre “Node Graph”:

Gráfico de trazabilidad.

Y veremos, de forma gráfica, el camino que ha seguido la petición:

Vemos de forma gráfica el camino de la petición.

Información sobre las peticiones

En la parte baja del dashboard vemos información sobre las peticiones al API Rest (similar al método RED):

Peticiones al API Rest.

Como podemos observar en el caso de la PoC, al haber lanzado todas las peticiones casi de forma simultánea con el script, existen picos de peticiones que coinciden con los picos de duración y con las peticiones erróneas.

Conclusión

En este post hemos visto cómo podemos crear una herramienta de observabilidad sencilla, apoyada en el colector de OpenTelemetry y en el stack de Grafana, que es capaz de recoger información de servicios de diferente naturaleza.

Esperamos que os haya gustado y pueda ser de utilidad. ¡Os leemos en los comentarios!

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.