151 أسطر
5.3 KiB
Python
151 أسطر
5.3 KiB
Python
"""
|
|
collector.py
|
|
يجمع مقاييس مراقبة لموقع mithal.space كل دقيقة ويخزنها في CSV و JSON:
|
|
- Latency: زمن استجابة HTTP
|
|
- Uptime: هل الموقع متاح (status code)
|
|
- SSL: حالة الشهادة وتاريخ انتهائها
|
|
- DNS: وقت تحليل DNS
|
|
- Search Response: زمن الرد على عملية بحث (query) داخل الموقع
|
|
|
|
الاستخدام:
|
|
python collector.py # يعمل باستمرار كل 60 ثانية
|
|
python collector.py --once # فحص واحد فقط (مفيد للاختبار / cron)
|
|
"""
|
|
|
|
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 requests
|
|
|
|
TARGET_URL = "https://mithal.space"
|
|
# عدّل هذا لمسار بحث فعلي في الموقع لو مختلف
|
|
SEARCH_URL = "https://mithal.space/search?q=test"
|
|
|
|
CSV_FILE = Path("metrics.csv")
|
|
JSON_FILE = Path("latest.json")
|
|
HISTORY_JSON = Path("history.json")
|
|
CHECK_INTERVAL = 60 # ثانية
|
|
HISTORY_MAX = 1440 # الاحتفاظ بآخر 24 ساعة (فحص كل دقيقة)
|
|
|
|
|
|
def check_dns(hostname: str) -> float:
|
|
"""يقيس وقت تحليل DNS بالمللي ثانية."""
|
|
start = time.perf_counter()
|
|
try:
|
|
socket.gethostbyname(hostname)
|
|
return round((time.perf_counter() - start) * 1000, 2)
|
|
except socket.gaierror:
|
|
return -1.0
|
|
|
|
|
|
def check_http(url: str) -> dict:
|
|
"""يقيس زمن الاستجابة وكود الحالة."""
|
|
try:
|
|
start = time.perf_counter()
|
|
resp = requests.get(url, timeout=10)
|
|
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
|
|
return {"status_code": resp.status_code, "latency_ms": elapsed_ms, "up": resp.status_code < 400}
|
|
except requests.RequestException as e:
|
|
return {"status_code": None, "latency_ms": -1, "up": False, "error": str(e)}
|
|
|
|
|
|
def check_ssl(hostname: str, port: int = 443) -> dict:
|
|
"""يفحص حالة شهادة SSL وعدد الأيام المتبقية على انتهائها."""
|
|
try:
|
|
ctx = ssl.create_default_context()
|
|
with socket.create_connection((hostname, port), timeout=10) 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_left = (expiry - datetime.now(timezone.utc)).days
|
|
return {"valid": True, "expires_on": expiry.isoformat(), "days_left": days_left}
|
|
except Exception as e:
|
|
return {"valid": False, "error": str(e)}
|
|
|
|
|
|
def check_search(url: str) -> dict:
|
|
"""يرسل طلب بحث ويقيس زمن الرد."""
|
|
try:
|
|
start = time.perf_counter()
|
|
resp = requests.get(url, timeout=10)
|
|
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
|
|
return {"status_code": resp.status_code, "latency_ms": elapsed_ms}
|
|
except requests.RequestException as e:
|
|
return {"status_code": None, "latency_ms": -1, "error": str(e)}
|
|
|
|
|
|
def run_check() -> dict:
|
|
hostname = urlparse(TARGET_URL).hostname
|
|
timestamp = datetime.now(timezone.utc).isoformat()
|
|
|
|
http_result = check_http(TARGET_URL)
|
|
dns_ms = check_dns(hostname)
|
|
ssl_result = check_ssl(hostname)
|
|
search_result = check_search(SEARCH_URL)
|
|
|
|
return {
|
|
"timestamp": timestamp,
|
|
"up": http_result.get("up", False),
|
|
"status_code": http_result.get("status_code"),
|
|
"latency_ms": http_result.get("latency_ms"),
|
|
"dns_ms": dns_ms,
|
|
"ssl_valid": ssl_result.get("valid"),
|
|
"ssl_days_left": ssl_result.get("days_left"),
|
|
"search_latency_ms": search_result.get("latency_ms"),
|
|
}
|
|
|
|
|
|
def append_csv(row: dict):
|
|
is_new = not CSV_FILE.exists()
|
|
with open(CSV_FILE, "a", newline="", encoding="utf-8") as f:
|
|
writer = csv.DictWriter(f, fieldnames=list(row.keys()))
|
|
if is_new:
|
|
writer.writeheader()
|
|
writer.writerow(row)
|
|
|
|
|
|
def update_json(row: dict):
|
|
# أحدث قراءة (يقرأها الـ dashboard مباشرة)
|
|
JSON_FILE.write_text(json.dumps(row, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
|
|
# سجل تاريخي محدود (لآخر 24 ساعة) يستخدمه الـ dashboard للرسم البياني والجدول
|
|
history = []
|
|
if HISTORY_JSON.exists():
|
|
try:
|
|
history = json.loads(HISTORY_JSON.read_text(encoding="utf-8"))
|
|
except json.JSONDecodeError:
|
|
history = []
|
|
history.append(row)
|
|
history = history[-HISTORY_MAX:]
|
|
HISTORY_JSON.write_text(json.dumps(history, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--once", action="store_true", help="تشغيل فحص واحد فقط ثم الخروج")
|
|
args = parser.parse_args()
|
|
|
|
while True:
|
|
row = run_check()
|
|
append_csv(row)
|
|
update_json(row)
|
|
print(f"[{row['timestamp']}] up={row['up']} latency={row['latency_ms']}ms "
|
|
f"dns={row['dns_ms']}ms ssl_days_left={row['ssl_days_left']} "
|
|
f"search={row['search_latency_ms']}ms")
|
|
|
|
if args.once:
|
|
break
|
|
time.sleep(CHECK_INTERVAL)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|