Мониторинг начинается с вопроса, на который должен отвечать график. Например: приложение недоступно совсем или доступно, но возвращает ошибки? Мы создадим 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 ₽/мес. и тестовый период.
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.
Разберём задачу и предложим понятный состав работ, срок и стоимость.
Статья может содержать партнёрские ссылки. Это не влияет на стоимость для вас, но помогает развивать блог.

