
Мониторинг доступности сайта на 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, то есть без прав системного администратора.
Самопроверка
Проверьте, что материал усвоен
Ответьте на все вопросы. Результат сохранится только в этом браузере и будет учтён в статистике прочитанных материалов.
Разбираем коротко
Частые вопросы
Почему скрипт выполняет одну проверку и завершается?
Повторение, расписание и политика перезапуска надёжнее задаются cron или systemd timer. Каждый запуск получает отдельный код выхода и запись журнала.
Достаточно ли статуса 200 для проверки сайта?
Не всегда. Для важной страницы дополнительно проверяют конечный URL, тип ответа и небольшой ожидаемый маркер в теле, не загружая документ без ограничения.
Какой код выхода означает успех?
В учебном инструменте 0 означает успешную проверку, 1 — сетевой или HTTP-сбой, 2 — некорректную конфигурацию или аргументы.


