Монитор отправляет повторяемые проверки к сайту и отделяет неуспешный результат в сигнал ошибки
Программирование

Мониторинг доступности сайта на Python: итоговый консольный инструмент

Собираем однократную HTTP-проверку с тайм-аутом, измерением времени, понятным журналом и кодами выхода для cron или systemd timer.

Содержание

Последний проект маршрута объединяет пройденные темы в один полезный инструмент. Он принимает URL и предел времени, отправляет один запрос, измеряет длительность, проверяет результат, пишет понятную строку в журнал и возвращает код выхода. Периодический запуск настраивается отдельно — скрипт не остаётся висеть бесконечным циклом.

Проверка доступности отвечает на узкий вопрос: получил ли клиент ожидаемый HTTP-ответ за допустимое время. Она не доказывает, что работают вход, корзина, база данных и все пользовательские сценарии. Для них нужны отдельные проверки.

Инструмент опирается на уроки об HTTP через urllib, argparse, logging и переменных окружения.

Определите результат одной проверки

Создайте файл monitor.py. Начните с функции, которая возвращает словарь при успешном ответе и вызывает понятное исключение при сбое:

import time
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen


def check_url(url, timeout, max_bytes=65536):
    request = Request(
        url,
        headers={"User-Agent": "site-monitor/1.0"},
        method="GET",
    )
    started = time.monotonic()

    try:
        with urlopen(request, timeout=timeout) as response:
            body = response.read(max_bytes + 1)
            final_url = response.url
            status = response.status
            content_type = response.headers.get_content_type()
    except HTTPError as error:
        raise RuntimeError(f"HTTP {error.code}") from error
    except URLError as error:
        raise RuntimeError(f"Сетевая ошибка: {error.reason}") from error

    elapsed_ms = round((time.monotonic() - started) * 1000)
    if len(body) > max_bytes:
        raise RuntimeError(f"Ответ больше {max_bytes} байт")

    return {
        "status": status,
        "elapsed_ms": elapsed_ms,
        "final_url": final_url,
        "content_type": content_type,
        "body": body,
    }

time.monotonic() используется для интервала: его значение движется только вперёд и не ломается при переводе системных часов. Функция ограничивает тело и сохраняет фактический адрес после перенаправлений.

Сейчас успешными считаются статусы, которые urlopen возвращает без HTTPError. Нужный статус и содержимое будут проверены отдельно: у разных проверяемых страниц могут быть разные ожидаемые ответы.

Добавьте параметры и устойчивые коды выхода

Дополните файл импортами и функцией, которая создаёт обработчик аргументов командной строки. Код 0 будет означать успех, 1 — неуспешную проверку, 2 — ошибку настройки:

import argparse
import logging
import os


logger = logging.getLogger(__name__)


def build_parser():
    parser = argparse.ArgumentParser(
        description="Выполняет одну проверку HTTP-адреса"
    )
    parser.add_argument("url", nargs="?", help="адрес http:// или https://")
    parser.add_argument("--timeout", type=float, default=5.0)
    parser.add_argument("--max-ms", type=int, default=3000)
    parser.add_argument("--expect", help="строка, которая должна быть в ответе")
    return parser

Позиционный URL можно передать в команде, а для планировщика — через MONITOR_URL. Секретные заголовки этот учебный монитор не поддерживает: проверка публичной страницы не должна требовать токена.

Добавьте функцию main, которая проверяет аргументы до обращения в сеть:

def main(argv=None):
    args = build_parser().parse_args(argv)
    url = args.url or os.environ.get("MONITOR_URL")

    if not url:
        logger.error("URL не указан аргументом и отсутствует MONITOR_URL")
        return 2
    if not url.startswith(("http://", "https://")):
        logger.error("Допустимы только адреса http:// и https://")
        return 2
    if args.timeout <= 0 or args.max_ms <= 0:
        logger.error("timeout и max-ms должны быть больше нуля")
        return 2

    try:
        result = check_url(url, args.timeout)
    except RuntimeError as error:
        logger.error("FAIL url=%s reason=%s", url, error)
        return 1

    if result["status"] != 200:
        logger.error("FAIL url=%s status=%s", url, result["status"])
        return 1
    if result["elapsed_ms"] > args.max_ms:
        logger.error(
            "FAIL url=%s elapsed_ms=%s limit_ms=%s",
            url,
            result["elapsed_ms"],
            args.max_ms,
        )
        return 1

    return validate_content(url, result, args.expect)

Функция validate_content появится в следующем разделе. До её добавления файл ещё не готов к запуску. Обратите внимание: журнал содержит URL и причину, но не тело ответа и не переменные окружения.

Проверьте конечный адрес, тип и маркер

Страница входа может вернуть статус 200 с текстом ошибки, а сломанная конфигурация — перенаправить на чужой домен. Добавьте разбор URL и проверку тела:

from urllib.parse import urlsplit


def validate_content(url, result, expected_text):
    requested_host = urlsplit(url).hostname
    final_host = urlsplit(result["final_url"]).hostname
    if requested_host != final_host:
        logger.error(
            "FAIL unexpected_redirect from=%s to=%s",
            requested_host,
            final_host,
        )
        return 1

    if result["content_type"] != "text/html":
        logger.error(
            "FAIL url=%s content_type=%s",
            url,
            result["content_type"],
        )
        return 1

    if expected_text:
        try:
            text = result["body"].decode("utf-8")
        except UnicodeDecodeError:
            logger.error("FAIL url=%s reason=invalid_utf8", url)
            return 1
        if expected_text not in text:
            logger.error("FAIL url=%s reason=marker_missing", url)
            return 1

    logger.info(
        "OK url=%s status=%s elapsed_ms=%s",
        url,
        result["status"],
        result["elapsed_ms"],
    )
    return 0

Свойство hostname содержит имя узла — доменную часть адреса без пути. Его сравнение разрешает обычное перенаправление внутри того же домена, например с / на /home, но отклоняет другой узел. Для сайта, который штатно переходит с example.com на www.example.com, перечислите допустимые имена явно вместо удаления проверки.

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

Настройте журнал и точку входа

Добавьте в конец файла конфигурацию. Она пишет одну строку в stderr, откуда cron или systemd сможет забрать её в свой журнал:

if __name__ == "__main__":
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(levelname)s %(message)s",
    )
    raise SystemExit(main())

После сохранения полного файла выполните команду из каталога проекта. Вторая строка показывает код выхода предыдущего процесса в Bash:

python monitor.py https://example.com/ --timeout 5 --max-ms 3000 --expect Example
echo $?

В Bash ожидается строка OK и код 0, если удалённый сайт доступен быстрее заданного предела. В PowerShell код последней программы смотрят через $LASTEXITCODE. Результат зависит от сети, поэтому единичный медленный запуск ещё не доказывает постоянную проблему.

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

Передайте расписание планировщику

Скрипт делает одну работу и завершает процесс. Cron или systemd timer задаёт период, сохраняет журнал, ограничивает параллельные запуски и может запустить внешний канал уведомления. Не добавляйте while True без обработки сигналов, паузы и контроля зависших экземпляров.

Перед запуском на рабочем сервере используйте абсолютный путь к Python из виртуального окружения, абсолютный путь к monitor.py и явно задайте MONITOR_URL. Сначала выполните ту же команду вручную от пользователя планировщика.

Код выхода — контракт с автоматизацией: 0 означает, что проверка прошла, 1 — сайт не ответил по заданным условиям, 2 — сама команда настроена неверно. Уведомления следует отправлять после нескольких согласованных неудач, чтобы один сетевой всплеск не создавал ложную тревогу.

Официальные справочники описывают urllib.request и time.monotonic. Маршрут завершён рабочим каркасом; следующий практический шаг — адаптировать его к одной собственной странице, добавить тесты функций и проверить запуск планировщиком без привилегий root, то есть без прав системного администратора.

Рекламное местоВаша компания здесьРазместить рекламу

Самопроверка

Проверьте, что материал усвоен

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

01Почему длительность измеряется time.monotonic?
02Кто должен задавать период запуска?
03Что означает код выхода 1 в учебном инструменте?

Разбираем коротко

Частые вопросы

Почему скрипт выполняет одну проверку и завершается?

Повторение, расписание и политика перезапуска надёжнее задаются cron или systemd timer. Каждый запуск получает отдельный код выхода и запись журнала.

Достаточно ли статуса 200 для проверки сайта?

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

Какой код выхода означает успех?

В учебном инструменте 0 означает успешную проверку, 1 — сетевой или HTTP-сбой, 2 — некорректную конфигурацию или аргументы.