Как собрать метрики Node.js приложений в PM2 с экспортом в Prometheus

Ни для кого не секрет что для устойчивой и надежной работы node.js приложений необходимо проводить мониторинг их работы и делать полезные выводы глядя на их метрики. Это означает, что вы способны получать информацию о состоянии до возникновения проблем, таким образом, предотвращая сбои.

В этой статье я хотел бы рассказать о способе сбора статистики из node.js приложений, которые запущены в PM2, и экспорт этих данных в Prometheus.

Когда вы просто запускаете node.js приложение через команду node или же через PM2 командой pm2 start app.js, то новое приложение поднимается в единственном экземпляре. Сбор и экспорт каких либо метрик в этом случае будет не затруднительным. Для этого устанавливаем пакет prom-client и добавляем нужные нам метрики, например, количество обращений к нашему приложению.

import client from 'prom-client';
import { createServer } from 'http';

const registry = new client.Registry();
const PREFIX = `nodejs_app_`;

export const metricRequestsTotal = new client.Counter({
    name: `${PREFIX}request_counter`,
    help: 'Show total request count',
    registers: [registry],
});

// Старт вашего приложения
// ...
nodejsapp.get('/*', async (req, res) => {
    metricRequestsTotal.inc();
});

// Запуск сервера для отдачи метрик
const promServer = createServer(async (req, res) => {
    res.setHeader('Content-Type', registry.contentType);
    res.end(await registry.metrics());
    return;
});

promServer.listen(9100, () =>
    console.log('Prom server started')
);

В данном примере запускается ваше приложение и вместе с ним стартует экспорт метрик на дополнительном порту 9100. Можно использовать какой-то отдельный URL и на основном порту приложения, но он не должен быть публичным для пользователей.

Такой способ будет работать до тех пор, пока вам не нужно будет запустить несколько инстансов приложения в режиме кластера. Для сбора метрик в таком режиме у prom-client есть хороший пример, где происходит агрегация метрик. Но как быть, если вы запускаете приложение через менеджер процессов PM2 в котором у вас нет доступа к запущенному кластеру?

В одной из моих предыдущих статей я рассказал о модуле pm2-prom-module который позволял собирать общую статистику по приложениям из PM2, но не мог собирать внутреннюю статистику из каждого приложения. Теперь же, начиная с версии 2.0, такого ограничения нет и вся статистика из PM2 и внутри приложения отдается через один модуль.

Для обмена данными между приложением nodejs и pm2-prom-module необходимо установить npm пакет pm2-prom-module-client в ваше приложение. В итоге пример выше будет выглядеть вот таким образом:

import client from 'prom-client';
import { initMetrics } from 'pm2-prom-module-client';

const registry = new client.Registry();
const PREFIX = `nodejs_app_`;

const metricRequestCounter = new client.Counter({
    name: `${PREFIX}request_counter`,
    help: 'Show total request count',
    registers: [registry],
});

// Регистрируем registry для отправки данных в модуль
initMetrics(registry);

// ...
nodejsapp.get('/*', async (req, res) => {
    // ...
    metricRequestCounter?.inc();
    // ...
});

Теперь pm2-prom-module будет отдавать не только PM2 статистику, но и внутреннюю статистику из ваших приложений.

/// Общая статистика PM2

# HELP pm2_free_memory Show available host free memory
# TYPE pm2_free_memory gauge
pm2_free_memory{serviceName="my-app"} 377147392

# HELP pm2_cpu_count Show available CPUs count
# TYPE pm2_cpu_count gauge
pm2_cpu_count{serviceName="my-app"} 4

# HELP pm2_available_apps Show available apps to monitor
# TYPE pm2_available_apps gauge
pm2_available_apps{serviceName="my-app"} 2

/// Статистика из приложений
# HELP nodejs_app_request_counter Show total request count
# TYPE nodejs_app_request_counter counter
nodejs_app_request_counter{app="app",instance="13",serviceName="my-app"} 10
nodejs_app_request_counter{app="app",instance="14",serviceName="my-app"} 17

По-умолчанию вся статистика агрегируется по всем запущенным инстансам, но если вы хотите детальную статистику по каждому инстансу, то это можно включить через параметр конфигурации модуля:

pm2 set pm2-prom-module:aggregate_app_metrics false

Этот модуль, в отличие от некоторых других, позволяет собирать метрики по каждому приложению отдельно и они не пересекаются.

В целом, статистика, которая отдается PM2 достаточна для базового мониторинга приложений, но в нашем случае, например, нужно было больше детальных данных по времени рендера страниц или загрузки некоторых блоков данных. Для таких целей мы использовали гистограмму:

// ...
export const metricRequestTime = new client.Histogram({
    name: 'nodejs_app_page_execute',
    help: 'Time to processing request',
    registers: [registry],
    buckets: [0.1, 0.2, 0.3, 0.5, 0.7, 1, 2, 3, 5],
    labelNames: ['code', 'page'],
});

// ...
const endMetricRequestTime = metricRequestTime.startTimer();
// ...
endMetricRequestTime({ code: 200, page: 'Homepage });

В данном примере мы используем 2 дополнительных параметра code (код ответа) и page (идентификатор страницы) благодаря которым предоставляется детальная статистика, например, можно построить такие графики в Grafana:

  • Какие страницы чаще всего запрашивают sum(rate(nodejs_app_page_execute_count{serviceName="$serviceName",code="200"}[5m])) by (page)
  • Количество ошибок в ответах сервиса (коды ответов) sum(rate(nodejs_app_page_execute_count{serviceName="$serviceName"}[5m])) by (code)
  • Время загрузки каждой страницы указывая, например, 99 перцентиль histogram_quantile(0.99, sum(rate(nodejs_app_page_execute_bucket{serviceName="$serviceName", code="200"}[1m])) by (page, le))
  • И множество других графиков по каждой отдельной странице
Кстати, я не советую использовать URL страницы как значение page - это увеличит во много раз использование памяти и размер ответа в Prometheus и как следствие - неразбериху в графиках. Лучше определять какая страница загрузилась и использовать ее идентификатор.

В итоге мы получаем единственную точку сбора и отдачи статистики по всем приложениям запущенным в PM2 и систему мониторинга построенную на базе Prometheus и функциональные дашбоарды в Grafana.

Пример дашбоарда с pm2-prom-module