Рассмотрим легковесный мониторинг Prometheus + Grafana на Docker контейнерах. У меня появилась необходимость мониторинга своих виртуальных машин расположенных на домашнем сервере

Начнем с создания docker-compose.yml файла. В нем будет перечислены все необходимые контейнеры:

  • Prometheus - система агрегации метрик
  • Grafana - визуализатор данных, к ней будет подлюкчен наш Prometheus-сервер
  • Node-exporter - контейнер считывающий данные с хост-системы и передающий в Prometheus
  • Alertmanager - менеджер уведомлений, для оповещения о возникновении проблем и их решений

Docker Compose

version: '3'
services:
  prometheus:
    container_name: prometheus
    hostname: prometheus
    image: prom/prometheus
    restart: unless-stopped
    user: root
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - ./prometheus/alert_rules.yml:/etc/prometheus/alert_rules.yml
      - ./prometheus/prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention=200h'
    ports:
      - 9090:9090

  grafana:
    container_name: grafana
    hostname: grafana
    image: grafana/grafana
    restart: unless-stopped
    user: root
    volumes:
      - ./grafana/grafana_datasources.yml:/etc/grafana/provisioning/datasources/all.yaml
      - ./grafana/grafana_config.ini:/etc/grafana/config.ini
      - ./grafana/grafana_data:/var/lib/grafana
    ports:
      - 3000:3000

  node-exporter:
    image: prom/node-exporter
    container_name: node-exporter
    restart: unless-stopped
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.ignored-mount-points'
      - "^/(sys|proc|dev|host|etc|rootfs/var/lib/docker/containers|rootfs/var/lib/docker/overlay2|rootfs/run/docker/netns|rootfs/var/lib/docker/aufs)($$|/)"
    #ports:
      #- 9100:9100

  alertmanager:
    image: prom/alertmanager
    container_name: alertmanager
    restart: unless-stopped
    volumes:
      - ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml
      - ./alertmanager/templates:/etc/alertmanager/templates
    command: --config.file=/etc/alertmanager/alertmanager.yml --log.level=debug
    #ports:
      #- 9093:9093

Рассмотрим каждый контейнер и начнем с самого Prometheus

prometheus:
    container_name: prometheus
    hostname: prometheus
    image: prom/prometheus
    restart: unless-stopped
    user: root
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - ./prometheus/alert_rules.yml:/etc/prometheus/alert_rules.yml
      - ./prometheus/prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention=200h'
    ports:
      - 9090:9090

volumes:

./prometheus/prometheus.yml - основной конфиг Prometheus

./prometheus/alert_rules.yml - настройка правил алретнов, при каких условиях алерты будут срабатывать

./prometheus/prometheus_data - данные Prometheus, по своей сути это файловая БД. Если место на хосте будет заканчитваться. Базу можно будет удалить и пересоздать контейнер заново

command:

--config.file=/etc/prometheus/prometheus.yml' - конфиг-файл, который мы пробросили в контейнер

'--storage.tsdb.retention=200h' - параметр, который будет чистить базу от метрик, у которых время жизни привышает 200 часов (~15 дней)

C Prometheus закончили, перейдем к Grafana

  grafana:
    container_name: grafana
    hostname: grafana
    image: grafana/grafana
    restart: unless-stopped
    user: root
    volumes:
      - ./grafana/grafana_datasources.yml:/etc/grafana/provisioning/datasources/all.yaml
      - ./grafana/grafana_config.ini:/etc/grafana/config.ini
      - ./grafana/grafana_data:/var/lib/grafana
    ports:
      - 3000:3000

Тут все просто. В файле grafana_datasources.yml будут перечислены данные подключения Grafana к Prometheus. Этот файл не обязателен, настройку подключения можно сделать через GUI.

grafana_config.ini - содержит основной конфиг настройки Grafana

grafana_data - данная директория будет хранить в себе остальные данные. Такие как, шаблоны алертов, плагины, SQLite базу

Перейдем к Node-exporter

  node-exporter:
    image: prom/node-exporter
    container_name: node-exporter
    restart: unless-stopped
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.ignored-mount-points'
      - "^/(sys|proc|dev|host|etc|rootfs/var/lib/docker/containers|rootfs/var/lib/docker/overlay2|rootfs/run/docker/netns|rootfs/var/lib/docker/aufs)($$|/)"
    #ports:
      #- 9100:9100

Для Node-exporter, не требуется особых настроек. Достаточно просто его запустить и указать в Prometheus конфиге. Порт был закомментирован, потому как Node-exporter и Prometheus будут находится на одном хосте, в одной виртуальной Docker сети.

mkdir -p ./prometheus/prometheus_data ./grafana/grafana_data ./alertmanager/templates

Так же, нам необходимо создать каждый файл относительно той папки, в который вы находитесь. Заполнением и разбором данных файлов мы сейчас займемся.

./prometheus/prometheus.yml

## prometheus.yml ##

# global settings
global:
  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

# Connect alertmanager
alerting:
  alertmanagers:
    - static_configs:
      - targets: ["alertmanager:9093"]

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
  - /etc/prometheus/alert_rules.yml

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: [ 'localhost:9090' ]
        labels:
          container: 'prometheus'

  - job_name: node-exporter-clients
    scrape_interval: 5s
    static_configs:
      - targets:
        - node-exporter:9100

Думаю тут все ясно, так как есть комментарии. Если есть вопросы, то есть документация: https://prometheus.io/docs/prometheus/latest/configuration/configuration/

global: - глобальные настройки

alerting: alertmanagers: - подключение Alert Manager

rule_files - подключаемые файлы с правилами и правилами алертов

scrape_configs - подключение узлов на которых установлен Node exporter

./prometheus/alert_rules.yml

Разберем файл который взаимодействует с данными Prometheus и Alertmanages. Я не смог найти, где конкретно был найдено содержимое файла. Тут просто описаны условия при которых будут прилетать алерты для node-exporter.

По ссылке на этом сайте можно найти другие правила алертов, для других приложений. https://samber.github.io/awesome-prometheus-alerts/rules.html

groups:
- name: node_exporter_alerts
  rules:
  - alert: Node down
    expr: up{job="monitoring-pi"} == 0
    for: 2m
    labels:
      severity: warning
    annotations:
      title: Node {{ $labels.instance }} is down
      description: Failed to scrape {{ $labels.job }} on {{ $labels.instance }} for more than 2 minutes. Node seems down.


  - alert: HostOutOfMemory
    expr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100 < 10
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: Host out of memory (instance {{ $labels.instance }})
      description: Node memory is filling up (< 10% left)\n  VALUE = {{ $value }}

  - alert: HostMemoryUnderMemoryPressure
    expr: rate(node_vmstat_pgmajfault[1m]) > 1000
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: Host memory under memory pressure (instance {{ $labels.instance }})
      description: The node is under heavy memory pressure. High rate of major page faults\n  VALUE = {{ $value }}

  - alert: HostUnusualNetworkThroughputIn
    expr: sum by (instance) (rate(node_network_receive_bytes_total[2m])) / 1024 / 1024 > 100
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: Host unusual network throughput in (instance {{ $labels.instance }})
      description: Host network interfaces are probably receiving too much data (> 100 MB/s)\n  VALUE = {{ $value }}

  - alert: HostUnusualNetworkThroughputOut
    expr: sum by (instance) (rate(node_network_transmit_bytes_total[2m])) / 1024 / 1024 > 100
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: Host unusual network throughput out (instance {{ $labels.instance }})
      description: Host network interfaces are probably sending too much data (> 100 MB/s)\n  VALUE = {{ $value }}

  - alert: HostUnusualDiskReadRate
    expr: sum by (instance) (rate(node_disk_read_bytes_total[2m])) / 1024 / 1024 > 50
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: Host unusual disk read rate (instance {{ $labels.instance }})
      description: Disk is probably reading too much data (> 50 MB/s)\n  VALUE = {{ $value }}

  - alert: HostUnusualDiskWriteRate
    expr: sum by (instance) (rate(node_disk_written_bytes_total[2m])) / 1024 / 1024 > 50
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: Host unusual disk write rate (instance {{ $labels.instance }})
      description: Disk is probably writing too much data (> 50 MB/s)\n  VALUE = {{ $value }}

  # Please add ignored mountpoints in node_exporter parameters like
  # "--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|run)($|/)".
  # Same rule using "node_filesystem_free_bytes" will fire when disk fills for non-root users.
  - alert: HostOutOfDiskSpace
    expr: (node_filesystem_avail_bytes * 100) / node_filesystem_size_bytes < 10 and ON (instance, device, mountpoint) node_filesystem_readonly == 0
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: Host out of disk space (instance {{ $labels.instance }})
      description: Disk is almost full (< 10% left)\n  VALUE = {{ $value }}

  # Please add ignored mountpoints in node_exporter parameters like
  # "--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|run)($|/)".
  # Same rule using "node_filesystem_free_bytes" will fire when disk fills for non-root users.
  - alert: HostDiskWillFillIn24Hours
    expr: (node_filesystem_avail_bytes * 100) / node_filesystem_size_bytes < 10 and ON (instance, device, mountpoint) predict_linear(node_filesystem_avail_bytes{fstype!~"tmpfs"}[1h], 24 * 3600) < 0 and ON (instance, device, mountpoint) node_filesystem_readonly == 0
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: Host disk will fill in 24 hours (instance {{ $labels.instance }})
      description: Filesystem is predicted to run out of space within the next 24 hours at current write rate\n  VALUE = {{ $value }}

  - alert: HostOutOfInodes
    expr: node_filesystem_files_free{mountpoint ="/rootfs"} / node_filesystem_files{mountpoint="/rootfs"} * 100 < 10 and ON (instance, device, mountpoint) node_filesystem_readonly{mountpoint="/rootfs"} == 0
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: Host out of inodes (instance {{ $labels.instance }})
      description: Disk is almost running out of available inodes (< 10% left)\n  VALUE = {{ $value }}

  - alert: HostInodesWillFillIn24Hours
    expr: node_filesystem_files_free{mountpoint ="/rootfs"} / node_filesystem_files{mountpoint="/rootfs"} * 100 < 10 and predict_linear(node_filesystem_files_free{mountpoint="/rootfs"}[1h], 24 * 3600) < 0 and ON (instance, device, mountpoint) node_filesystem_readonly{mountpoint="/rootfs"} == 0
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: Host inodes will fill in 24 hours (instance {{ $labels.instance }})
      description: Filesystem is predicted to run out of inodes within the next 24 hours at current write rate\n  VALUE = {{ $value }}

  - alert: HostUnusualDiskReadLatency
    expr: rate(node_disk_read_time_seconds_total[1m]) / rate(node_disk_reads_completed_total[1m]) > 0.1 and rate(node_disk_reads_completed_total[1m]) > 0
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: Host unusual disk read latency (instance {{ $labels.instance }})
      description: Disk latency is growing (read operations > 100ms)\n  VALUE = {{ $value }}

  - alert: HostUnusualDiskWriteLatency
    expr: rate(node_disk_write_time_seconds_totali{device!~"mmcblk.+"}[1m]) / rate(node_disk_writes_completed_total{device!~"mmcblk.+"}[1m]) > 0.1 and rate(node_disk_writes_completed_total{device!~"mmcblk.+"}[1m]) > 0
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: Host unusual disk write latency (instance {{ $labels.instance }})
      description: Disk latency is growing (write operations > 100ms)\n  VALUE = {{ $value }}

  - alert: HostHighCpuLoad
    expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[2m])) * 100) > 80
    for: 0m
    labels:
      severity: warning
    annotations:
      summary: Host high CPU load (instance {{ $labels.instance }})
      description: CPU load is > 80%\n  VALUE = {{ $value }}

  - alert: HostCpuStealNoisyNeighbor
    expr: avg by(instance) (rate(node_cpu_seconds_total{mode="steal"}[5m])) * 100 > 10
    for: 0m
    labels:
      severity: warning
    annotations:
      summary: Host CPU steal noisy neighbor (instance {{ $labels.instance }})
      description: CPU steal is > 10%. A noisy neighbor is killing VM performances or a spot instance may be out of credit.\n  VALUE = {{ $value }}

  # 1000 context switches is an arbitrary number.
  # Alert threshold depends on nature of application.
  # Please read: https://github.com/samber/awesome-prometheus-alerts/issues/58
  - alert: HostContextSwitching
    expr: (rate(node_context_switches_total[5m])) / (count without(cpu, mode) (node_cpu_seconds_total{mode="idle"})) > 1000
    for: 0m
    labels:
      severity: warning
    annotations:
      summary: Host context switching (instance {{ $labels.instance }})
      description: Context switching is growing on node (> 1000 / s)\n  VALUE = {{ $value }}

  - alert: HostSwapIsFillingUp
    expr: (1 - (node_memory_SwapFree_bytes / node_memory_SwapTotal_bytes)) * 100 > 80
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: Host swap is filling up (instance {{ $labels.instance }})
      description: Swap is filling up (>80%)\n  VALUE = {{ $value }}

  - alert: HostSystemdServiceCrashed
    expr: node_systemd_unit_state{state="failed"} == 1
    for: 0m
    labels:
      severity: warning
    annotations:
      summary: Host SystemD service crashed (instance {{ $labels.instance }})
      description: SystemD service crashed\n  VALUE = {{ $value }}

  - alert: HostPhysicalComponentTooHot
    expr: node_hwmon_temp_celsius > 75
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: Host physical component too hot (instance {{ $labels.instance }})
      description: Physical hardware component too hot\n  VALUE = {{ $value }}

  - alert: HostNodeOvertemperatureAlarm
    expr: node_hwmon_temp_crit_alarm_celsius == 1
    for: 0m
    labels:
      severity: critical
    annotations:
      summary: Host node overtemperature alarm (instance {{ $labels.instance }})
      description: Physical node temperature alarm triggered\n  VALUE = {{ $value }}

  - alert: HostRaidArrayGotInactive
    expr: node_md_state{state="inactive"} > 0
    for: 0m
    labels:
      severity: critical
    annotations:
      summary: Host RAID array got inactive (instance {{ $labels.instance }})
      description: RAID array {{ $labels.device }} is in degraded state due to one or more disks failures. Number of spare drives is insufficient to fix issue automatically.\n  VALUE = {{ $value }}

  - alert: HostRaidDiskFailure
    expr: node_md_disks{state="failed"} > 0
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: Host RAID disk failure (instance {{ $labels.instance }})
      description: At least one device in RAID array on {{ $labels.instance }} failed. Array {{ $labels.md_device }} needs attention and possibly a disk swap\n  VALUE = {{ $value }}

  - alert: HostKernelVersionDeviations
    expr: count(sum(label_replace(node_uname_info, "kernel", "$1", "release", "([0-9]+.[0-9]+.[0-9]+).*")) by (kernel)) > 1
    for: 6h
    labels:
      severity: warning
    annotations:
      summary: Host kernel version deviations (instance {{ $labels.instance }})
      description: Different kernel versions are running\n  VALUE = {{ $value }}

  - alert: HostOomKillDetected
    expr: increase(node_vmstat_oom_kill[1m]) > 0
    for: 0m
    labels:
      severity: warning
    annotations:
      summary: Host OOM kill detected (instance {{ $labels.instance }})
      description: OOM kill detected\n  VALUE = {{ $value }}

  - alert: HostEdacCorrectableErrorsDetected
    expr: increase(node_edac_correctable_errors_total[1m]) > 0
    for: 0m
    labels:
      severity: info
    annotations:
      summary: Host EDAC Correctable Errors detected (instance {{ $labels.instance }})
      description: Instance has had {{ printf "%.0f" $value }} correctable memory errors reported by EDAC in the last 5 minutes.\n  VALUE = {{ $value }}

  - alert: HostEdacUncorrectableErrorsDetected
    expr: node_edac_uncorrectable_errors_total > 0
    for: 0m
    labels:
      severity: warning
    annotations:
      summary: Host EDAC Uncorrectable Errors detected (instance {{ $labels.instance }})
      description: Instance has had {{ printf "%.0f" $value }} uncorrectable memory errors reported by EDAC in the last 5 minutes.\n  VALUE = {{ $value }}

  - alert: HostNetworkReceiveErrors
    expr: rate(node_network_receive_errs_total[2m]) / rate(node_network_receive_packets_total[2m]) > 0.01
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: Host Network Receive Errors (instance {{ $labels.instance }}:{{ $labels.device }})
      description: Instance interface has encountered {{ printf "%.0f" $value }} receive errors in the last five minutes.\n  VALUE = {{ $value }}

  - alert: HostNetworkTransmitErrors
    expr: rate(node_network_transmit_errs_total[2m]) / rate(node_network_transmit_packets_total[2m]) > 0.01
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: Host Network Transmit Errors (instance {{ $labels.instance }}:{{ $labels.device }})
      description: Instance has encountered {{ printf "%.0f" $value }} transmit errors in the last five minutes.\n  VALUE = {{ $value }}

  - alert: HostNetworkInterfaceSaturated
    expr: (rate(node_network_receive_bytes_total{device!~"^tap.*"}[1m]) + rate(node_network_transmit_bytes_total{device!~"^tap.*"}[1m])) / node_network_speed_bytes{device!~"^tap.*"} > 0.8
    for: 1m
    labels:
      severity: warning
    annotations:
      summary: Host Network Interface Saturated (instance {{ $labels.instance }}:{{ $labels.interface }})
      description: The network interface is getting overloaded.\n  VALUE = {{ $value }}

  - alert: HostConntrackLimit
    expr: node_nf_conntrack_entries / node_nf_conntrack_entries_limit > 0.8
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: Host conntrack limit (instance {{ $labels.instance }})
      description: The number of conntrack is approching limit\n  VALUE = {{ $value }}

  - alert: HostClockSkew
    expr: (node_timex_offset_seconds > 0.05 and deriv(node_timex_offset_seconds[5m]) >= 0) or (node_timex_offset_seconds < -0.05 and deriv(node_timex_offset_seconds[5m]) <= 0)
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: Host clock skew (instance {{ $labels.instance }})
      description: Clock skew detected. Clock is out of sync.\n  VALUE = {{ $value }}

  - alert: HostClockNotSynchronising
    expr: min_over_time(node_timex_sync_status[1m]) == 0 and node_timex_maxerror_seconds >= 16
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: Host clock not synchronising (instance {{ $labels.instance }})
      description: Clock not synchronising.\n  VALUE = {{ $value }}

Расписывать данный файл не вижу смысла. Но вы можете сюда добавлять свои правила при которых будут срабатывать алерты

./grafana/grafana_datasources.yml

## grafana_datasources.yml ##

apiVersion: 1

datasources:
  - name: 'prometheus'
    type: 'prometheus'
    access: 'proxy'
    url: 'http://prometheus:9090'

Тут все просто, мы указываем название подключения, тип и HTTP адрес. Повторюсь, что данный файл подключать не обязательно данную настройку можно выполнить и в самой графане.

./grafana/grafana_config.ini

## grafana_config.ini ##

[paths]
provisioning = /etc/grafana/provisioning

[server]
enable_gzip = true

Тут тоже не много настроек, если нужно больше, можно найти все в документации https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/

./alertmanager/alertmanager.yml

route:
  receiver: telegram-alert-manager
  repeat_interval: 5m
templates:
  - '/etc/alertmanager/templates/telegram.default.message'
receivers:
  - name: 'telegram-alert-manager'
    telegram_configs:
      - bot_token: 
        api_url: https://api.telegram.org
        chat_id: 
        send_resolved: true
        parse_mode: 'HTML'
        disable_notifications: false
        message: '{{ template "telegram.default.message" .}}'

В данном примере у меня настроена нотификация в моего Telegram бота. Рассмотрим нужные поля

bot_token - сюда мы должны вставить токен своего бота

chat_id: - id чата, куда бот будет доставлять сообщения

Я не слишком разбирался в теме настройки нотификации, просто нашел и скопировал из готовых решений, немного поправив параметры обращаясь к документации.

https://prometheus.io/docs/prometheus/latest/configuration/configuration/#alertmanager_config

./alertmanager/templates/telegram.default.message

{{ define "telegram.default.message" }}
{{ range .Alerts }}
{{ if eq .Status "firing"}}🔥 <b>{{ .Labels.alertname }}</b> 🔥{{ else }}✅ <b>{{ .Labels.alertname }}</b> ✅{{ end }}
<b>Labels:</b>{{ range $key, $value := .Labels }}{{ if ne $key "alertname" }}
    <b>{{ $key }}</b>: <i>{{ $value }}</i>{{ end }}{{ end }}
<b>Annotations:</b>{{ range $key, $value := .Annotations }}
    <b>{{ $key }}</b>: <i>{{ $value }}</i>{{ end }}
{{ end }}
{{ end }}

А в этом файле сформирован шаблон сообщения, которое будет перед отправкой предварительно формироваться и уже после отправлено в Телеграм бота.

Настройка серверной части завершена. Поднимем все контейнеры разом

docker compose up -d

И мы должны увидеть примерно вот такую картину (uptime kuma уже было у меня ранее установлена)

Давайте убедимся в работе Prometheus и перейдем по ссылке http://<ip>:9090/targets?search=

Обратите внимание, что данные передаются в открытом виде, без шифрования. Если у вас есть необходимость в шифровании данных, потребуется использовать либо реверс прокси, поднимать VPN или пробрасывать порты через SSH. Тут решать каждому, нужно ли ему шифрование или нет. Для локальной сети это не так важно.

Анонсы и еще больше информации в Telegram-канале