173 أسطر
5.7 KiB
Python
173 أسطر
5.7 KiB
Python
"""
|
|
monitor.py — Monitoring script for mithal.space (Q5)
|
|
|
|
Collects every 60 seconds:
|
|
- Latency: HTTP request response time
|
|
- Uptime: is the site reachable? (status code)
|
|
- SSL: certificate validity + days until expiry
|
|
- DNS: DNS resolution time
|
|
- Search Response: time to send a query and get a response
|
|
|
|
Stores results in both CSV and JSON so dashboard.html can read either.
|
|
|
|
Usage:
|
|
python3 monitor.py --url https://mithal.space --interval 60
|
|
python3 monitor.py --once # single run, useful for cron/testing
|
|
"""
|
|
|
|
import argparse
|
|
import csv
|
|
import json
|
|
import socket
|
|
import ssl
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from urllib.parse import urlparse
|
|
|
|
import urllib.request
|
|
import urllib.error
|
|
|
|
CSV_FILE = Path(__file__).parent / "metrics.csv"
|
|
JSON_FILE = Path(__file__).parent / "metrics.json"
|
|
CSV_FIELDS = [
|
|
"timestamp", "up", "status_code", "latency_ms",
|
|
"dns_ms", "ssl_valid", "ssl_days_remaining",
|
|
"search_ok", "search_response_ms", "error",
|
|
]
|
|
|
|
|
|
def check_dns(hostname: str):
|
|
"""Returns DNS resolution time in ms, or None on failure."""
|
|
start = time.perf_counter()
|
|
try:
|
|
socket.gethostbyname(hostname)
|
|
return round((time.perf_counter() - start) * 1000, 2)
|
|
except socket.gaierror:
|
|
return None
|
|
|
|
|
|
def check_ssl(hostname: str, port: int = 443):
|
|
"""Returns (is_valid, days_remaining) for the site's SSL certificate."""
|
|
try:
|
|
ctx = ssl.create_default_context()
|
|
with socket.create_connection((hostname, port), timeout=5) as sock:
|
|
with ctx.wrap_socket(sock, server_hostname=hostname) as ssock:
|
|
cert = ssock.getpeercert()
|
|
expiry = datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z")
|
|
expiry = expiry.replace(tzinfo=timezone.utc)
|
|
days_remaining = (expiry - datetime.now(timezone.utc)).days
|
|
return True, days_remaining
|
|
except Exception:
|
|
return False, None
|
|
|
|
|
|
def check_http(url: str, timeout: int = 10):
|
|
"""Returns (up, status_code, latency_ms, error)."""
|
|
start = time.perf_counter()
|
|
try:
|
|
req = urllib.request.Request(url, headers={"User-Agent": "Ghaymah-SRE-Monitor/1.0"})
|
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
latency_ms = round((time.perf_counter() - start) * 1000, 2)
|
|
return True, resp.status, latency_ms, None
|
|
except urllib.error.HTTPError as e:
|
|
latency_ms = round((time.perf_counter() - start) * 1000, 2)
|
|
# site responded, just with an error status (e.g. 404/500)
|
|
return e.code < 500, e.code, latency_ms, str(e)
|
|
except Exception as e:
|
|
latency_ms = round((time.perf_counter() - start) * 1000, 2)
|
|
return False, None, latency_ms, str(e)
|
|
|
|
|
|
def check_search(base_url: str, query: str = "test", timeout: int = 10):
|
|
"""
|
|
Simulates sending a search query to the site and measuring response time.
|
|
Adjust `search_path` below to match mithal.space's actual search endpoint
|
|
once known; falls back to hitting the homepage with a query string.
|
|
"""
|
|
search_path = "/search"
|
|
url = f"{base_url.rstrip('/')}{search_path}?q={query}"
|
|
start = time.perf_counter()
|
|
try:
|
|
req = urllib.request.Request(url, headers={"User-Agent": "Ghaymah-SRE-Monitor/1.0"})
|
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
|
|
return resp.status < 400, elapsed_ms
|
|
except Exception:
|
|
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
|
|
return False, elapsed_ms
|
|
|
|
|
|
def run_check(url: str):
|
|
parsed = urlparse(url)
|
|
hostname = parsed.hostname
|
|
|
|
dns_ms = check_dns(hostname)
|
|
up, status_code, latency_ms, error = check_http(url)
|
|
ssl_valid, ssl_days = check_ssl(hostname)
|
|
search_ok, search_ms = check_search(url)
|
|
|
|
return {
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"up": up,
|
|
"status_code": status_code,
|
|
"latency_ms": latency_ms,
|
|
"dns_ms": dns_ms,
|
|
"ssl_valid": ssl_valid,
|
|
"ssl_days_remaining": ssl_days,
|
|
"search_ok": search_ok,
|
|
"search_response_ms": search_ms,
|
|
"error": error,
|
|
}
|
|
|
|
|
|
def append_csv(row: dict):
|
|
is_new = not CSV_FILE.exists()
|
|
with open(CSV_FILE, "a", newline="") as f:
|
|
writer = csv.DictWriter(f, fieldnames=CSV_FIELDS)
|
|
if is_new:
|
|
writer.writeheader()
|
|
writer.writerow(row)
|
|
|
|
|
|
def append_json(row: dict, keep_last: int = 1440):
|
|
"""Keeps a rolling window of the last N checks (1440 = 24h at 1/min)."""
|
|
data = []
|
|
if JSON_FILE.exists():
|
|
try:
|
|
data = json.loads(JSON_FILE.read_text())
|
|
except json.JSONDecodeError:
|
|
data = []
|
|
data.append(row)
|
|
data = data[-keep_last:]
|
|
JSON_FILE.write_text(json.dumps(data, indent=2))
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Monitor mithal.space")
|
|
parser.add_argument("--url", default="https://mithal.space")
|
|
parser.add_argument("--interval", type=int, default=60, help="seconds between checks")
|
|
parser.add_argument("--once", action="store_true", help="run a single check and exit")
|
|
args = parser.parse_args()
|
|
|
|
while True:
|
|
result = run_check(args.url)
|
|
append_csv(result)
|
|
append_json(result)
|
|
|
|
status = "UP" if result["up"] else "DOWN"
|
|
print(
|
|
f"[{result['timestamp']}] {status} | "
|
|
f"latency={result['latency_ms']}ms | dns={result['dns_ms']}ms | "
|
|
f"ssl_days_left={result['ssl_days_remaining']} | "
|
|
f"search={result['search_response_ms']}ms"
|
|
)
|
|
|
|
if args.once:
|
|
break
|
|
time.sleep(args.interval)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|