# plugins/http/scheduler.py from __future__ import annotations import asyncio import json import logging from datetime import datetime, timezone, timedelta from fabledscryer.core.scheduler import ScheduledTask from fabledscryer.core.alerts import record_metric logger = logging.getLogger(__name__) def make_task(app) -> ScheduledTask: interval = int( app.config["PLUGINS"]["http"].get("check_interval_seconds", 60) ) async def check(): await _do_checks(app) return ScheduledTask( name="http_check", coro_factory=check, interval_seconds=interval, run_on_startup=True, ) async def _do_checks(app) -> None: from sqlalchemy import select from .models import HttpMonitor, HttpResult from .checker import run_check cfg = app.config["PLUGINS"]["http"] global_interval = int(cfg.get("check_interval_seconds", 60)) global_timeout = int(cfg.get("default_timeout_seconds", 10)) global_follow = bool(cfg.get("follow_redirects", True)) global_verify = bool(cfg.get("verify_ssl", True)) now = datetime.now(timezone.utc) async with app.db_sessionmaker() as session: result = await session.execute( select(HttpMonitor).where(HttpMonitor.enabled == True) # noqa: E712 ) monitors = list(result.scalars()) # Filter to monitors whose next check time has passed due: list[HttpMonitor] = [] for m in monitors: effective_interval = m.check_interval_seconds or global_interval if m.last_checked_at is None: due.append(m) elif (now - m.last_checked_at).total_seconds() >= effective_interval: due.append(m) if not due: return async def _check_one(monitor: HttpMonitor) -> tuple[HttpMonitor, dict]: try: headers = json.loads(monitor.headers_json or "{}") except (ValueError, TypeError): headers = {} result = await run_check( url=monitor.url, method=monitor.method, expected_status=monitor.expected_status, content_match=monitor.content_match, headers=headers, timeout_seconds=monitor.timeout_seconds or global_timeout, follow_redirects=monitor.follow_redirects if monitor.follow_redirects is not None else global_follow, verify_ssl=monitor.verify_ssl if monitor.verify_ssl is not None else global_verify, ) return monitor, result pairs = await asyncio.gather(*[_check_one(m) for m in due], return_exceptions=True) check_time = datetime.now(timezone.utc) async with app.db_sessionmaker() as session: async with session.begin(): for item in pairs: if isinstance(item, Exception): logger.error("HTTP check task error: %s", item) continue monitor, res = item session.add(HttpResult( monitor_id=monitor.id, checked_at=check_time, status_code=res["status_code"], response_ms=res["response_ms"], is_up=res["is_up"], content_matched=res["content_matched"], error_msg=res["error_msg"], tls_expires_at=res["tls_expires_at"], )) # Update monitor's last_checked_at for next-run scheduling db_monitor = await session.get(HttpMonitor, monitor.id) if db_monitor: db_monitor.last_checked_at = check_time # Alert pipeline if res["response_ms"] is not None: await record_metric( session=session, source_module="http", resource_name=monitor.name, metric_name="response_ms", value=res["response_ms"], ) await record_metric( session=session, source_module="http", resource_name=monitor.name, metric_name="is_up", value=1.0 if res["is_up"] else 0.0, )