Métricas para aplicaciones de Node.js con Prometheus

Métricas para aplicaciones de Node.js con Prometheus

Cuando las aplicaciones desempeñan un papel crucial en la operación de negocios, educación, entretenimiento y prácticamente todas las facetas de la vida diaria, garantizar que funcionen sin problemas es más importante que nunca. Actualmente, no solo se trata de mantenerlas siempre en línea, sino también de entender cómo están funcionando y cómo podríamos mejorarlas. Por esta razón, el monitoreo de aplicaciones no es solo esencial para mantener su salud y rendimiento, sino también para anticiparse a problemas futuros, optimizar recursos y asegurar una experiencia de usuario ininterrumpida.

Para ello existe Prometheus, una herramienta de monitoreo y alerta de código abierto que ha ganado popularidad entre los desarrolladores por su facilidad de uso, eficiencia y capacidad de integración con otras herramientas.

Tipos de métricas en Prometheus

Antes de profundizar, es esencial que quienes estén empezando con Prometheus comprendan los diferentes tipos de métricas que esta herramienta ofrece. A continuación, detallaré el significado de cada uno de los métodos que Prometheus pone a disposición para el registro de métricas. Es importante recordar que la finalidad de estas métricas es emplearlas en cada ejecución de un proceso.

En Prometheus, cuando defines una métrica, hay dos propiedades esenciales que siempre debes especificar: name y help. Las que significan:

  1. name: Esta propiedad identifica de manera única a la métrica en el sistema. Es el nombre con el que referenciarás la métrica cuando quieras obtener o manipular sus datos.

    • Por convención, el nombre de una métrica debe describir claramente qué está midiendo y seguir las convenciones de nomenclatura. Por ejemplo, si estás contando solicitudes, requests_total es un buen nombre porque indica que estás llevando un conteo acumulado de las solicitudes.

    • Además, las métricas en Prometheus suelen seguir un patrón de nomenclatura en el que se utiliza el tipo de métrica (como total para contadores, seconds para duraciones) y guiones bajos para separar palabras. Estas convenciones ayudan a que la métrica sea autoexplicativa y fácilmente identificable.

  2. help: Esta propiedad proporciona una descripción de la métrica. Es una cadena de texto que explica qué es lo que mide esa métrica en específico.

    • Es importante ser claro y conciso en esta descripción, ya que facilita a otros desarrolladores (o a ti mismo en el futuro) entender rápidamente el propósito y el uso de la métrica.

    • Cuando estás usando herramientas que interactúan con Prometheus o cuando estás explorando métricas directamente en la interfaz web de Prometheus, la descripción help es una guía útil para comprender qué representa cada métrica.

Ambas propiedades son sumamente importantes al momento de definir nuevas métricas.

  1. buckets: Este último parámetro es específico para el tipo de métrica Histogram. Un histograma permite medir la distribución observada de valores en "cubos" o "buckets". En lugar de simplemente contar eventos o sumar valores, un histograma divide el rango de valores observados en intervalos y cuenta cuántas observaciones caen en cada intervalo.

El parámetro buckets define los límites superiores de estos intervalos.

Por ejemplo, al definir:

buckets: [1, 5, 10, 20, 50, 100, 200, 500]

Estás creando los siguientes intervalos:

  • (-inf, 1]
  • (1, 5]
  • (5, 10]
  • (10, 20]
  • (20, 50]
  • (50, 100]
  • (100, 200]
  • (200, 500]
  • (500, +inf)

Si registras un valor de, digamos, 7, incrementaría el contador del bucket (5, 10].

Usarías un histograma en casos donde:

  1. Quieres observar la distribución de valores. Por ejemplo, podrías usar un histograma para medir la latencia de las respuestas de un servicio. En lugar de simplemente conocer la latencia promedio, podrías saber cuántas solicitudes se completaron en menos de 1 ms, cuántas entre 1 ms y 5 ms, cuántas entre 5 ms y 10 ms, etc.

  2. Quieres calcular percentiles. A partir de un histograma, puedes calcular percentiles, que son medidas útiles para comprender la experiencia del usuario. Por ejemplo, el percentil 95 (P95) te indica el valor por debajo del cual se encuentra el 95% de las observaciones.

  3. Quieres monitorear SLAs o SLOs. Puedes usar histogramas para monitorear acuerdos de nivel de servicio o objetivos de nivel de servicio. Por ejemplo, podrías tener un SLO que dice que el 99% de las solicitudes deben completarse en menos de 100 ms. Un histograma te permitiría medir este objetivo fácilmente.

Es importante elegir los límites de tus buckets cuidadosamente. Deben ser relevantes para las métricas que estás observando y proporcionar suficiente detalle donde lo necesites. Por ejemplo, si la mayoría de tus latencias están entre 10 ms y 20 ms, podría tener sentido tener más buckets en ese rango para obtener una visión más detallada. Por otro lado, tener demasiados buckets puede aumentar innecesariamente el uso de memoria y almacenamiento en Prometheus.

Ahora, con esta información veamos los diferentes tipos de métricas:

  • Counter (Contador): Una métrica acumulativa que representa un valor único que solo puede incrementarse o resetearse a cero en su reinicio. Es ideal para contar eventos, como solicitudes a un endpoint o errores.
const requestCounter = new prometheus.Counter({
  name: 'requests_total',
  help: 'Número total de solicitudes realizadas'
});

// Incrementar el contador
requestCounter.inc();
  • Gauge (Medidor): Representa un valor que puede subir o bajar arbitrariamente. Es útil para métricas que varían con el tiempo, como la memoria utilizada o el número de conexiones activas.
const memoryUsageGauge = new prometheus.Gauge({
  name: 'memory_usage_bytes',
  help: 'Uso actual de memoria en bytes'
});

// Establecer el valor del medidor
memoryUsageGauge.set(process.memoryUsage().heapUsed);
  • Histogram: Permite observar la distribución de valores numéricos en intervalos. Es excelente para medir la latencia de las operaciones o el tamaño de las peticiones. Prometheus registra un conteo y la suma acumulativa de los valores observados, permitiendo calcular promedios.
const requestDurationHistogram = new prometheus.Histogram({
  name: 'request_duration_seconds',
  help: 'Duración de las solicitudes en segundos',
  buckets: [0.1, 0.2, 0.5, 1, 2]  // Definición de los intervalos
});

// Observar un valor
requestDurationHistogram.observe(0.35);  // Supongamos que una solicitud tardó 0.35 segundos
  • Summary (Resumen): Similar a un histograma, registra también una serie de percentiles definidos. No es tan eficiente en términos de rendimiento y espacio como el histograma, pero puede ser útil cuando se necesitan percentiles precisos.
const requestDurationSummary = new prometheus.Summary({
  name: 'request_duration_summary_seconds',
  help: 'Resumen de la duración de las solicitudes en segundos',
  percentiles: [0.5, 0.9, 0.95, 0.99]  // Percentiles que queremos calcular
});

// Observar un valor
requestDurationSummary.observe(1.2);  // Supongamos que una solicitud tardó 1.2 segundos

Entender estos tipos de métricas es esencial, ya que nos permiten escoger el instrumento adecuado según la naturaleza de la métrica que queremos observar, y con esta información pasemos a hablar sobre las metricas en las aplicaciones y específicamente en aplicaciones de Node.js.

Debes tener en cuenta que su forma de uso es bastante simple, pues solo necesitas llamar o invocar la función de la métricas cuando deseas registrar un evento.

Métricas Generales

Desde un punto de vista global, existen ciertas métricas que son cruciales para todas las aplicaciones, independientemente del lenguaje o plataforma (me estoy refiriendo específicamente a aplicaciones webs), por ej.:

  • Tiempo de respuesta de las peticiones (latencia): El tiempo que tarda la aplicación en responder.
  • Tasa de errores: Porcentaje de solicitudes fallidas.
  • Tasa de peticiones (throughput): Peticiones procesadas por unidad de tiempo.
  • Uso de memoria: Memoria RAM consumida por la aplicación.
  • Uso de CPU: Porcentaje de uso del CPU por la aplicación.

Métricas específicas para Node.js

Dado que Node.js es un entorno de ejecución de JavaScript basado en eventos, presenta métricas únicas que deben monitorearse:

  • Número de conexiones activas: Conexiones en tiempo real a la aplicación.

Supongamos que estamos utilizando un servidor web basado en Express. Queremos rastrear conexiones activas.

const express = require('express');
const prometheus = require('prom-client');

const app = express();

const activeConnectionsGauge = new prometheus.Gauge({
  name: 'active_connections',
  help: 'Número de conexiones activas en tiempo real'
});

app.use((req, res, next) => {
  activeConnectionsGauge.inc();
  res.on('finish', () => {
    activeConnectionsGauge.dec();
  });
  next();
});

app.get('/', (req, res) => {
  res.send('Hello World');
});

app.listen(3000);
  • Tamaño de la cola: Si se emplea un sistema de cola, la cantidad de mensajes en espera.

Imagina que usamos una cola basada en RabbitMQ y queremos rastrear el tamaño de la cola.

const amqp = require('amqplib/callback_api');
const prometheus = require('prom-client');

const queueSizeGauge = new prometheus.Gauge({
  name: 'queue_size',
  help: 'Número de mensajes en espera en la cola'
});

amqp.connect('amqp://localhost', (error, connection) => {
  connection.createChannel((error, channel) => {
    const queue = 'task_queue';

    channel.assertQueue(queue, { durable: true });
    channel.consume(queue, (msg) => {
      // Procesar mensaje
      queueSizeGauge.dec();
    }, { noAck: true });

    queueSizeGauge.set(channel.messageCount(queue));
  });
});
  • Tiempos de inicio/finalización de procesos: Duración del arranque y cierre de la aplicación.

Supongamos que queremos medir cuánto tiempo lleva procesar una solicitud en particular.

const express = require('express');
const prometheus = require('prom-client');

const app = express();

const processDurationHistogram = new prometheus.Histogram({
  name: 'process_duration_seconds',
  help: 'Duración del arranque y cierre de la aplicación en segundos',
  buckets: [0.1, 0.5, 1, 2, 5, 10]
});

app.get('/process', (req, res) => {
  const startTime = Date.now();
  // Simular algún procesamiento
  setTimeout(() => {
    const duration = (Date.now() - startTime) / 1000;
    processDurationHistogram.observe(duration);
    res.send('Process completed');
  }, Math.random() * 5000);
});

app.listen(3000);
  • Tamaño de payload: Volumen de datos transmitidos por solicitud.

Si estamos usando Express y queremos rastrear el tamaño del cuerpo de la solicitud (payload):

const express = require('express');
const prometheus = require('prom-client');

const app = express();

const payloadSizeHistogram = new prometheus.Histogram({
  name: 'request_payload_size_bytes',
  help: 'Volumen de datos transmitidos por solicitud en bytes',
  buckets: [100, 500, 1000, 5000, 10000, 50000]
});

app.use(express.json());

app.post('/data', (req, res) => {
  const payloadSize = Buffer.from(JSON.stringify(req.body)).length;
  payloadSizeHistogram.observe(payloadSize);
  res.send('Data received');
});

app.listen(3000);

Específicamente relacionado con el Event Loop de Node.js:

  • Tiempo de Event Loop: En aplicaciones Node.js debido a la naturaleza de su modelo de ejecución basado en un único hilo. Medir este tiempo puede ayudarte a comprender y optimizar el rendimiento de tu aplicación por las siguientes razones:
  1. Diagnóstico de Cuellos de Botella: Si una tarea está ocupando demasiado tiempo en el Event Loop, puede causar que otras tareas tarden más en ser procesadas, afectando la capacidad de respuesta y el rendimiento general de la aplicación.

  2. Prevenir Bloqueos: En un entorno de Node.js, el bloqueo del Event Loop (por ejemplo, debido a una operación síncrona costosa) significa que toda la aplicación se bloquea. Si el tiempo del Event Loop aumenta drásticamente, es una señal de alerta de que algo en el código podría estar acercándose a bloquear el Event Loop.

  3. Optimización del Rendimiento: Al monitorear el tiempo del Event Loop, puedes identificar operaciones ineficientes o tareas intensivas que podrían ser optimizadas o movidas a procesos en segundo plano.

  4. Mejor Experiencia del Usuario: Una aplicación que tiene un Event Loop rápido y eficiente será más receptiva y brindará una mejor experiencia al usuario.

  5. Capacidad de Planificación: Si observas que regularmente el tiempo del Event Loop está cerca de su límite, puede ser una señal de que es necesario escalar la aplicación, bien sea optimizando el código o añadiendo más recursos.

  6. Identificar Regresiones: Después de implementar nuevas características o realizar cambios en el código, el monitoreo del tiempo del Event Loop puede ayudar a identificar regresiones en el rendimiento antes de que los usuarios se den cuenta.

Sí, cuando ejecutas una aplicación Node.js en modo cluster, la métrica del Tiempo de Event Loop sigue siendo relevante, pero su interpretación y monitoreo tienen algunas consideraciones adicionales.

Incluso en el modo cluster, Node.js permite la creación de procesos hijos (workers) que comparten el mismo puerto del servidor. Cada worker corre en su propio hilo de ejecución y tiene su propio Event Loop. Esto es utilizado para aprovechar al máximo CPUs multi-core, ya que por defecto, Node.js es de un único hilo y no usaría todos los cores disponibles en un sistema.

Algunas de las razones y consideraciones sobre por qué deberías seguir monitoreando el tiempo de Event Loop en un contexto multi hilos serían:

  1. Análisis detallado por worker: Cada worker tiene su propio Event Loop, y podría ser beneficioso monitorear el tiempo de Event Loop de cada worker individualmente. Esto puede ayudar a identificar si un worker específico está experimentando problemas.

  2. Balanceo de carga: Aunque el modo cluster está diseñado para balancear la carga entre workers, no siempre garantiza un balanceo perfecto. Si un worker tiene un tiempo de Event Loop consistentemente más alto que otros, podría ser una indicación de que está recibiendo una carga desproporcionada o está manejando tareas más costosas.

  3. Escalabilidad: Monitorear el tiempo de Event Loop en un entorno clusterizado puede ofrecer insights sobre cuándo añadir más workers o si hay necesidad de optimizar el código antes de escalar.

  4. Resiliencia: Si un worker se bloquea o se cierra debido a un error, el sistema puede seguir funcionando con los workers restantes. Sin embargo, esto puede llevar a un aumento en la carga de los workers restantes. Monitorear el tiempo de Event Loop puede alertarte sobre estos escenarios.

Para serles honesto, nunca he monitoreado servicios creados con Node.js usando clusters porque siempre creo pequeños servicios en lugar de servicios grandes y complicados, así es que no tengo mucho conocimiento sobre como ser verían en Prometheus este tipo de mediciones, pero en teoría debería funcionar igual y por supuesto, el nombre debería de ser diferente para cada worker porque eso nos ayudaría a determinar la razón del problema desde el panel de monitoreo de Prometheus.

Para monitorear el tiempo de Event Loop, podemos usar el módulo event-loop-lag:

const prometheus = require('prom-client');
const createLag = require('event-loop-lag');

const lag = createLag();

const eventLoopLagHistogram = new prometheus.Histogram({
  name: 'event_loop_lag_milliseconds',
  help: 'Duración de una iteración del Event Loop en milisegundos',
  buckets: [1, 5, 10, 20, 50, 100, 200, 500]
});

setInterval(() => {
  const lagInMilliseconds = lag();
  eventLoopLagHistogram.observe(lagInMilliseconds);
}, 1000);

Métricas adicionales

Hay otras métricas que, dependiendo de la aplicación, pueden ser relevantes:

  • Número de hilos: Hilos en ejecución.
  • Número de procesos: Procesos en ejecución.
  • Uso de E/S: Datos leídos y escritos en memoria.
  • Uso de red y almacenamiento: Datos transmitidos y almacenados.

Reflexión sobre las métricas

Los datos son el núcleo de muchas decisiones estratégicas. Las métricas, siendo una representación cuantificable de estos datos, son esenciales para entender y mejorar cualquier sistema o proceso. Medir es el primer paso para mejorar, y en el ámbito del desarrollo, las métricas adecuadas ofrecen insights valiosos que trascienden simples números.

Más allá de simplemente detectar cuellos de botella, las métricas bien elegidas permiten una optimización efectiva de recursos, ya sean humanos, financieros o computacionales. Esto puede traducirse en ahorros significativos y una mejor asignación de esfuerzos y recursos en el ciclo de desarrollo y puesta en marcha de cualquier aplicación.

Al reflexionar sobre la importancia de las métricas, es evidente que no son solo un conjunto de números en un tablero. Son la brújula que nos guía a los desarrolladores hacia decisiones más informadas y al éxito a largo plazo de nuestras aplicaciones.

Conclusión

Prometheus es una herramienta poderosa para monitorear aplicaciones de cualquier tipo; al integrar métricas adecuadas, ganamos una visibilidad invaluable sobre las aplicaciones, permitiendo intervenciones proactivas y entregando una experiencia de usuario de calidad.

Este ha sido un artículo bastante extenso, pero muy completo a la vez, espero que te sirva y también que quede claro que es Prometheus y como se puede utilizar para medir el rendimiento de cualquier aplicación, especialmente en Node.js.

Happy coding! :D


Photo by Tekton on Unsplash

Jack Fiallos

Jack Fiallos

Te gustó este artículo?