Monitoreo de aplicaciones Node.js con Prometheus y Grafana

En esta guía, veremos cómo monitorear aplicaciones Node.js usando Prometheus y Grafana, con código práctico y ejemplos.

Monitoreo de aplicaciones

El monitoreo de aplicaciones sigue siendo una parte fundamental del mundo de los microservicios. Los desafíos asociados con el monitoreo de microservicios suelen ser exclusivos de su ecosistema y, a menudo, las fallas pueden ser discretas: la falla de un módulo pequeño puede pasar desapercibida durante algún tiempo.

Si observamos una aplicación monolítica más tradicional, instalada como una sola biblioteca o servicio ejecutable, las fallas suelen ser más explícitas ya que sus módulos no están destinados a ejecutarse como servicios independientes.

Durante el desarrollo, el monitoreo a menudo no se toma mucho en cuenta inicialmente, ya que normalmente hay asuntos más urgentes que atender. Sin embargo, una vez implementado, especialmente si el tráfico a la aplicación comienza a aumentar, es necesario monitorear los cuellos de botella y la salud del sistema para una respuesta rápida en caso de que algo falle.

Las fallas del sistema son el mejor caso para monitorear sus aplicaciones. Los sistemas distribuidos complejos, así como los monolitos genéricos, pueden operar en un estado degradado que afecta el rendimiento. Estos estados degradados a menudo conducen a fallas eventuales. El monitoreo del comportamiento de las aplicaciones puede alertar a los operadores sobre el estado degradado antes de que ocurra una falla total.

En esta guía, analizaremos Prometheus y Grafana para monitorear una aplicación Node.js. Usaremos una biblioteca Node.js para enviar métricas útiles a Prometheus, que luego las exporta a Grafana para la visualización de datos.

Prometheus: un producto con mentalidad de DevOps

Prometeo es un sistema de monitoreo de código abierto y miembro de la Base de computación nativa en la nube. Originalmente se creó como una solución de monitoreo interna para SoundCloud, pero ahora lo mantiene una [comunidad] de desarrolladores y usuarios (https://prometheus.io/community).

Características de Prometheus

Algunas de las características clave de Prometheus son:

  • Prometheus recopila las métricas del servidor o dispositivo extrayendo sus extremos de métricas a través de HTTP en un intervalo de tiempo predefinido.
  • Un modelo de datos de serie temporal multidimensional. En términos más simples, realiza un seguimiento de los datos de series temporales para diferentes características/métricas (dimensiones).
  • Ofrece un lenguaje de consulta funcional propietario, conocido como PromQL (lenguaje de consulta de Prometheus). PromQL se puede utilizar para la selección y agregación de datos.
  • Pushgateway: una memoria caché de métricas, desarrollada para guardar métricas de trabajos por lotes cuya corta vida generalmente las hace poco confiables o imposibles de extraer a intervalos regulares a través de HTTP.
  • Una interfaz de usuario web para ejecutar la expresión PromQL y visualizar los resultados en una tabla o gráfico a lo largo del tiempo.
  • También proporciona funciones de alerta para enviar alertas a un administrador de alertas al coincidir con una regla definida y enviar notificaciones por correo electrónico u otras plataformas.
  • La comunidad mantiene una gran cantidad de [exportadores e integradores] de terceros (https://prometheus.io/docs/instrumenting/exporters/#exporters-and-integrations) que ayudan a obtener métricas.

Diagrama de arquitectura {#diagrama de arquitectura}

Architecture Diagram

[Crédito: Prometeo.io]{.small}

Presentamos prom-client

Prometheus se ejecuta en su propio servidor. Para conectar su propia aplicación al servidor Prometheus, deberá usar un exportador de métricas, y exponer las métricas para que Prometheus pueda extraerlos a través de HTTP.

Confiaremos en la biblioteca cliente de baile para exportar métricas desde nuestra aplicación. Admite las exportaciones de datos necesarias para producir histogramas, resúmenes, indicadores y contadores.

Instalación de prom-client

La forma más fácil de instalar el módulo prom-client es a través de npm:

1
$ npm install prom-client

Exposición de métricas predeterminadas de Prometheus con prom-client

El equipo de Prometheus tiene un conjunto de métricas recomendadas para realizar un seguimiento, que prom-client incluye en consecuencia como las métricas predeterminadas, que se pueden obtener del cliente a través de collectDefaultMetrics().

Estas son, entre otras métricas, el tamaño de la memoria virtual, la cantidad de descriptores de archivos abiertos, el tiempo total de CPU empleado, etc.:

1
2
3
4
5
const client = require('prom-client');

// Create a Registry to register the metrics
const register = new client.Registry();
client.collectDefaultMetrics({register});

Realizamos un seguimiento de las métricas recopiladas en un “Registro”, por lo que cuando recopilamos las métricas predeterminadas del cliente, pasamos la instancia de “Registro”. También puede proporcionar otras opciones de personalización en la llamada collectDefaultMetrics():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const client = require('prom-client');

// Create a Registry to register the metrics
const register = new client.Registry();

client.collectDefaultMetrics({
    app: 'node-application-monitoring-app',
    prefix: 'node_',
    timeout: 10000,
    gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5],
    register
});

Aquí, hemos agregado el nombre de nuestra aplicación, un ‘prefijo’ para las métricas para facilitar la navegación, un parámetro de ’tiempo de espera’ para especificar cuándo se agota el tiempo de espera de las solicitudes, así como un ‘gcDurationBuckets’ que define qué tan grandes deben ser los cubos. ser para el Histograma de Recolección de Basura.

La recopilación de otras métricas sigue el mismo patrón: las recopilaremos a través del “cliente” y luego las registraremos en el registro. Más sobre esto más adelante.

Una vez que las métricas están ubicadas en el registro, podemos devolverlas desde el registro en un punto final del que Prometheus extraerá datos. Vamos a crear un servidor HTTP, exponiendo un punto final /metrics, que devuelve metrics() del registro cuando se pulsa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const client = require('prom-client');
const express = require('express');
const app = express();

// Create a registry and pull default metrics
// ...

app.get('/metrics', async (req, res) => {
    res.setHeader('Content-Type', register.contentType);
    res.send(await register.metrics());
});

app.listen(8080, () => console.log('Server is running on http://localhost:8080, metrics are exposed on http://localhost:8080/metrics'));

Usamos Express.js para exponer un punto final en el puerto ‘8080’, que cuando recibe una solicitud ‘GET’ devuelve las métricas del registro. Dado que metrics() devuelve una Promesa, hemos usado la sintaxis async/await para recuperar los resultados.

If you're unfamiliar with Express.js - read our Guía para construir una API REST con Node.js y Express.

Avancemos y enviemos una solicitud curl a este punto final:

1
$ curl -GET localhost:8080/metrics
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# HELP node_process_cpu_user_seconds_total Total user CPU time spent in seconds.
# TYPE node_process_cpu_user_seconds_total counter
node_process_cpu_user_seconds_total 0.019943

# HELP node_process_cpu_system_seconds_total Total system CPU time spent in seconds.
# TYPE node_process_cpu_system_seconds_total counter
node_process_cpu_system_seconds_total 0.006524

# HELP node_process_cpu_seconds_total Total user and system CPU time spent in seconds.
# TYPE node_process_cpu_seconds_total counter
node_process_cpu_seconds_total 0.026467

# HELP node_process_start_time_seconds Start time of the process since unix epoch in seconds.
...

Las métricas consisten en un montón de métricas útiles, cada una explicada a través de comentarios. Sin embargo, volviendo a la declaración de la introducción, en muchos casos, sus necesidades de monitoreo pueden ser específicas del ecosistema. Afortunadamente, también tiene total flexibilidad para exponer sus propias métricas personalizadas.

Exposición de métricas personalizadas con prom-client

Aunque exponer las métricas predeterminadas es un buen punto de partida para comprender el marco y su aplicación, en algún momento necesitaremos definir métricas personalizadas para emplear un ojo de halcón en algunos flujos de solicitudes.

Vamos a crear una métrica que realice un seguimiento de las duraciones de las solicitudes HTTP. Para simular una operación pesada en un punto final determinado, crearemos una operación simulada que tarde de 3 a 6 segundos en devolver una respuesta. Visualizaremos un Histograma de los tiempos de respuesta y la distribución que tienen. También tomaremos en consideración las rutas y sus códigos de retorno.

Para registrar y realizar un seguimiento de una métrica como esta, crearemos un nuevo ‘Histograma’ y usaremos el método ‘startTimer()’ para iniciar un temporizador. El tipo de retorno del método startTimer() es otra función que puede invocar para observar (registrar) las métricas registradas y finalizar el temporizador, pasando las etiquetas que le gustaría asociar a las métricas del histograma con.

Sin embargo, puede observar() manualmente los valores, es más fácil y limpio invocar el método devuelto.

Primero avancemos y creemos un ‘Histograma’ personalizado para esto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Create a custom histogram metric
const httpRequestTimer = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'code'],
  buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10] // 0.1 to 10 seconds
});

// Register the histogram
register.registerMetric(httpRequestTimer);

Nota: Los cubos son simplemente las etiquetas de nuestro Histograma y se refieren a la duración de las solicitudes. Si una solicitud tarda menos de 0.1s en ejecutarse, pertenece al depósito 0.1.

Nos referiremos a esta instancia cada vez que deseemos cronometrar algunas solicitudes y registrar su distribución. Definamos también un controlador de retraso, que retrasa la respuesta y, por lo tanto, simula una operación pesada:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Mock slow endpoint, waiting between 3 and 6 seconds to return a response
const createDelayHandler = async (req, res) => {
  if ((Math.floor(Math.random() * 100)) === 0) {
    throw new Error('Internal Error')
  }
  // Generate number between 3-6, then delay by a factor of 1000 (miliseconds)
  const delaySeconds = Math.floor(Math.random() * (6 - 3)) + 3
  await new Promise(res => setTimeout(res, delaySeconds * 1000))
  res.end('Slow url accessed!');
};

Finalmente, podemos definir nuestros extremos /metrics y /slow, uno de los cuales usa el controlador de retraso para retrasar las respuestas. Cada uno de estos será cronometrado con nuestra instancia httpRequestTimer y registrado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Prometheus metrics route
app.get('/metrics', async (req, res) => {
  // Start the HTTP request timer, saving a reference to the returned method
  const end = httpRequestTimer.startTimer();
  // Save reference to the path so we can record it when ending the timer
  const route = req.route.path;
    
  res.setHeader('Content-Type', register.contentType);
  res.send(await register.metrics());

  // End timer and add labels
  end({ route, code: res.statusCode, method: req.method });
});

// 
app.get('/slow', async (req, res) => {
  const end = httpRequestTimer.startTimer();
  const route = req.route.path;
  await createDelayHandler(req, res);
  end({ route, code: res.statusCode, method: req.method });
});

// Start the Express server and listen to a port
app.listen(8080, () => {
  console.log('Server is running on http://localhost:8080, metrics are exposed on http://localhost:8080/metrics')
});

Ahora, cada vez que enviamos una solicitud al punto final /slow, o al punto final /metrics, la duración de la solicitud se registra y se agrega al registro de Prometheus. Por cierto, también exponemos estas métricas en el punto final /metrics. Enviemos una solicitud GET a /slow y luego observemos /metrics nuevamente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ curl -GET localhost:8080/slow
Slow url accessed!

$ curl -GET localhost:8080/metrics
# HELP http_request_duration_seconds Duration of HTTP requests in seconds
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.1",route="/metrics",code="200",method="GET"} 1
http_request_duration_seconds_bucket{le="0.3",route="/metrics",code="200",method="GET"} 1
http_request_duration_seconds_bucket{le="0.5",route="/metrics",code="200",method="GET"} 1
http_request_duration_seconds_bucket{le="0.7",route="/metrics",code="200",method="GET"} 1
http_request_duration_seconds_bucket{le="1",route="/metrics",code="200",method="GET"} 1
http_request_duration_seconds_bucket{le="3",route="/metrics",code="200",method="GET"} 1
http_request_duration_seconds_bucket{le="5",route="/metrics",code="200",method="GET"} 1
http_request_duration_seconds_bucket{le="7",route="/metrics",code="200",method="GET"} 1
http_request_duration_seconds_bucket{le="10",route="/metrics",code="200",method="GET"} 1
http_request_duration_seconds_bucket{le="+Inf",route="/metrics",code="200",method="GET"} 1
http_request_duration_seconds_sum{route="/metrics",code="200",method="GET"} 0.0042126
http_request_duration_seconds_count{route="/metrics",code="200",method="GET"} 1
http_request_duration_seconds_bucket{le="0.1",route="/slow",code="200",method="GET"} 0
http_request_duration_seconds_bucket{le="0.3",route="/slow",code="200",method="GET"} 0
http_request_duration_seconds_bucket{le="0.5",route="/slow",code="200",method="GET"} 0
http_request_duration_seconds_bucket{le="0.7",route="/slow",code="200",method="GET"} 0
http_request_duration_seconds_bucket{le="1",route="/slow",code="200",method="GET"} 0
http_request_duration_seconds_bucket{le="3",route="/slow",code="200",method="GET"} 0
http_request_duration_seconds_bucket{le="5",route="/slow",code="200",method="GET"} 0
http_request_duration_seconds_bucket{le="7",route="/slow",code="200",method="GET"} 1
http_request_duration_seconds_bucket{le="10",route="/slow",code="200",method="GET"} 1
http_request_duration_seconds_bucket{le="+Inf",route="/slow",code="200",method="GET"} 1
http_request_duration_seconds_sum{route="/slow",code="200",method="GET"} 5.0022148
http_request_duration_seconds_count{route="/slow",code="200",method="GET"} 1

El histograma tiene varios cubos y realiza un seguimiento de la “ruta”, el “código” y el “método” que hemos usado para acceder a un punto final. Tomó 0.0042126 segundos para acceder a /metrics, pero la friolera de 5.0022148 para acceder al extremo /slow. Ahora, a pesar de que este es un registro realmente pequeño, realizar un seguimiento de una sola solicitud cada uno a solo dos puntos finales, no es muy fácil para los ojos. Los humanos no son buenos para digerir una gran cantidad de información como esta, por lo que es mejor consultar las visualizaciones de estos datos.

Para hacer esto, usaremos Grafana para consumir las métricas del punto final /metrics y visualizarlas. Grafana, al igual que Prometheus, se ejecuta en su propio servidor, y una forma fácil de instalarlos junto con nuestra aplicación Node.js es a través de un Docker Compose Cluster.

Configuración del clúster de Docker Compose

Comencemos creando un archivo docker-compose.yml que usaremos para que Docker sepa cómo iniciar y exponer los puertos respectivos para el servidor Node.js, el servidor Prometheus y el servidor Grafana. Dado que Prometheus y Grafana están disponibles como imágenes de Docker, podemos extraer sus imágenes directamente de Docker Hub:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
version: '2.1'
networks:
  monitoring:
    driver: bridge
volumes:
    prometheus_data: {}
    grafana_data: {}
services:
  prometheus:
    image: prom/prometheus:v2.20.1
    container_name: prometheus
    volumes:
      - ./prometheus:/etc/prometheus
      - prometheus_data:/prometheus
    ports:
      - 9090:9090
    expose:
      - 9090
    networks:
      - monitoring
  grafana:
    image: grafana/grafana:7.1.5
    container_name: grafana
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning
    environment:
      - GF_AUTH_DISABLE_LOGIN_FORM=true
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
    ports:
      - 3000:3000
    expose:
      - 3000
    networks:
      - monitoring
  node-application-monitoring-app:
    build:
      context: node-application-monitoring-app
    ports:
      - 8080:8080
    expose:
      - 8080
    networks:
      - monitoring

La aplicación Node está expuesta en el puerto 8080, Grafana está expuesta en 3000 y Prometheus está expuesta en 9090. Alternativamente, puede clonar nuestro repositorio GitHub:

1
$ git clone https://github.com/wikihtp/node-prometheus-grafana.git

También puede consultar el repositorio si no está seguro de qué archivos de configuración se supone que deben estar situados en qué directorios.

Todos los contenedores docker se pueden iniciar a la vez usando el comando docker-compose. Como requisito previo, ya sea que desee alojar este clúster en una máquina Windows, Mac o Linux, Motor acoplable y [Componer ventana acoplable](https://docs. docker.com/compose) deben estar instalados.

Note: If you'd like to read more about Docker and Docker Compose, you can read our guide to Docker: una introducción de alto nivel or Cómo Docker puede hacer su vida más fácil como desarrollador.

Una vez instalado, puede ejecutar el siguiente comando en el directorio raíz del proyecto:

1
$ docker-compose up -d

Después de ejecutar este comando, se ejecutarán tres aplicaciones en segundo plano: un servidor Node.js, la interfaz de usuario web de Prometheus y el servidor, así como la interfaz de usuario de Grafana.

Configuración de Prometheus para raspar métricas {#configuración de prometheus para raspar métricas}

Prometheus raspa el punto final relevante en intervalos de tiempo determinados. Para saber cuándo raspar, así como dónde, necesitaremos crear un archivo de configuración: prometheus.yml:

1
2
3
4
5
6
global:
  scrape_interval: 5s
scrape_configs:
  - job_name: "node-application-monitoring-app"
    static_configs:
      - targets: ["docker.host:8080"]

Nota: docker.host debe reemplazarse con el nombre de host real del servidor Node.js configurado en el archivo YAML docker-compose.

Aquí, lo hemos programado para raspar las métricas cada 5 segundos. La configuración global predeterminada es de 15 segundos, por lo que la hemos hecho un poco más frecuente. El nombre del trabajo es para nuestra propia conveniencia y para identificar la aplicación que estamos controlando. Finalmente, el punto final /metrics del objetivo es lo que Prometheus observará.

Configurar fuente de datos para Grafana

Mientras configuramos Prometheus, creemos también una fuente de datos para Grafana. Como se mencionó anteriormente, y como se elaborará más adelante, acepta datos de una fuente de datos y los visualiza. Por supuesto, estas fuentes de datos deben ajustarse a algunos protocolos y estándares.

El archivo datasources.yml alberga la configuración de todas las fuentes de datos de Grafana. Solo tenemos uno: nuestro servidor Prometheus, expuesto en el puerto 9090:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    orgId: 1
    url: http://docker.prometheus.host:9090
    basicAuth: false
    isDefault: true
    editable: true

Nota: docker.prometheus.host se debe reemplazar con el nombre de host real de Prometheus configurado en el archivo YAML docker-compose.

Simular tráfico de grado de producción {#simular tráfico de grado de producción}

Finalmente, será más fácil ver los resultados si generamos algo de tráfico sintético en la aplicación. Simplemente puede volver a cargar las páginas varias veces o enviar muchas solicitudes, pero dado que esto llevaría mucho tiempo hacerlo a mano, puede usar cualquiera de las [varias herramientas] (https://github.com/denji/awesome- http-benchmark) como ApacheBench, ali, API Bench, etc.

Nuestra aplicación Node.js utilizará prom-client para registrarlos y enviarlos al servidor de Prometheus. Todo lo que queda es usar Grafana para visualizarlos.

Grafana: un panel fácil de configurar

Grafana es una plataforma de análisis utilizada para monitorear y visualizar todo tipo de métricas. Le permite agregar consultas personalizadas para sus fuentes de datos, visualizar, alertar y comprender sus métricas sin importar dónde estén almacenadas. Puede crear, explorar y compartir paneles con su equipo y fomentar una cultura basada en datos.

Grafana recopila datos de varias fuentes de datos y Prometheus es solo una de ellas.

Paneles de control de Grafana

Se incluyen algunos paneles listos para usar para proporcionar una descripción general de lo que está sucediendo. El Panel de aplicaciones de NodeJS recopila las métricas predeterminadas y las visualiza:

NodeJS Application Dashboard

El panel Métricas de aplicación de alto nivel muestra métricas de alto nivel para la aplicación Node.js utilizando métricas predeterminadas como la tasa de error, el uso de CPU, el uso de memoria, etc.:

High Level Application Metrics

El Panel de flujo de solicitudes muestra métricas de flujo de solicitudes utilizando las API que hemos creado en la aplicación Node.js. Es decir, aquí es donde brilla el ‘Histograma’ que hemos creado:

NodeJS Request Flow Dashboard

Gráfico de uso de memoria

En lugar de los paneles listos para usar, también puede crear agregaciones para calcular diferentes métricas. Por ejemplo, podemos calcular el uso de la memoria a lo largo del tiempo a través de:

1
avg(node_nodejs_external_memory_bytes / 1024) by (route)

Memory Usage Chart

Gráfico de histograma de solicitud por segundo

O bien, podemos trazar un gráfico que muestre las solicitudes por segundo (en intervalos de 2 minutos), utilizando los datos de nuestro propio recopilador de datos:

1
sum(rate(http_request_duration_seconds_count[2m]))

Request per second Histogram

Conclusión

Prometheus y Grafana son poderosas herramientas de código abierto para el monitoreo de aplicaciones. Con una comunidad activa y muchas bibliotecas de clientes e integraciones, pocas líneas de código brindan una visión clara y clara del sistema. a.