Prometheus с нуля: метрики, targets, exporter и первые запросы PromQL

Prometheus собирает метрики Linux-сервера и показывает графики мониторинга

Мониторинг начинается с вопроса, на который должен отвечать график. Например: приложение недоступно совсем или доступно, но возвращает ошибки? Мы создадим HTTP-сервис с управляемым ответом 500, добавим метрики и научимся различать эти состояния в Prometheus.

Результат: три контейнера в одной сети, живые targets, работающий счётчик запросов и запросы PromQL. Нужны Docker с Compose v2 и curl. HTTP-порты будут опубликованы только на 127.0.0.1. Пример не требует Grafana, облачного аккаунта или пароля от рабочего сервера.

1. Создайте приложение, которое действительно экспортирует метрики

В каталоге monitoring-lab создайте app.py. Счётчик хранится в памяти процесса и обнуляется при его перезапуске. Для обучения это удобно; для бухгалтерского учёта такие данные не подходят.

from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Lock
from urllib.parse import parse_qs, urlparse


class RequestMetrics:
    def __init__(self):
        self._counts = {200: 0, 500: 0}
        self._lock = Lock()

    def record(self, status):
        with self._lock:
            self._counts[status] += 1

    def render(self):
        with self._lock:
            rows = ['# HELP http_requests_total Processed work requests',
                    '# TYPE http_requests_total counter']
            for code, count in self._counts.items():
                rows.append(
                    f'http_requests_total{{service="demo",code="{code}"}} {count}'
                )
        return '\n'.join(rows) + '\n'


metrics = RequestMetrics()


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        request = urlparse(self.path)
        content_type = 'text/plain; charset=utf-8'
        if request.path == '/metrics':
            status, body = 200, metrics.render()
            content_type = 'text/plain; version=0.0.4; charset=utf-8'
        elif request.path == '/work':
            failed = parse_qs(request.query).get('fail') == ['1']
            status = 500 if failed else 200
            metrics.record(status)
            body = 'failed\n' if failed else 'ok\n'
        elif request.path == '/health':
            status, body = 200, 'ok\n'
        else:
            status, body = 404, 'not found\n'
        payload = body.encode('utf-8')
        self.send_response(status)
        self.send_header('Content-Type', content_type)
        self.send_header('Content-Length', str(len(payload)))
        self.end_headers()
        self.wfile.write(payload)


ThreadingHTTPServer(('0.0.0.0', 8000), Handler).serve_forever()

Класс метрик отделён от HTTP-обработчика. Lock защищает изменение счётчиков при нескольких потоках. Значения label ограничены двумя кодами; URL, email и пользовательские ID не попадают в метки. Для настоящего приложения предпочтительна официальная клиентская библиотека языка, а не собственная реализация exposition.

Где разместить проект

Для сайтов, ботов и pet-проектов можно использовать Beget: виртуальный хостинг от 420 ₽/мес., VPS от 330 ₽/мес. и тестовый период.

Перейти на Beget →

2. Настройте сбор, не перепутав адреса контейнеров

Файл prometheus.yml:

global:
  scrape_interval: 5s
  evaluation_interval: 5s
scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: ['localhost:9090']
  - job_name: demo
    static_configs:
      - targets: ['demo:8000']
  - job_name: node
    static_configs:
      - targets: ['node-exporter:9100']

Внутри контейнера localhost указывает на него самого. Поэтому Prometheus находит приложение по имени Compose-сервиса demo, а exporter — по node-exporter. Запись 127.0.0.1:8000 здесь не означала бы соседний контейнер.

Файл compose.yaml:

services:
  demo:
    image: python:3.13-slim
    user: '10001:10001'
    working_dir: /app
    command: ['python', '-u', 'app.py']
    volumes:
      - ./app.py:/app/app.py:ro
    ports:
      - '127.0.0.1:8000:8000'
  prometheus:
    image: prom/prometheus:latest
    command:
      - --config.file=/etc/prometheus/prometheus.yml
      - --storage.tsdb.path=/prometheus
      - --storage.tsdb.retention.time=2d
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus
    ports:
      - '127.0.0.1:9090:9090'
  node-exporter:
    image: prom/node-exporter:latest
    read_only: true
    cap_drop: [ALL]
volumes:
  prometheus-data:

В лаборатории latest уменьшает привязку инструкции к старому выпуску, но не обеспечивает воспроизводимость во времени. После первого успешного запуска сохраните фактические RepoDigests через docker image inspect и фиксируйте их для повторяемого стенда. В production обновление версии проходит отдельную проверку.

Граница Node Exporter: здесь он запущен без host mounts и не является полноценным мониторингом хоста. Часть его данных описывает доступное контейнеру окружение. Нельзя выдавать такие filesystem/network-метрики за полный обзор сервера. Для рабочей Linux-машины exporter устанавливают на узел либо настраивают документированный доступ к host namespaces и файловым системам. Эта лаборатория учит механизму scrape без предоставления контейнеру всего корня хоста.

3. Запустите и проверьте все три уровня

docker compose config --quiet
docker compose up -d
docker compose ps
docker compose exec prometheus promtool check config /etc/prometheus/prometheus.yml
curl --fail http://127.0.0.1:8000/health
curl --fail http://127.0.0.1:8000/metrics

В metrics должны быть две строки counter со значениями 0. Откройте http://127.0.0.1:9090/targets в браузере. После нескольких scrape у demo, node и prometheus ожидается UP. Если интерфейс вашей версии группирует targets иначе, найдите список targets через меню.

Три проверки отвечают на разные вопросы: Compose видит запущенный процесс; curl получает HTTP-ответ; Prometheus смог собрать метрики. Ни одна из них ещё не доказывает успешность пользовательского действия /work.

4. Создайте нагрузку с известным результатом

for i in $(seq 1 20); do curl -s -o /dev/null http://127.0.0.1:8000/work; done
for i in $(seq 1 5); do curl -s -o /dev/null 'http://127.0.0.1:8000/work?fail=1'; done
curl --fail http://127.0.0.1:8000/metrics

Ожидайте счётчики 20 и 5, если до этого не вызывали work. Запросы metrics не считаются пользовательскими. За эти 25 обращений доля ошибок равна 20%, но это доля конкретной тестовой пачки, а не постоянная нагрузка в секунду.

5. Выполните осмысленные запросы PromQL

В expression browser начните с:

up{job="demo"}
http_requests_total{service="demo"}
sum(rate(http_requests_total{service="demo"}[1m]))
sum(rate(http_requests_total{service="demo",code=~"5.."}[1m]))
/
sum(rate(http_requests_total{service="demo"}[1m]))

Первый запрос показывает результат последнего scrape. Второй — накопленные значения, третий — оценку запросов в секунду за окно. Последний — долю серверных ошибок в этом окне. Для rate нужны как минимум две точки, а для устойчивой оценки — достаточное окно и трафик. После запуска подождите несколько интервалов сбора.

При полном отсутствии трафика знаменатель может быть нулевым, и результат станет NaN. Не превращайте отсутствие данных в «100% успеха» автоматически. Отдельно следите за доступностью target и наличием нужной серии.

Пример системной метрики exporter:

100 * (1 - avg by (instance) (
  rate(node_cpu_seconds_total{job="node",mode="idle"}[5m])
))

Формула оценивает не-idle долю CPU, но её трактовка зависит от окружения exporter. Среднее по ядрам и короткое окно могут скрыть отдельный перегруженный CPU. Всегда указывайте единицы и границу измерения.

6. Отличите падение процесса от ошибки приложения

Выполните docker compose stop demo. После нескольких scrape up{job="demo"} станет 0, в targets будет ошибка соединения. Затем docker compose start demo восстановит процесс; счётчики приложения начнутся заново. rate рассчитан на counter resets, поэтому абсолютные значения нельзя просто вычитать между произвольными перезапусками.

Теперь оставьте приложение запущенным и вызывайте только /work?fail=1. Target останется UP, потому что endpoint metrics отвечает, но доля ошибок work вырастет. Это причина строить алерты на пользовательские сигналы, а не только на живой exporter.

Диагностика и самостоятельная работа

  • Target DOWN: проверьте имя сервиса, порт, logs и формат ответа metrics.
  • Пустой PromQL: сравните точное имя метрики и labels с содержимым /metrics.
  • Нет rate: дождитесь минимум двух scrape и создайте трафик.
  • Слишком много рядов: уберите неограниченные метки; не используйте полный URL и ID запроса.

Задание: создайте за минуту 90 успешных и 10 ошибочных запросов, затем сравните counter и rate-отношение. Объясните расхождение, если окно захватило прежние запросы. Завершите стенд через docker compose down; именованный том сохраняет историю. Удаляйте его через down -v только если учебные данные больше не нужны.

Источники: модель Prometheus, rate, границы установки Node Exporter. Журналы событий дополняют метрики; на сайте есть практикум по auditd и journald.

Хотите системно изучить программирование?

Подборка онлайн-курсов для разработчиков — Python, Java, веб и DevOps.

Смотреть курсы →

Бесплатные инструменты

Калькулятор подсетей · SSH-конфиг · Nginx · Docker Compose

Нужно настроить сервер, сайт или автоматизацию?

Разберём задачу и предложим понятный состав работ, срок и стоимость.

Узнать состав и стоимость → 8 918 532 81 08

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