"""Uptime Kuma push heartbeats. A standard up/down monitor (e.g. Docker container running) can't tell that the bot is busy failing. These push heartbeats report real health instead: - the primary heartbeat reports UP only while Reddit fetches are succeeding; - the optional fallback heartbeat reports DOWN when OAuth is expected but the bot is on an RSS/JSON fallback, so degradation is alerted while the primary stays UP. """ import logging import os import time import requests from . import config, credentials, sources def send_kuma_heartbeat(): """Report bot health to the primary Uptime Kuma Push monitor. Reports UP only if a Reddit fetch has succeeded within KUMA_FETCH_STALE_SECONDS; otherwise DOWN. No-op unless KUMA_PUSH_URL is set. """ push_url = os.getenv('KUMA_PUSH_URL') if push_url: stale_after = int(os.getenv('KUMA_FETCH_STALE_SECONDS', '1500')) # 25 min now = time.time() last = sources.get_last_fetch_success_ts() if last is not None and (now - last) < stale_after: status, msg = 'up', f"ok (last good fetch {int(now - last)}s ago)" elif last is None: status, msg = 'down', 'no successful Reddit fetch since startup' else: status, msg = 'down', f"no successful Reddit fetch for {int(now - last)}s (Reddit blocking?)" try: requests.get(push_url, params={'status': status, 'msg': msg}, timeout=5) except requests.RequestException as e: logging.warning(f"Failed to send Uptime Kuma heartbeat: {e}") send_kuma_fallback_heartbeat() def _oauth_expected(): """True if the authenticated API is configured and in the source order, i.e. running on RSS/JSON instead is a degradation worth alerting on (not an intentional setup).""" creds = credentials.CREDENTIALS or {} has_app = bool(creds.get('reddit_client_id') and creds.get('reddit_client_secret')) return has_app and 'oauth' in config.get_source_order() def send_kuma_fallback_heartbeat(): """Report to a SECOND Uptime Kuma Push monitor that tracks API vs fallback usage. Reports DOWN when OAuth is expected but the bot is currently on RSS/JSON. No-op unless KUMA_FALLBACK_PUSH_URL is set.""" url = os.getenv('KUMA_FALLBACK_PUSH_URL') if not url: return active = sources.get_active_source() if not _oauth_expected(): status, msg = 'up', f"using {active or 'rss/json'} (by configuration)" elif active == 'oauth': status, msg = 'up', 'using authenticated API' else: status, msg = 'down', f"OAuth unavailable - on fallback source ({active or 'none'})" try: requests.get(url, params={'status': status, 'msg': msg}, timeout=5) except requests.RequestException as e: logging.warning(f"Failed to send Uptime Kuma fallback heartbeat: {e}")