В статье показано как настроить мониторинг веб-приложения на Rust. Приложение выставляет наружу Prometheus метрики, которые визуализируются с помощью Grafana. Мониторинг осуществляется для проекта mongodb-redis demo, детально рассмотренного здесь. В итоге получена следующая архитектура:


architecture


Система мониторинга включает:


  • Prometheus — платформа для мониторинга, которая агрегирует метрики в режиме реального времени и сохраняет их в базу данных временных рядов (time series database).
  • Grafana — платформа для анализа и визуализации метрик
  • AlertManager — приложение, которое обрабатывает уведомления (alerts), отправляемые Prometheus сервером (например, когда в вашем приложении что-то идёт не так), и оповещает пользователя с помощью электронной почты, Slack, Telegram и т. д.
  • cAdvisor — платформа для пользователей, использующих контейнеризацию, которая предоставляет данные по использованию ресурсов и параметрам производительности запущенных контейнеров. (На самом деле cAdvisor собирает данные со всех Docker контейнеров на схеме)

Для запуска всех этих инструментов вы можете воспользоваться следующим:


Docker Compose файл


version: '3.8'
services:

  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    restart: always
    ports:
      - '9090:9090'
    volumes:
      - ./monitoring/prometheus:/etc/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--web.external-url=http://localhost:9090'

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: always
    ports:
      - '3000:3000'
    volumes:
      - ./monitoring/grafana/data:/var/lib/grafana
      - ./monitoring/grafana/provisioning:/etc/grafana/provisioning
    environment:
      GF_SECURITY_ADMIN_USER: admin
      GF_SECURITY_ADMIN_PASSWORD: admin

  alertmanager:
    image: prom/alertmanager:latest
    container_name: alertmanager
    ports:
      - '9093:9093'
    volumes:
      - ./monitoring/alertmanager:/etc/alertmanager
    command:
      - '--config.file=/etc/alertmanager/alertmanager.yml'
      - '--web.external-url=http://localhost:9093'

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    container_name: cadvisor
    restart: always
    ports:
      - '8080:8080'
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:rw
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro

Отдача Prometheus метрик Rust приложением


Отдача метрик имплементируется с помощью крейта prometheus.


Существует четыре типа Prometheus метрик: счётчик (counter), датчик/измеритель/шкала (gauge), гистограмма (histogram), сводка (summary). Использование первых трёх из них будет описано в статье (в настоящее время крейт не поддерживает сводки).


Создание метрик


Метрики могут быть созданы и зарегистрированы следующим образом:


Создание и регистрация метрик


lazy_static! {
    pub static ref HTTP_REQUESTS_TOTAL: IntCounterVec = register_int_counter_vec!(
        opts!("http_requests_total", "HTTP requests total"),
        &["method", "path"]
    )
    .expect("Can't create a metric");
    pub static ref HTTP_CONNECTED_SSE_CLIENTS: IntGauge =
        register_int_gauge!(opts!("http_connected_sse_clients", "Connected SSE clients"))
            .expect("Can't create a metric");
    pub static ref HTTP_RESPONSE_TIME_SECONDS: HistogramVec = register_histogram_vec!(
        "http_response_time_seconds",
        "HTTP response times",
        &["method", "path"],
        HTTP_RESPONSE_TIME_CUSTOM_BUCKETS.to_vec()
    )
    .expect("Can't create a metric");
}

В примере кода выше метрики добавляются в реестр по умолчанию. Также возможно зарегистрировать их в кастомном реестре (пример).


Счётчик


Если требуется посчитать все входящие HTTP запросы, то возможно использовать тип IntCounter. Но более полезно видеть не только общее число запросов, но также некоторые дополнительные измерения, такие как path и HTTP метод. Это можно сделать с помощью IntCounterVec; метрика HTTP_REQUESTS_TOTAL этого типа используется в кастомном Actix middleware таким образом:


Использование метрики HTTP_REQUESTS_TOTAL


let request_path = req.path();
let is_registered_resource = req.resource_map().has_resource(request_path);
// this check prevents possible DoS attacks that can be done by flooding the application
// using requests to different unregistered paths. That can cause high memory consumption
// of the application and Prometheus server and also overflow Prometheus's TSDB
if is_registered_resource {
    let request_method = req.method().to_string();
    metrics::HTTP_REQUESTS_TOTAL
        .with_label_values(&[&request_method, request_path])
        .inc();
}

После того, как вы сделаете несколько запросов к API, появится что-то похожее на:


Выходные данные метрики HTTP_REQUESTS_TOTAL


# HELP http_requests_total HTTP requests total
# TYPE http_requests_total counter
http_requests_total{method="GET",path="/"} 1
http_requests_total{method="GET",path="/events"} 1
http_requests_total{method="GET",path="/metrics"} 22
http_requests_total{method="GET",path="/planets"} 20634

Каждая выборка метрики содержит лейблы (атрибуты метрики) method и path, что позволяет Prometheus серверу различать выборки.


Как показано в фрагменте выше, запросы к GET /metrics (эндпоинт, с которого Prometheus сервер собирает метрики приложения) также учитываются.


Датчик


Датчик отличается от счётчика тем, что его значение может уменьшаться. Пример датчика показывает сколько клиентов в настоящий момент подключены с помощью SSE. Используется следующим образом:


Использование метрики HTTP_CONNECTED_SSE_CLIENTS


crate::metrics::HTTP_CONNECTED_SSE_CLIENTS.inc();

crate::metrics::HTTP_CONNECTED_SSE_CLIENTS.set(broadcaster_mutex.clients.len() as i64)

При переходе на http://localhost:9000 в браузере будет установлено SSE соединение, что инкрементирует значение метрики. После этого выходные данные станут такими:


Выходные данные метрики HTTP_CONNECTED_SSE_CLIENTS


# HELP http_connected_sse_clients Connected SSE clients
# TYPE http_connected_sse_clients gauge
http_connected_sse_clients 1

Broadcaster


Для имплементации датчика числа SSE клиентов потребовался рефакторинг кода приложения и реализация broadcaster'а. Он сохраняет всех подключённых (с помощью функции sse) клиентов в вектор и периодически их пингует (с помощью функции remove_stale_clients), чтобы убедиться, что соединение по-прежнему активно, в противном случае удаляет отсоединённых клиентов из вектора. Broadcaster позволяет открывать всего лишь одно Redis Pub/Sub соединение; сообщения из него отправляются (broadcasted) всем клиентам.


Гистограмма


В этом гайде гистограмма используется для сбора данных о времени ответа. Как и в случае со счётчиком запросов, трекинг осуществляется в Actix middleware; это реализует следущий код:


Трекинг времени ответа


...

histogram_timer = Some(
    metrics::HTTP_RESPONSE_TIME_SECONDS
        .with_label_values(&[&request_method, request_path])
        .start_timer(),
);

...

if let Some(histogram_timer) = histogram_timer {
    histogram_timer.observe_duration();
};

Полагаю, этот способ не очень точный (вопрос в том, насколько измеренное время ответа меньше реального), но тем не менее данные наблюдений будут полезны в качестве примера гистограммы и для её дальнейшей визуализации в Grafana.


Гистограмма принимает результаты наблюдений и подсчитывает их количество в конфигурируемых bucket'ах (есть bucket'ы по умолчанию, но скорее всего вам потребуется определить кастомные bucket'ы, подходящие к вашему use case); чтобы их сконфигурировать, было бы неплохо знать примерное рспределение значений определённой метрики. В этом приложении время ответа невелико, поэтому используется следующая конфигурация:


Bucket'ы для метрики времени ответа


const HTTP_RESPONSE_TIME_CUSTOM_BUCKETS: &[f64; 14] = &[
    0.0005, 0.0008, 0.00085, 0.0009, 0.00095, 0.001, 0.00105, 0.0011, 0.00115, 0.0012, 0.0015,
    0.002, 0.003, 1.0,
];

Выходные данные будут выглядеть примерно так (показана только часть данных):


Выходные данные метрики HTTP_RESPONSE_TIME_SECONDS


# HELP http_response_time_seconds HTTP response times
# TYPE http_response_time_seconds histogram
http_response_time_seconds_bucket{method="GET",path="/planets",le="0.0005"} 0
http_response_time_seconds_bucket{method="GET",path="/planets",le="0.0008"} 6
http_response_time_seconds_bucket{method="GET",path="/planets",le="0.00085"} 1307
http_response_time_seconds_bucket{method="GET",path="/planets",le="0.0009"} 10848
http_response_time_seconds_bucket{method="GET",path="/planets",le="0.00095"} 22334
http_response_time_seconds_bucket{method="GET",path="/planets",le="0.001"} 31698
http_response_time_seconds_bucket{method="GET",path="/planets",le="0.00105"} 38973
http_response_time_seconds_bucket{method="GET",path="/planets",le="0.0011"} 44619
http_response_time_seconds_bucket{method="GET",path="/planets",le="0.00115"} 48707
http_response_time_seconds_bucket{method="GET",path="/planets",le="0.0012"} 51495
http_response_time_seconds_bucket{method="GET",path="/planets",le="0.0015"} 57066
http_response_time_seconds_bucket{method="GET",path="/planets",le="0.002"} 59542
http_response_time_seconds_bucket{method="GET",path="/planets",le="0.003"} 60532
http_response_time_seconds_bucket{method="GET",path="/planets",le="1"} 60901
http_response_time_seconds_bucket{method="GET",path="/planets",le="+Inf"} 60901
http_response_time_seconds_sum{method="GET",path="/planets"} 66.43133770000004
http_response_time_seconds_count{method="GET",path="/planets"} 60901

Данные показывают число наблюдений, попавших в определённые bucket'ы. Также предоставляются данные по общему числу и сумме наблюдений.


Системные метрики


process фича позволяет экспортировать метрики процесса, такие как использование процессора или памяти. Для этого нужно указать фичу в Cargo.toml. После этого вы получите что-то вроде:


Выходные данные метрик процесса


# HELP process_cpu_seconds_total Total user and system CPU time spent in seconds.
# TYPE process_cpu_seconds_total counter
process_cpu_seconds_total 134.49
# HELP process_max_fds Maximum number of open file descriptors.
# TYPE process_max_fds gauge
process_max_fds 1048576
# HELP process_open_fds Number of open file descriptors.
# TYPE process_open_fds gauge
process_open_fds 37
# HELP process_resident_memory_bytes Resident memory size in bytes.
# TYPE process_resident_memory_bytes gauge
process_resident_memory_bytes 15601664
# HELP process_start_time_seconds Start time of the process since unix epoch in seconds.
# TYPE process_start_time_seconds gauge
process_start_time_seconds 1636309802.38
# HELP process_threads Number of OS threads in the process.
# TYPE process_threads gauge
process_threads 6
# HELP process_virtual_memory_bytes Virtual memory size in bytes.
# TYPE process_virtual_memory_bytes gauge
process_virtual_memory_bytes 439435264

Обратите внимание, что крейт prometheus поддерживает экспорт метрик процесса в приложениях, запущенных на Linux (например, в таком Docker контейнере).


Эндпоинт для отдачи метрик


Actix сконфигурирован так, чтобы обрабатывать запрос к GET /metrics с помощью следующего хэндлера:


Хэндлер для метрик


pub async fn metrics() -> Result<HttpResponse, CustomError> {
    let encoder = TextEncoder::new();
    let mut buffer = vec![];
    encoder
        .encode(&prometheus::gather(), &mut buffer)
        .expect("Failed to encode metrics");

    let response = String::from_utf8(buffer.clone()).expect("Failed to convert bytes to string");
    buffer.clear();

    Ok(HttpResponse::Ok()
        .insert_header(header::ContentType(mime::TEXT_PLAIN))
        .body(response))
}

Теперь, после успешного конфигуриования приложения, вы можете получить все ранее описанные метрики выполнив запрос GET http://localhost:9000/metrics. Этот эндпоинт используется Prometheus сервером для получения метрик приложения.


Метрики отдаются в простом текстовом формате.


Настройка Prometheus для сбора метрик


Prometheus собирает метрики используя следующий конфиг:


Конфиг Prometheus для сбора метрик


scrape_configs:

  - job_name: mongodb_redis_web_app
    scrape_interval: 5s
    static_configs:
      - targets: ['host.docker.internal:9000']

  - job_name: cadvisor
    scrape_interval: 5s
    static_configs:
      - targets: ['cadvisor:8080']

В конфиге определены две job'ы. Первая собирает ранее описанные метрики приложения, вторая — метрики использования ресурсов и производительности запущенных контейнеров (это будет подробно рассмотрено в разделе, описывающем использование cAdvisor). scrape_interval указывает как часто забирать данные с target. Параметр metrics_path не указан, поэтому Prometheus ожидает, что метрики будут доступны на target'ах по пути /metrics.


Expression browser и графический интерфейс


Для использования встроенного Prometheus expression browser перейдите на http://localhost:9090/graph и попробуйте запросить любую из ранее описанных метрик, например, http_requests_total. Используйте вкладку "Graph" для визуализации данных.


PromQL позволяет делать более сложные запросы; рассмотрим пару примеров.


  • вернуть все временные ряды для метрики http_requests_total и заданной job'ы:


    http_requests_total{job="mongodb_redis_web_app"}


    Лейблы job и instance автоматически добавляются к собираемым Prometheus сервером временным рядам.


  • вернуть с помощью функции rate число запросов в секунду на основании измерений в последние 5 минут:


    rate(http_requests_total[5m])



Вы можете найти больше примеров здесь.


Настройка Grafana для визуализации метрик


В этом проекте Grafana сконфигурирована с помощью следующих параметров:


  • источники данных (откуда Grafana будет запрашивать данные)


    Конфиг источников данных для Grafana


    apiVersion: 1
    
    datasources:
      - name: Prometheus
        type: prometheus
        access: proxy
        url: prometheus:9090
        isDefault: true

  • провайдер дашбордов (откуда Grafana загрузит дашборды)


    Конфиг дашбордов для Grafana


    apiVersion: 1
    
    providers:
      - name: 'default'
        folder: 'default'
        type: file
        allowUiUpdates: true
        updateIntervalSeconds: 30
        options:
          path: /etc/grafana/provisioning/dashboards
          foldersFromFilesStructure: true


После запуска проекта с помощью файла Docker Compose вы можете перейти на http://localhost:3000/, войти с помощью admin/admin и найти дашборд webapp_metrics. Некоторое время спустя он будет выглядеть примерно так:


grafana


Дашборд показывает состояние приложения под простым нагрузочным тестом. (Если вы запустите какой-нибудь нагрузочный тест, то для большей наглядности графиков (особенно гистограммы) вам нужно будет отключить ограничение MAX_REQUESTS_PER_MINUTE, например, путём резкого увеличения этого числа.)


Для визуализации данных в дашборде используются PromQL запросы, включающие ранее показанные метрики, например:


  • rate(http_response_time_seconds_sum[5m]) / rate(http_response_time_seconds_count[5m])


    Показать среднее время ответа за последние 5 минут


  • sum(increase(http_response_time_seconds_bucket{path="/planets"}[30s])) by (le)


    Используется для визуализации распределения времени ответа в форме тепловой карты. Тепловая карта похожа на гистограмму, но во времени; каждый интервал времени представляет собой отдельную гистограмму:


  • rate(process_cpu_seconds_total{job="mongodb_redis_web_app"}[1m]), sum(rate(container_cpu_usage_seconds_total{name='mongodb-redis'}[1m])) by (name)


    Показывает использование процессора за последние 5 минут. Запрашиваемые данные приходят из двух источников и показывают использование ресурса процессом и контейнером соответственно. Два графика почти одинаковы. (sum используется поскольку container_cpu_usage_seconds_total предоставляет информацию по использованию каждого ядра.)



Примечание: График "Memory usage" показывает память, используемую:


  • процессом (process_resident_memory_bytes{job="mongodb_redis_web_app"} / 1024 / 1024)
  • контейнером (container_memory_usage_bytes{name="mongodb-redis"} / 1024 / 1024)

По неизвестной мне причине график показывает, что процесс потребляет намного больше памяти, чем весь контейнер. Я создал issue по данному вопросу. Напишите, если что-то не так в этом сравнении или знаете, как это объясняется.


Мониторинг метрик контейнера приложения с помощью cAdvisor


В дополнение к системным метрикам процесса (было показано ранее) также могут быть экспортированы системные метрики Docker контейнера приложения. Это можно сделать с помощью cAdvisor.


Веб-интерфейс cAdvisor доступен по http://localhost:8080/. Все запущенные Docker контейнеры показаны на http://localhost:8080/docker/:


cadvisor docker containers


Вы можете получить информацию по использованию ресурсов любым контейнером:


cadvisor container info


Метрики собираются Prometheus сервером с http://localhost:8080/metrics.


Метрики, экспортируемые cAdvisor'ом, перечислены здесь.


Системные метрики серверов могут быть экспортированы с помощью Node exporter или Windows exporter.


Настройка уведомлений с помощью правил и AlertManager


В этом проекте следующая часть конфига Prometheus отвечает за уведомления:


Конфиг Prometheus для уведомлений


rule_files:
  - 'rules.yml'

alerting:
  alertmanagers:
    - static_configs:
        - targets: ['alertmanager:9093']

В разделе alerting определён инстанс AlertManager, с которым взаимодействет Prometheus сервер.


Правила оповещений позволяют определить условия на основе PromQL выражений:


Пример правила оповещения в rules.yml


groups:
- name: default
  rules:
  - alert: SseClients
    expr:  http_connected_sse_clients > 0
    for: 1m
    labels:
      severity: high
    annotations:
      summary: Too many SSE clients

  • alert – название оповещения
  • expr – определение правила в виде Prometheus выражения
  • for – как долго правило должно быть нарушено перед отправкой оповещения. В нашем случае если число SSE клиентов будет больше 0 в течение 1 минуты, то будет отправлено оповещение
  • labels – дополнительная информация, которая может быть добавлена к оповещению, например, уровень серьёзности проблемы
  • annotations – дополнительные сведения, которые могут быть добавлены к оповещению

Указанное правило, что число SSE клиентов больше 0, — это не то, что вы обычно настраиваете в приложении. Оно используется в качестве примера только потому, что позволяет легко его нарушить с помощью всего лишь одного запроса.


Если правило нарушено, Prometheus сервер отправит оповещение на инстанс AlertManager, который предоставляет множество фич, таких как дедупликация оповещений, группировка, отключение и роутинг уведомлений конечного пользователя. Здесь мы рассмотрим только роутинг: уведомление будлет перенаправлено на email.


AlertManager сконфигурирован так:


Конфиг AlertManager


route:
  receiver: gmail

receivers:
- name: gmail
  email_configs:
  - to: recipient@gmail.com
    from: email_id@gmail.com
    smarthost: smtp.gmail.com:587
    auth_username: email_id@gmail.com
    auth_identity: email_id@gmail.com
    auth_password: password

В этом проекте AlertManager сконфигурирован с помощью Gmail аккаунта. Чтобы сгенерировать app password, вы можете использовать этот гайд.


Чтобы правило оповещения SseClients сработало, вам нужно перейти на http://localhost:9000 в браузере. Это увеличит значение метрики http_connected_sse_clients на 1. Вы можете видеть изменения статуса оповещения SseClients на http://localhost:9090/alerts. После срабатывания оповещение перейдёт в статус Pending. По прошествии интервала for, определённого в rules.yml (в нашем случае это 1 минута), оповещение перейдёт в статус Firing.


prometheus alert


Это приведёт к тому, что Prometheus сервер отправит оповещение в AlertManager, который определит что с ним делать. В нашем случае будет отправлен email:


gmail alert


Мониторинг third-party систем с помощью Prometheus exporters


Для third-party тулов, таких как MongoDB, Redis и многих других, есть возможность настроить мониторинг с помощью Prometheus exporters.


Запуск


docker compose up --build


Заключение


В этой статье было показано как настроить отдачу метрик веб-приложения на Rust, их сбор Prometheus'ом и визуализацию данных с помощью Grafana. Также было показано как начать работу с cAdvisor для сбора метрик контейнеров и с нотификациями с помощью AlertManager. Не стесняйтесь написать мне, если нашли какие-либо ошибки в статье или исходном коде. Спасибо за внимание!


Полезные ссылки


Комментарии (10)


  1. gecube
    12.01.2022 20:18
    +3

    Спасибо. Очень познавательно. Интересно было именно посмотреть на интеграцию мониторинга с Rust.

    Касательно развертывания через докер компоуз - спорно, интереснее было бы на каком-нибудь minikube показать. Даром, что многие уже отъехали в кубер. cadvisor туда же - он попросту в таком сетапе становится не нужен. Если считаете, что миникуб ту мач - есть k3s, microk8s. В конце-концов на docker desktop кубернетес тоже включается одной галочкой (!)

    По неизвестной мне причине график показывает, что процесс потребляет намного больше памяти, чем весь контейнер

    это логично, потому что эти метрики считают разные типы памяти... Т.е. это не является багом, а скорее фичей. Хоть и весьма неочевидной и очень раздражающей. Но это больше вопросы в целом к линуксу и его аккаунтингу памяти...


    1. not_bad Автор
      13.01.2022 09:32
      +1

      Спасибо.

      cadvisor туда же - он попросту в таком сетапе становится не нужен.

      При деплое в k8s что-то другое используется для мониторинга контейнеров?

      это логично, потому что эти метрики считают разные типы памяти...

      Да, к сожалению пока не нашёл такой вариант метрик памяти контейнера и приложения, который измеряет один и тот же тип памяти и при этом корректно отображается на графике


      1. gecube
        13.01.2022 13:51

        При деплое в k8s что-то другое используется для мониторинга контейнеров?

        kubelet экспоузит метрики аналогичные cadvisor

        потом это точно так же собирается https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack


  1. wadik69
    12.01.2022 20:33

    Спасибо за статью! А не подскажите, сколько у Вас cadvisor ресурсов CPU потребляет? У меня доходит порою до 100%(


    1. not_bad Автор
      13.01.2022 09:01

      Не сталкивался с проблемой высокого потребления CPU, но нашёл issue, где также есть варианты решения


  1. mouze1976
    13.01.2022 08:44

    Спасибо за статью, подскажите почему не использовали zabbix?


    1. not_bad Автор
      13.01.2022 09:02

      Интересен был именно такой набор инструментов, возможно потому, что в работе не сталкивался с Zabbix


    1. gecube
      13.01.2022 13:52
      +1

      кратко - заббикс прошлый век, плохо масштабируется.

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


  1. mouze1976
    14.01.2022 06:17

    Спасибо за ответы! Единственное не согласен про плохое масштабирование. Возможно предложенный подход более оптимален для данной задачи, не готов оценить.


    1. denaspireone
      14.01.2022 13:37

      +1 за плохое масштабирование и убогость api
      К тому же, если вы начнёте собирать метрики с деплоев в k8s - забикс просто начнёт упираться в полку по ресурсам в сравнении с теми же специлизированными решениями для time series(prometheus, victoriametrics) и конечно же? оверхед по необходимым ресурсам: к примеру для обработки запросов за очень большие промежутки времени.

      Лучше посмотрите в сторону https://gitlab.com/mikler/glaber/-/wikis/home