Prometheus

Данное практическое занятие посвящено знакомству с инструментами мониторинга prometheus и grafana.

Vagrant

Для работы будем использовать следующий Vagrantfile:

Vagrant.configure("2") do |config|
  config.vm.define "prometheus" do |c|
    c.vm.box = "ubuntu/lunar64"
    c.vm.hostname = "prometheus"
    c.vm.network "forwarded_port", guest: 8888, host: 8888
    c.vm.network "forwarded_port", guest: 8889, host: 8889
    c.vm.provision "shell", inline: <<-SHELL
      apt-get update -q
      apt-get install -yq docker.io docker-compose-v2
      chmod o+rw /var/run/docker.sock
    SHELL
  end
end

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

Prometheus

Для развертывания prometheus определим для него файл конфигурации prometheus.yml:

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'prometheus'
    scrape_interval: 1m
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'node'
    static_configs:
      - targets: ['node-exporter:9100']

Здесь мы в глобальной конфигурации задает частоту сбора метрик с объектов мониторинга, а также в scrape_configs определяем отдельные конфигурации для самих объектов. В качестве объектов мониторинга у нас будут выступать сам сервер prometheus и дополнительный экспортер метрик о состоянии виртуальной машины - node-exporter.

Также зададим конфигурацию compose.yaml для развертывания данных компонентов:

name: mon

services:
  prometheus:
    image: prom/prometheus:v2.50.1
    ports:
      - 8889:9090
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
  node-exporter:
    image: prom/node-exporter:v1.7.0
    command:
      - '--path.procfs=/host/proc'
      - '--path.rootfs=/rootfs'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro

volumes:
  prometheus_data: {}

После чего запустим docker compose up:

$ docker compose up -d
[+] Running 4/4
 ✔ Network mon_default            Created                                   0.0s
 ✔ Volume "mon_prometheus_data"   Created                                   0.0s
 ✔ Container mon-prometheus-1     Started                                   0.3s
 ✔ Container mon-node-exporter-1  Started                                   0.3s

После запуска по адресу localhost:8889 будет доступен веб интерфейс:

Список объектов мониторинга можно увидеть на странице localhost:8889/targets:

Чтобы ознакомиться со списком доступных метрик можно нажать на кнопку metrics explorer слева от кнопки Execute:

Как видно, список довольно большой. Выберем метрику go_info, которая выдает информацию о используемой версии golang при сборке:

В качестве языка запросов используется PromQL, который позволяет производить различные выборки по временным рядам. Например, для просмотра свободного места на файловой системе в корневом разделе можно воспользоваться запросом node_filesystem_avail_bytes{mountpoint="/"}:

Обычно метрики в базе хранятся в системе СИ и данные о файловой системе хранятся в байтах, так что требуются дополнительные преобразования для вывода значений в ГБ. Сравним эти значения с выводом утилиты df:

$ df -hT /
Filesystem     Type  Size  Used Avail Use% Mounted on
/dev/sda1      ext4   39G  4.4G   35G  12% /

Запишем 5ГБ данных и проверим результат:

$ dd if=/dev/zero of=big_file bs=1M count=5120
5120+0 records in
5120+0 records out
5368709120 bytes (5.4 GB, 5.0 GiB) copied, 5.36085 s, 1.0 GB/s
$ df -hT /
Filesystem     Type  Size  Used Avail Use% Mounted on
/dev/sda1      ext4   39G  9.4G   30G  25% /

После чего можем удалить файл:

$ rm big_file

Спустя некоторое время на вкладке Graph можем увидеть процесс изменения места на файловой системе:

Grafana

Добавим дополнительное средство визуализации метрик в нашу инсталляцию - grafana. Для этого дополним compose.yaml:

name: mon

services:
  prometheus:
    image: prom/prometheus:v2.50.1
    ports:
      - 8889:9090
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
  grafana:
    image: grafana/grafana:10.4.0
    ports:
      - 8888:3000
  node-exporter:
    image: prom/node-exporter:v1.7.0
    command:
      - '--path.procfs=/host/proc'
      - '--path.rootfs=/rootfs'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro

volumes:
  prometheus_data: {}
$ docker compose up -d
[+] Running 3/3
 ✔ Container mon-grafana-1        Started                                   0.4s
 ✔ Container mon-node-exporter-1  Running                                   0.0s
 ✔ Container mon-prometheus-1     Running                                   0.0s

После запуска grafana будет доступна по адресу localhost:8888, где в ней можно авторизоваться с использованием стандартной пары логин и пароль - admin/admin. После авторизации нам понадобится добавить наш prometheus в качестве источника данных в grafana, для этого необходимо перейти в раздел connections/datasources и нажать кнопку Add data source, после чего выбрать тип Prometheus и заполнить адрес http://prometheus:9090:

В конце нажав кнопку Save & test:

После чего все метрики из prometheus будут доступны в grafana. Посмотреть доступные метрики можно на странице explore:
Либо используя режим builder

Либо используя режим code указывая запрос на языке promql

Dashboard

Также для визуализации можно создать дашборд на странице dashboards. Добавим новую визуализацию, в которой зададим запрос на promql для отображения графика по изменению свободного места на файловой системе. Зададим заголовок, Unit в котором хранится метрика, а также можем задать custom легенду для указания на дашборде. После чего сохраним нажав Apply.

Добавим новую визуализацию для отображения графика по потреблению CPU, для этого воспользуемся метрикой node_cpu_seconds_total, которая считает время проведенное процессором в каждом режиме для каждого ядра. Таким образом общий счетчик времени в режиме бездействия можно посмотреть запросом node_cpu_seconds_total{mode="idle"}, а для процентного отображения можно воспользоваться функцией rate, которая покажет насколько увеличился счетчик за одну секунду в заданный период. В итоге мы можем с помощью запроса 1-rate(node_cpu_seconds_total{mode="idle"}[1m]) увидеть процентное потребление по каждому ядру процессора.

Добавим также визуализация для потребления оперативной памяти добавив в нее два запроса: общее количество памяти на виртуальной машине - node_memory_MemTotal_bytes и количество потребляемой памяти - node_memory_MemTotal_bytes-node_memory_MemAvailable_bytes.

В итоге получим следующий дашборд:

После чего можно сохранить дашборд нажав на иконку дискеты.

App metrics

Сделаем простое приложение, которое будет принимать http запросы и с некоторой вероятностью возвращать ошибку, а также будет отдавать метрики в формате prometheus. Пример на golang может быть следующим в main.go:

package main

import (
        "math/rand"
        "net/http"

        "github.com/prometheus/client_golang/prometheus"
        "github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
        reqTotal := prometheus.NewCounterVec(
                prometheus.CounterOpts{Name: "app_req_total"},
                []string{"code"},
        )
        prometheus.MustRegister(reqTotal)
        http.Handle("/metrics", promhttp.Handler())
        http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                if rand.Intn(10) > 0 {
                        w.WriteHeader(200)
                        w.Write([]byte("OK\n"))
                        reqTotal.WithLabelValues("200").Inc()

                        return
                }

                w.WriteHeader(500)
                w.Write([]byte("NE OK\n"))
                reqTotal.WithLabelValues("500").Inc()
        }))
        http.ListenAndServe(":8080", nil)
}

Также добавим Dockerfile для сборки:

FROM golang:1.21 as build

WORKDIR /src

COPY main.go /src/main.go
RUN go mod init example \
  && go mod tidy \
  && CGO_ENABLED=0 go build -o /bin/app ./main.go

FROM scratch
COPY --from=build /bin/app /app
CMD ["/app"]

И добавим приложение в compose.yaml:

name: mon

services:
  prometheus:
    image: prom/prometheus:v2.50.1
    ports:
      - 8889:9090
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
  grafana:
    image: grafana/grafana:10.4.0
    ports:
      - 8888:3000
  node-exporter:
    image: prom/node-exporter:v1.7.0
    command:
      - '--path.procfs=/host/proc'
      - '--path.rootfs=/rootfs'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
  app:
    image: test
    build: .
    ports:
      - 8080:8080

volumes:
  prometheus_data: {}

А также в конфигурацию prometheus.yml:

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'prometheus'
    scrape_interval: 1m
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'node'
    static_configs:
      - targets: ['node-exporter:9100']

  - job_name: 'app'
    static_configs:
      - targets: ['app:8080']

Запустим:

$ docker restart mon-prometheus-1
mon-prometheus-1
$ docker compose up -d
[+] Running 4/4
 ✔ Container mon-app-1            Started                                   0.6s
 ✔ Container mon-grafana-1        Running                                   0.0s
 ✔ Container mon-node-exporter-1  Running                                   0.0s
 ✔ Container mon-prometheus-1     Running                                   0.0s

После чего в prometheus на странице targets можно увидеть наше приложение:

А в момент сборки увидеть потребление ресурсов на дашборде:

Обратимся к нашему приложению из терминала:

$ for i in {1..100};do curl localhost:8080;done
OK
OK
OK
OK
OK
OK
OK
OK
OK
OK
OK
OK
OK
OK
OK
OK
OK
NE OK
OK
...

Как видно приложение просто возвращает ответ 200 OK и с некоторой вероятностью ответ 500 NE OK. Для просмотра метрик необходимо обратиться к эндпоинту /metrics:

$ curl localhost:8080/metrics
# HELP app_req_total
# TYPE app_req_total counter
app_req_total{code="200"} 95
app_req_total{code="500"} 5
# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 6.7428e-05
go_gc_duration_seconds{quantile="0.25"} 0.000103126
go_gc_duration_seconds{quantile="0.5"} 0.000108896
go_gc_duration_seconds{quantile="0.75"} 0.000115438
go_gc_duration_seconds{quantile="1"} 0.000146137
go_gc_duration_seconds_sum 0.000541025
go_gc_duration_seconds_count 5
# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
go_goroutines 7
# HELP go_info Information about the Go environment.
# TYPE go_info gauge
go_info{version="go1.21.8"} 1
# HELP go_memstats_alloc_bytes Number of bytes allocated and still in use.
# TYPE go_memstats_alloc_bytes gauge
go_memstats_alloc_bytes 2.073528e+06
# HELP go_memstats_alloc_bytes_total Total number of bytes allocated, even if freed.
# TYPE go_memstats_alloc_bytes_total counter
go_memstats_alloc_bytes_total 8.224104e+06
# HELP go_memstats_buck_hash_sys_bytes Number of bytes used by the profiling bucket hash table.
# TYPE go_memstats_buck_hash_sys_bytes gauge
go_memstats_buck_hash_sys_bytes 4250
# HELP go_memstats_frees_total Total number of frees.
# TYPE go_memstats_frees_total counter
go_memstats_frees_total 36883
# HELP go_memstats_gc_sys_bytes Number of bytes used for garbage collection system metadata.
# TYPE go_memstats_gc_sys_bytes gauge
go_memstats_gc_sys_bytes 3.681824e+06
# HELP go_memstats_heap_alloc_bytes Number of heap bytes allocated and still in use.
# TYPE go_memstats_heap_alloc_bytes gauge
go_memstats_heap_alloc_bytes 2.073528e+06
# HELP go_memstats_heap_idle_bytes Number of heap bytes waiting to be used.
# TYPE go_memstats_heap_idle_bytes gauge
go_memstats_heap_idle_bytes 4.530176e+06
# HELP go_memstats_heap_inuse_bytes Number of heap bytes that are in use.
# TYPE go_memstats_heap_inuse_bytes gauge
go_memstats_heap_inuse_bytes 3.432448e+06
# HELP go_memstats_heap_objects Number of allocated objects.
# TYPE go_memstats_heap_objects gauge
go_memstats_heap_objects 1361
# HELP go_memstats_heap_released_bytes Number of heap bytes released to OS.
# TYPE go_memstats_heap_released_bytes gauge
go_memstats_heap_released_bytes 3.653632e+06
# HELP go_memstats_heap_sys_bytes Number of heap bytes obtained from system.
# TYPE go_memstats_heap_sys_bytes gauge
go_memstats_heap_sys_bytes 7.962624e+06
# HELP go_memstats_last_gc_time_seconds Number of seconds since 1970 of last garbage collection.
# TYPE go_memstats_last_gc_time_seconds gauge
go_memstats_last_gc_time_seconds 1.7102744046326103e+09
# HELP go_memstats_lookups_total Total number of pointer lookups.
# TYPE go_memstats_lookups_total counter
go_memstats_lookups_total 0
# HELP go_memstats_mallocs_total Total number of mallocs.
# TYPE go_memstats_mallocs_total counter
go_memstats_mallocs_total 38244
# HELP go_memstats_mcache_inuse_bytes Number of bytes in use by mcache structures.
# TYPE go_memstats_mcache_inuse_bytes gauge
go_memstats_mcache_inuse_bytes 2400
# HELP go_memstats_mcache_sys_bytes Number of bytes used for mcache structures obtained from system.
# TYPE go_memstats_mcache_sys_bytes gauge
go_memstats_mcache_sys_bytes 15600
# HELP go_memstats_mspan_inuse_bytes Number of bytes in use by mspan structures.
# TYPE go_memstats_mspan_inuse_bytes gauge
go_memstats_mspan_inuse_bytes 63168
# HELP go_memstats_mspan_sys_bytes Number of bytes used for mspan structures obtained from system.
# TYPE go_memstats_mspan_sys_bytes gauge
go_memstats_mspan_sys_bytes 65184
# HELP go_memstats_next_gc_bytes Number of heap bytes when next garbage collection will take place.
# TYPE go_memstats_next_gc_bytes gauge
go_memstats_next_gc_bytes 4.3168e+06
# HELP go_memstats_other_sys_bytes Number of bytes used for other system allocations.
# TYPE go_memstats_other_sys_bytes gauge
go_memstats_other_sys_bytes 401862
# HELP go_memstats_stack_inuse_bytes Number of bytes in use by the stack allocator.
# TYPE go_memstats_stack_inuse_bytes gauge
go_memstats_stack_inuse_bytes 425984
# HELP go_memstats_stack_sys_bytes Number of bytes obtained from system for stack allocator.
# TYPE go_memstats_stack_sys_bytes gauge
go_memstats_stack_sys_bytes 425984
# HELP go_memstats_sys_bytes Number of bytes obtained from system.
# TYPE go_memstats_sys_bytes gauge
go_memstats_sys_bytes 1.2557328e+07
# HELP go_threads Number of OS threads created.
# TYPE go_threads gauge
go_threads 5
# HELP process_cpu_seconds_total Total user and system CPU time spent in seconds.
# TYPE process_cpu_seconds_total counter
process_cpu_seconds_total 0.12
# HELP process_max_fds Maximum number of open file descriptors.
# TYPE process_max_fds gauge
process_max_fds 1.048576e+06
# HELP process_open_fds Number of open file descriptors.
# TYPE process_open_fds gauge
process_open_fds 9
# HELP process_resident_memory_bytes Resident memory size in bytes.
# TYPE process_resident_memory_bytes gauge
process_resident_memory_bytes 1.2488704e+07
# 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 1.7102737835e+09
# HELP process_virtual_memory_bytes Virtual memory size in bytes.
# TYPE process_virtual_memory_bytes gauge
process_virtual_memory_bytes 1.26386176e+09
# HELP process_virtual_memory_max_bytes Maximum amount of virtual memory available in bytes.
# TYPE process_virtual_memory_max_bytes gauge
process_virtual_memory_max_bytes 1.8446744073709552e+19
# HELP promhttp_metric_handler_requests_in_flight Current number of scrapes being served.
# TYPE promhttp_metric_handler_requests_in_flight gauge
promhttp_metric_handler_requests_in_flight 1
# HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.
# TYPE promhttp_metric_handler_requests_total counter
promhttp_metric_handler_requests_total{code="200"} 43
promhttp_metric_handler_requests_total{code="500"} 0
promhttp_metric_handler_requests_total{code="503"} 0

Наша метрика app_req_total показывает количество запросов в разрезе разных кодов возврата. Также библиотека добавляет набор метрик golang runtime.

Добавим визуализацию метрик приложения на наш дашборд.

Общий процент ошибок:

И количество запросов в секунду:

Сгруппировать визуализации можно добавив Row:

Запустим в цикле запросы к нашему приложению, чтобы посмотреть как изменятся графики:

$ while sleep .3;do curl localhost:8080;done
OK
OK
OK
OK
OK
OK
OK
OK
OK
OK
OK
OK
NE OK
OK
...

Спустя некоторое время посмотрим на наш дашборд:

На графиках можно заметить изменение потребления ресурсов, а также метрики нашего приложения. Попробуем увеличить частоту запросов и оставим еще на некоторое время:

$ while sleep .1;do curl localhost:8080;done
OK
OK
OK
OK
OK
OK
OK
OK
OK
OK
NE OK
OK
...

Как видно потребление CPU и rps увеличились, а процент ошибок все также в районе 10%, как и указано у нас в коде. После прерывания цикла запросов, то мы увидим падение на графиках по CPU и rps:

Таким образом с помощью prometheus и grafana можно визуализировать и отслеживать различные метрики инфраструктуры и приложений.