#!/usr/bin/env python3 """ Monitoring script for mithal.space Collects every 60 seconds: - Latency: HTTP response time for the homepage - Uptime: whether the site responds with a healthy status code - SSL: certificate validity and days remaining until expiry - DNS: DNS resolution time - Search response: time to get a response from the search endpoint Storage: - dashboard/data/metrics_log.csv -> full historical log (append-only) - dashboard/data/metrics_history.json -> rolling window (last 24h) used by the dashboard Only standard library is used - no extra dependencies to install. """ import csv import json import socket import ssl import time import urllib.request import urllib.error from datetime import datetime, timezone from pathlib import Path # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- TARGET_HOST = "mithal.space" TARGET_URL = "https://mithal.space/" SEARCH_URL = "https://mithal.space/search?q=test" # adjust if the real search path differs CHECK_INTERVAL_SECONDS = 60 REQUEST_TIMEOUT_SECONDS = 10 ROLLING_WINDOW_MINUTES = 24 * 60 # keep last 24 hours (1 check/minute) DATA_DIR = Path(__file__).parent / "dashboard" / "data" CSV_LOG_PATH = DATA_DIR / "metrics_log.csv" HISTORY_JSON_PATH = DATA_DIR / "metrics_history.json" CSV_FIELDS = [ "timestamp", "up", "status_code", "latency_ms", "dns_ms", "ssl_valid", "ssl_days_remaining", "ssl_expiry", "search_status", "search_latency_ms", "error", ] def now_iso(): return datetime.now(timezone.utc).isoformat() def check_dns(host): """Measure DNS resolution time in milliseconds.""" start = time.perf_counter() try: socket.getaddrinfo(host, 443) elapsed_ms = round((time.perf_counter() - start) * 1000, 2) return elapsed_ms, None except socket.gaierror as exc: elapsed_ms = round((time.perf_counter() - start) * 1000, 2) return elapsed_ms, str(exc) def check_ssl(host, port=443): """Connect to the host and read certificate expiry info.""" context = ssl.create_default_context() try: with socket.create_connection((host, port), timeout=REQUEST_TIMEOUT_SECONDS) as sock: with context.wrap_socket(sock, server_hostname=host) as ssock: cert = ssock.getpeercert() not_after = cert.get("notAfter") expiry_dt = datetime.strptime(not_after, "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc) days_remaining = (expiry_dt - datetime.now(timezone.utc)).days return { "ssl_valid": True, "ssl_days_remaining": days_remaining, "ssl_expiry": expiry_dt.isoformat(), }, None except Exception as exc: return { "ssl_valid": False, "ssl_days_remaining": None, "ssl_expiry": None, }, str(exc) def check_http(url): """Measure HTTP response time and status code.""" start = time.perf_counter() try: req = urllib.request.Request(url, headers={"User-Agent": "mithal-monitor/1.0"}) with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT_SECONDS) as resp: elapsed_ms = round((time.perf_counter() - start) * 1000, 2) return resp.getcode(), elapsed_ms, None except urllib.error.HTTPError as exc: elapsed_ms = round((time.perf_counter() - start) * 1000, 2) return exc.code, elapsed_ms, None except Exception as exc: elapsed_ms = round((time.perf_counter() - start) * 1000, 2) return None, elapsed_ms, str(exc) def run_check(): record = {"timestamp": now_iso(), "error": None} # 1) DNS resolution time dns_ms, dns_err = check_dns(TARGET_HOST) record["dns_ms"] = dns_ms # 2) SSL certificate status ssl_info, ssl_err = check_ssl(TARGET_HOST) record.update(ssl_info) # 3) HTTP latency + uptime (status code) status_code, latency_ms, http_err = check_http(TARGET_URL) record["status_code"] = status_code record["latency_ms"] = latency_ms record["up"] = status_code is not None and 200 <= status_code < 400 # 4) Search response time search_status, search_latency_ms, search_err = check_http(SEARCH_URL) record["search_status"] = search_status record["search_latency_ms"] = search_latency_ms errors = [e for e in (dns_err, ssl_err, http_err, search_err) if e] if errors: record["error"] = " | ".join(errors) return record def append_csv(record): DATA_DIR.mkdir(parents=True, exist_ok=True) file_exists = CSV_LOG_PATH.exists() with CSV_LOG_PATH.open("a", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=CSV_FIELDS) if not file_exists: writer.writeheader() writer.writerow(record) def update_history_json(record): DATA_DIR.mkdir(parents=True, exist_ok=True) history = [] if HISTORY_JSON_PATH.exists(): try: history = json.loads(HISTORY_JSON_PATH.read_text(encoding="utf-8")) except json.JSONDecodeError: history = [] history.append(record) history = history[-ROLLING_WINDOW_MINUTES:] HISTORY_JSON_PATH.write_text(json.dumps(history, indent=2, ensure_ascii=False), encoding="utf-8") def main(): print(f"[{now_iso()}] Starting monitor for {TARGET_URL} (every {CHECK_INTERVAL_SECONDS}s)") while True: record = run_check() append_csv(record) update_history_json(record) status_label = "UP" if record["up"] else "DOWN" print( f"[{record['timestamp']}] {status_label} " f"status={record['status_code']} latency={record['latency_ms']}ms " f"dns={record['dns_ms']}ms ssl_days={record['ssl_days_remaining']} " f"search={record['search_latency_ms']}ms" ) if record["error"]: print(f" -> issues: {record['error']}") time.sleep(CHECK_INTERVAL_SECONDS) if __name__ == "__main__": main()