first commit
هذا الالتزام موجود في:
11
q5-mithal-monitor/.dockerignore
Normal file
11
q5-mithal-monitor/.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
||||
.git
|
||||
.gitignore
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.env
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
README.md
|
||||
25
q5-mithal-monitor/Dockerfile
Normal file
25
q5-mithal-monitor/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies if required
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python requirements
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application files
|
||||
COPY monitor.py index.html entrypoint.sh ./
|
||||
COPY data.jsonl data.csv monitoring_data.csv ./
|
||||
|
||||
# Create symlink so dashboard.html also works without code duplication
|
||||
RUN ln -sf index.html dashboard.html && chmod +x entrypoint.sh
|
||||
|
||||
# Expose HTTP dashboard port
|
||||
EXPOSE 8080
|
||||
|
||||
# Run entrypoint script
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
60
q5-mithal-monitor/README.md
Normal file
60
q5-mithal-monitor/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# مراقب موقع mithal.space (Mithal Website Monitor & Dashboard)
|
||||
|
||||
تطبيق متكامل لمراقبة موقع `mithal.space` يجمع مقاييس الـ Uptime, Latency, SSL Certificate Expiry, DNS Resolution, و Search Latency بشكل مستمر ويعرضها على لوحة تحكم تفاعلية.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 التشغيل عبر Docker Compose (الافتراضي والمستحسن)
|
||||
|
||||
قم بتشغيل الحاوية في الخلفية مع حفظ البيانات تلقائياً على الجهاز المستضيف:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
افتخ المتصفح وانتقل إلى:
|
||||
👉 **[http://localhost:8080](http://localhost:8080)**
|
||||
|
||||
لإيقاف الخدمة:
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐳 التشغيل المباشر عبر Docker (دون Compose)
|
||||
|
||||
### 1. بناء صورة الـ Docker:
|
||||
```bash
|
||||
docker build -t mithal-monitor .
|
||||
```
|
||||
|
||||
### 2. تشغيل الحاوية مع حفظ البيانات:
|
||||
```bash
|
||||
docker run -d \
|
||||
--name mithal-monitor-app \
|
||||
-p 8080:8080 \
|
||||
-v "$(pwd)/data.jsonl:/app/data.jsonl" \
|
||||
-v "$(pwd)/data.csv:/app/data.csv" \
|
||||
mithal-monitor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠 التشغيل المحلي بدون Docker
|
||||
|
||||
### 1. تثبيت المتطلبات:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. تشغيل المراقبة:
|
||||
```bash
|
||||
python monitor.py
|
||||
```
|
||||
|
||||
### 3. تشغيل لوحة التحكم:
|
||||
```bash
|
||||
python -m http.server 8080
|
||||
```
|
||||
تصفح **[http://localhost:8080](http://localhost:8080)**.
|
||||
24
q5-mithal-monitor/data.csv
Normal file
24
q5-mithal-monitor/data.csv
Normal file
@@ -0,0 +1,24 @@
|
||||
timestamp,dns_time_ms,http_up,http_status_code,http_latency_ms,ssl_valid,ssl_days_remaining,ssl_expiry_date,search_up,search_status_code,search_latency_ms,error
|
||||
2026-07-26T16:29:34.975347+00:00,7.38,True,200,1107.77,False,,,True,200,947.57,SSL invalid: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1002)
|
||||
2026-07-26T16:31:48.964707+00:00,24.95,True,200,1147.77,False,,,True,200,988.58,SSL invalid: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1002)
|
||||
2026-07-26T16:32:58.200316+00:00,8.97,True,200,6586.57,False,,,True,200,1298.52,SSL invalid: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1002)
|
||||
2026-07-26T16:34:00.920377+00:00,7.73,True,200,1153.76,False,,,True,200,1139.45,SSL invalid: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1002)
|
||||
2026-07-26T16:35:03.095644+00:00,8.06,True,200,1063.03,True,,,True,200,982.81,تعذّر قراءة تاريخ الانتهاء: ثبّت حزمة cryptography عبر: pip install cryptography
|
||||
2026-07-26T16:36:06.934149+00:00,1.67,True,200,1306.19,True,,,True,200,1216.5,تعذّر قراءة تاريخ الانتهاء: ثبّت حزمة cryptography عبر: pip install cryptography
|
||||
2026-07-26T16:37:10.770763+00:00,1.28,True,200,1358.48,True,,,True,200,1217.19,تعذّر قراءة تاريخ الانتهاء: ثبّت حزمة cryptography عبر: pip install cryptography
|
||||
2026-07-26T16:40:13.186999+00:00,13.85,True,200,1177.05,True,50,2026-09-15,True,200,1001.08,
|
||||
2026-07-26T16:40:35.147022+00:00,8.38,True,200,1001.0,True,50,2026-09-15,True,200,1051.0,
|
||||
2026-07-26T16:41:37.737274+00:00,14.84,True,200,984.06,True,50,2026-09-15,True,200,1035.39,
|
||||
2026-07-26T16:42:46.446648+00:00,1.15,True,200,1056.56,True,50,2026-09-15,True,200,2815.26,
|
||||
2026-07-26T16:43:52.360311+00:00,1.09,True,200,2388.43,True,50,2026-09-15,True,200,1096.36,
|
||||
2026-07-26T16:44:55.263746+00:00,0.97,True,200,945.49,True,50,2026-09-15,True,200,977.1,
|
||||
2026-07-26T16:45:58.906118+00:00,0.91,True,200,978.28,True,50,2026-09-15,True,200,1013.73,
|
||||
2026-07-26T16:47:05.160803+00:00,9.87,True,200,909.02,True,50,2026-09-15,True,200,917.93,
|
||||
2026-07-26T16:51:32.327343+00:00,2.35,True,200,655.0,True,50,2026-09-15,True,200,658.37,
|
||||
2026-07-26T16:52:34.256793+00:00,6.05,True,200,640.74,True,50,2026-09-15,True,200,631.15,
|
||||
2026-07-26T16:53:39.970593+00:00,8.9,True,200,4007.5,True,50,2026-09-15,True,200,862.53,
|
||||
2026-07-26T16:54:23.722444+00:00,6.02,True,200,1312.45,True,50,2026-09-15,True,200,1295.87,
|
||||
2026-07-26T16:54:43.161904+00:00,7.25,True,200,1002.48,True,50,2026-09-15,True,200,840.82,
|
||||
2026-07-26T16:55:26.868127+00:00,116.93,True,200,989.42,True,50,2026-09-15,True,200,989.09,
|
||||
2026-07-26T16:55:45.089142+00:00,2.53,True,200,593.88,True,50,2026-09-15,True,200,675.95,
|
||||
2026-07-26T16:56:48.649570+00:00,2.16,True,200,662.51,True,50,2026-09-15,True,200,2219.87,
|
||||
|
23
q5-mithal-monitor/data.jsonl
Normal file
23
q5-mithal-monitor/data.jsonl
Normal file
@@ -0,0 +1,23 @@
|
||||
{"timestamp": "2026-07-26T16:29:34.975347+00:00", "dns": {"time_ms": 7.38, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 1107.77, "error": null}, "ssl": {"valid": false, "days_remaining": null, "expiry_date": null, "error": "SSL invalid: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1002)"}, "search": {"up": true, "status_code": 200, "latency_ms": 947.57, "error": null}, "error": "SSL invalid: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1002)"}
|
||||
{"timestamp": "2026-07-26T16:31:48.964707+00:00", "dns": {"time_ms": 24.95, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 1147.77, "error": null}, "ssl": {"valid": false, "days_remaining": null, "expiry_date": null, "error": "SSL invalid: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1002)"}, "search": {"up": true, "status_code": 200, "latency_ms": 988.58, "error": null}, "error": "SSL invalid: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1002)"}
|
||||
{"timestamp": "2026-07-26T16:32:58.200316+00:00", "dns": {"time_ms": 8.97, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 6586.57, "error": null}, "ssl": {"valid": false, "days_remaining": null, "expiry_date": null, "error": "SSL invalid: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1002)"}, "search": {"up": true, "status_code": 200, "latency_ms": 1298.52, "error": null}, "error": "SSL invalid: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1002)"}
|
||||
{"timestamp": "2026-07-26T16:34:00.920377+00:00", "dns": {"time_ms": 7.73, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 1153.76, "error": null}, "ssl": {"valid": false, "days_remaining": null, "expiry_date": null, "error": "SSL invalid: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1002)"}, "search": {"up": true, "status_code": 200, "latency_ms": 1139.45, "error": null}, "error": "SSL invalid: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1002)"}
|
||||
{"timestamp": "2026-07-26T16:35:03.095644+00:00", "dns": {"time_ms": 8.06, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 1063.03, "error": null}, "ssl": {"valid": true, "days_remaining": null, "expiry_date": null, "error": "تعذّر قراءة تاريخ الانتهاء: ثبّت حزمة cryptography عبر: pip install cryptography"}, "search": {"up": true, "status_code": 200, "latency_ms": 982.81, "error": null}, "error": "تعذّر قراءة تاريخ الانتهاء: ثبّت حزمة cryptography عبر: pip install cryptography"}
|
||||
{"timestamp": "2026-07-26T16:36:06.934149+00:00", "dns": {"time_ms": 1.67, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 1306.19, "error": null}, "ssl": {"valid": true, "days_remaining": null, "expiry_date": null, "error": "تعذّر قراءة تاريخ الانتهاء: ثبّت حزمة cryptography عبر: pip install cryptography"}, "search": {"up": true, "status_code": 200, "latency_ms": 1216.5, "error": null}, "error": "تعذّر قراءة تاريخ الانتهاء: ثبّت حزمة cryptography عبر: pip install cryptography"}
|
||||
{"timestamp": "2026-07-26T16:37:10.770763+00:00", "dns": {"time_ms": 1.28, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 1358.48, "error": null}, "ssl": {"valid": true, "days_remaining": null, "expiry_date": null, "error": "تعذّر قراءة تاريخ الانتهاء: ثبّت حزمة cryptography عبر: pip install cryptography"}, "search": {"up": true, "status_code": 200, "latency_ms": 1217.19, "error": null}, "error": "تعذّر قراءة تاريخ الانتهاء: ثبّت حزمة cryptography عبر: pip install cryptography"}
|
||||
{"timestamp": "2026-07-26T16:40:13.186999+00:00", "dns": {"time_ms": 13.85, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 1177.05, "error": null}, "ssl": {"valid": true, "days_remaining": 50, "expiry_date": "2026-09-15", "error": null}, "search": {"up": true, "status_code": 200, "latency_ms": 1001.08, "error": null}, "error": null}
|
||||
{"timestamp": "2026-07-26T16:40:35.147022+00:00", "dns": {"time_ms": 8.38, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 1001.0, "error": null}, "ssl": {"valid": true, "days_remaining": 50, "expiry_date": "2026-09-15", "error": null}, "search": {"up": true, "status_code": 200, "latency_ms": 1051.0, "error": null}, "error": null}
|
||||
{"timestamp": "2026-07-26T16:41:37.737274+00:00", "dns": {"time_ms": 14.84, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 984.06, "error": null}, "ssl": {"valid": true, "days_remaining": 50, "expiry_date": "2026-09-15", "error": null}, "search": {"up": true, "status_code": 200, "latency_ms": 1035.39, "error": null}, "error": null}
|
||||
{"timestamp": "2026-07-26T16:42:46.446648+00:00", "dns": {"time_ms": 1.15, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 1056.56, "error": null}, "ssl": {"valid": true, "days_remaining": 50, "expiry_date": "2026-09-15", "error": null}, "search": {"up": true, "status_code": 200, "latency_ms": 2815.26, "error": null}, "error": null}
|
||||
{"timestamp": "2026-07-26T16:43:52.360311+00:00", "dns": {"time_ms": 1.09, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 2388.43, "error": null}, "ssl": {"valid": true, "days_remaining": 50, "expiry_date": "2026-09-15", "error": null}, "search": {"up": true, "status_code": 200, "latency_ms": 1096.36, "error": null}, "error": null}
|
||||
{"timestamp": "2026-07-26T16:44:55.263746+00:00", "dns": {"time_ms": 0.97, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 945.49, "error": null}, "ssl": {"valid": true, "days_remaining": 50, "expiry_date": "2026-09-15", "error": null}, "search": {"up": true, "status_code": 200, "latency_ms": 977.1, "error": null}, "error": null}
|
||||
{"timestamp": "2026-07-26T16:45:58.906118+00:00", "dns": {"time_ms": 0.91, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 978.28, "error": null}, "ssl": {"valid": true, "days_remaining": 50, "expiry_date": "2026-09-15", "error": null}, "search": {"up": true, "status_code": 200, "latency_ms": 1013.73, "error": null}, "error": null}
|
||||
{"timestamp": "2026-07-26T16:47:05.160803+00:00", "dns": {"time_ms": 9.87, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 909.02, "error": null}, "ssl": {"valid": true, "days_remaining": 50, "expiry_date": "2026-09-15", "error": null}, "search": {"up": true, "status_code": 200, "latency_ms": 917.93, "error": null}, "error": null}
|
||||
{"timestamp": "2026-07-26T16:51:32.327343+00:00", "dns": {"time_ms": 2.35, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 655.0, "error": null}, "ssl": {"valid": true, "days_remaining": 50, "expiry_date": "2026-09-15", "error": null}, "search": {"up": true, "status_code": 200, "latency_ms": 658.37, "error": null}, "error": null}
|
||||
{"timestamp": "2026-07-26T16:52:34.256793+00:00", "dns": {"time_ms": 6.05, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 640.74, "error": null}, "ssl": {"valid": true, "days_remaining": 50, "expiry_date": "2026-09-15", "error": null}, "search": {"up": true, "status_code": 200, "latency_ms": 631.15, "error": null}, "error": null}
|
||||
{"timestamp": "2026-07-26T16:53:39.970593+00:00", "dns": {"time_ms": 8.9, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 4007.5, "error": null}, "ssl": {"valid": true, "days_remaining": 50, "expiry_date": "2026-09-15", "error": null}, "search": {"up": true, "status_code": 200, "latency_ms": 862.53, "error": null}, "error": null}
|
||||
{"timestamp": "2026-07-26T16:54:23.722444+00:00", "dns": {"time_ms": 6.02, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 1312.45, "error": null}, "ssl": {"valid": true, "days_remaining": 50, "expiry_date": "2026-09-15", "error": null}, "search": {"up": true, "status_code": 200, "latency_ms": 1295.87, "error": null}, "error": null}
|
||||
{"timestamp": "2026-07-26T16:54:43.161904+00:00", "dns": {"time_ms": 7.25, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 1002.48, "error": null}, "ssl": {"valid": true, "days_remaining": 50, "expiry_date": "2026-09-15", "error": null}, "search": {"up": true, "status_code": 200, "latency_ms": 840.82, "error": null}, "error": null}
|
||||
{"timestamp": "2026-07-26T16:55:26.868127+00:00", "dns": {"time_ms": 116.93, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 989.42, "error": null}, "ssl": {"valid": true, "days_remaining": 50, "expiry_date": "2026-09-15", "error": null}, "search": {"up": true, "status_code": 200, "latency_ms": 989.09, "error": null}, "error": null}
|
||||
{"timestamp": "2026-07-26T16:55:45.089142+00:00", "dns": {"time_ms": 2.53, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 593.88, "error": null}, "ssl": {"valid": true, "days_remaining": 50, "expiry_date": "2026-09-15", "error": null}, "search": {"up": true, "status_code": 200, "latency_ms": 675.95, "error": null}, "error": null}
|
||||
{"timestamp": "2026-07-26T16:56:48.649570+00:00", "dns": {"time_ms": 2.16, "error": null}, "http": {"up": true, "status_code": 200, "latency_ms": 662.51, "error": null}, "ssl": {"valid": true, "days_remaining": 50, "expiry_date": "2026-09-15", "error": null}, "search": {"up": true, "status_code": 200, "latency_ms": 2219.87, "error": null}, "error": null}
|
||||
15
q5-mithal-monitor/entrypoint.sh
Normal file
15
q5-mithal-monitor/entrypoint.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
export PYTHONUNBUFFERED=1
|
||||
|
||||
# Ensure dashboard.html points to index.html to avoid code duplication
|
||||
if [ ! -L dashboard.html ] && [ ! -f dashboard.html ]; then
|
||||
ln -sf index.html dashboard.html
|
||||
fi
|
||||
|
||||
echo "Starting Mithal website background monitor..."
|
||||
python3 monitor.py &
|
||||
|
||||
echo "Starting Mithal web dashboard on port 8080..."
|
||||
exec python3 -m http.server 8080
|
||||
385
q5-mithal-monitor/index.html
Normal file
385
q5-mithal-monitor/index.html
Normal file
@@ -0,0 +1,385 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>mithal.space — لوحة المراقبة</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.4/chart.umd.min.js"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{
|
||||
--bg: #0a0e17;
|
||||
--panel: #121826;
|
||||
--panel-border: #202838;
|
||||
--text: #e7ebf3;
|
||||
--muted: #7c8aa5;
|
||||
--ok: #34d399;
|
||||
--warn: #f2b84b;
|
||||
--danger: #ef5a6f;
|
||||
--accent: #4fd1ff;
|
||||
--mono: 'JetBrains Mono', monospace;
|
||||
--sans: 'Inter', sans-serif;
|
||||
}
|
||||
*{ box-sizing:border-box; }
|
||||
body{
|
||||
margin:0;
|
||||
background: radial-gradient(1200px 600px at 80% -10%, #12203a 0%, var(--bg) 55%);
|
||||
color: var(--text);
|
||||
font-family: var(--sans);
|
||||
min-height:100vh;
|
||||
padding: 28px 20px 60px;
|
||||
}
|
||||
header{
|
||||
display:flex; align-items:center; justify-content:space-between;
|
||||
max-width:1180px; margin:0 auto 24px; flex-wrap:wrap; gap:12px;
|
||||
}
|
||||
.brand{ display:flex; align-items:center; gap:12px; }
|
||||
.pulse-dot{
|
||||
width:14px; height:14px; border-radius:50%;
|
||||
background: var(--ok);
|
||||
box-shadow: 0 0 0 0 rgba(52,211,153,0.6);
|
||||
animation: pulse 1.8s infinite;
|
||||
}
|
||||
.pulse-dot.down{ background: var(--danger); box-shadow:0 0 0 0 rgba(239,90,111,0.6); animation: pulse-down 1.8s infinite; }
|
||||
@keyframes pulse{
|
||||
0%{ box-shadow: 0 0 0 0 rgba(52,211,153,0.55); }
|
||||
70%{ box-shadow: 0 0 0 12px rgba(52,211,153,0); }
|
||||
100%{ box-shadow: 0 0 0 0 rgba(52,211,153,0); }
|
||||
}
|
||||
@keyframes pulse-down{
|
||||
0%{ box-shadow: 0 0 0 0 rgba(239,90,111,0.55); }
|
||||
70%{ box-shadow: 0 0 0 12px rgba(239,90,111,0); }
|
||||
100%{ box-shadow: 0 0 0 0 rgba(239,90,111,0); }
|
||||
}
|
||||
h1{ font-size:20px; margin:0; letter-spacing:0.2px; }
|
||||
h1 span{ color: var(--muted); font-weight:400; font-size:14px; display:block; font-family: var(--mono); margin-top:2px; }
|
||||
.controls{ display:flex; gap:8px; align-items:center; }
|
||||
.controls label{
|
||||
font-family: var(--mono); font-size:12px; color:var(--muted);
|
||||
background: var(--panel); border:1px solid var(--panel-border);
|
||||
padding:8px 12px; border-radius:8px; cursor:pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.controls label:hover{ background: #1a2234; color: var(--text); }
|
||||
.controls input[type=file]{ display:none; }
|
||||
.last-updated{ font-family: var(--mono); font-size:12px; color:var(--muted); }
|
||||
|
||||
main{ max-width:1180px; margin:0 auto; display:grid; gap:16px; grid-template-columns: repeat(12,1fr); }
|
||||
|
||||
.card{
|
||||
background: var(--panel); border:1px solid var(--panel-border);
|
||||
border-radius:14px; padding:18px 20px;
|
||||
}
|
||||
.card h2{
|
||||
margin:0 0 10px; font-size:12px; text-transform:uppercase; letter-spacing:1px;
|
||||
color: var(--muted); font-weight:600;
|
||||
}
|
||||
.metric{ font-family: var(--mono); font-size:34px; font-weight:700; line-height:1; }
|
||||
.metric small{ font-size:13px; color:var(--muted); font-weight:400; margin-right:6px; }
|
||||
.sub{ margin-top:8px; font-family: var(--mono); font-size:12px; color:var(--muted); word-break: break-word; }
|
||||
.ok-text{ color:var(--ok); } .warn-text{ color:var(--warn); } .danger-text{ color:var(--danger); }
|
||||
|
||||
.col-4{ grid-column: span 4; }
|
||||
.col-12{ grid-column: span 12; }
|
||||
.col-6{ grid-column: span 6; }
|
||||
|
||||
@media (max-width: 800px){
|
||||
.col-4, .col-6{ grid-column: span 12; }
|
||||
}
|
||||
|
||||
canvas{ max-height:260px; width:100%; }
|
||||
|
||||
table{ width:100%; border-collapse:collapse; font-family: var(--mono); font-size:13px; }
|
||||
th, td{ text-align:right; padding:9px 8px; border-bottom:1px solid var(--panel-border); }
|
||||
th{ color:var(--muted); font-weight:500; font-size:11px; text-transform:uppercase; letter-spacing:0.5px; }
|
||||
.badge{ display:inline-block; padding:2px 9px; border-radius:20px; font-size:11px; font-weight:700; }
|
||||
.badge.up{ background: rgba(52,211,153,0.15); color:var(--ok); }
|
||||
.badge.down{ background: rgba(239,90,111,0.15); color:var(--danger); }
|
||||
|
||||
.empty-state{
|
||||
text-align:center; padding:60px 20px; color:var(--muted); font-family: var(--mono); font-size:13px;
|
||||
}
|
||||
.empty-state b{ color:var(--text); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<div class="brand">
|
||||
<div class="pulse-dot" id="pulseDot"></div>
|
||||
<h1>mithal.space<span>لوحة مراقبة الموقع — Uptime · Latency · SSL · DNS · Search</span></h1>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<span class="last-updated" id="lastUpdated">لا توجد بيانات بعد</span>
|
||||
<label for="fileInput">📂 تحميل data.jsonl</label>
|
||||
<input type="file" id="fileInput" accept=".jsonl,.json,.txt">
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="dashboard">
|
||||
<div class="card col-12" id="emptyCard" style="display: block;">
|
||||
<div class="empty-state" id="emptyState">
|
||||
شغّل <b>monitor.py</b> لجمع البيانات، ثم اخدم هذا المجلد عبر خادم محلي
|
||||
(مثال: <b>python3 -m http.server 8080</b>) ليتم تحميل <b>data.jsonl</b> تلقائياً،
|
||||
أو استخدم زر «تحميل data.jsonl» أعلاه لاختيار الملف يدوياً.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card col-4 main-content" style="display:none;">
|
||||
<h2>Uptime (24 ساعة)</h2>
|
||||
<div class="metric" id="uptimeMetric">—</div>
|
||||
<div class="sub" id="uptimeSub">—</div>
|
||||
</div>
|
||||
|
||||
<div class="card col-4 main-content" style="display:none;">
|
||||
<h2>شهادة SSL</h2>
|
||||
<div class="metric" id="sslMetric">—</div>
|
||||
<div class="sub" id="sslSub">—</div>
|
||||
</div>
|
||||
|
||||
<div class="card col-4 main-content" style="display:none;">
|
||||
<h2>DNS (متوسط 24س) / آخر بحث</h2>
|
||||
<div class="metric" id="dnsMetric">—</div>
|
||||
<div class="sub" id="dnsSub">—</div>
|
||||
</div>
|
||||
|
||||
<div class="card col-12 main-content" style="display:none;">
|
||||
<h2>زمن الاستجابة — آخر ساعة (Latency)</h2>
|
||||
<canvas id="latencyChart"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="card col-12 main-content" style="display:none;">
|
||||
<h2>سجل آخر 10 فحوصات</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>الوقت</th><th>الحالة</th><th>Latency</th><th>DNS</th><th>SSL (أيام)</th><th>Search</th></tr>
|
||||
</thead>
|
||||
<tbody id="logBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const REFRESH_MS = 60000; // إعادة تحميل تلقائي كل دقيقة
|
||||
|
||||
let records = [];
|
||||
let latencyChart = null;
|
||||
|
||||
function parseJsonl(text){
|
||||
return text.split('\n')
|
||||
.map(l => l.trim())
|
||||
.filter(Boolean)
|
||||
.map(l => { try { return JSON.parse(l); } catch(e){ return null; } })
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function fmtTime(iso){
|
||||
if (!iso) return '—';
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return iso;
|
||||
const hours = String(d.getHours()).padStart(2, '0');
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(d.getSeconds()).padStart(2, '0');
|
||||
return `${d.toLocaleDateString('ar-EG')} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
function fmtShortTime(iso){
|
||||
if (!iso) return '—';
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return iso;
|
||||
const hours = String(d.getHours()).padStart(2, '0');
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(d.getSeconds()).padStart(2, '0');
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
function statusColorClass(days){
|
||||
if (days === null || days === undefined) return 'danger-text';
|
||||
if (days <= 7) return 'danger-text';
|
||||
if (days <= 30) return 'warn-text';
|
||||
return 'ok-text';
|
||||
}
|
||||
|
||||
function initChart() {
|
||||
const chartCanvas = document.getElementById('latencyChart');
|
||||
if (!chartCanvas) return;
|
||||
const ctx = chartCanvas.getContext('2d');
|
||||
latencyChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'HTTP latency (ms)',
|
||||
data: [],
|
||||
borderColor: '#4fd1ff',
|
||||
backgroundColor: 'rgba(79,209,255,0.08)',
|
||||
tension: 0.3,
|
||||
pointRadius: 2,
|
||||
spanGaps: true,
|
||||
fill: true,
|
||||
},
|
||||
{
|
||||
label: 'Search latency (ms)',
|
||||
data: [],
|
||||
borderColor: '#f2b84b',
|
||||
backgroundColor: 'rgba(242,184,75,0.06)',
|
||||
tension: 0.3,
|
||||
pointRadius: 2,
|
||||
spanGaps: true,
|
||||
fill: true,
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { labels: { color: '#7c8aa5', font: { family: 'JetBrains Mono', size: 11 } } }
|
||||
},
|
||||
scales: {
|
||||
x: { ticks: { color: '#7c8aa5', maxTicksLimit: 12 }, grid: { color: '#202838' } },
|
||||
y: { ticks: { color: '#7c8aa5' }, grid: { color: '#202838' }, title:{display:true, text:'ms', color:'#7c8aa5'} }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function render(){
|
||||
const emptyCard = document.getElementById('emptyCard');
|
||||
const mainCards = document.querySelectorAll('.main-content');
|
||||
|
||||
if (!records.length){
|
||||
if (emptyCard) emptyCard.style.display = 'block';
|
||||
mainCards.forEach(c => c.style.display = 'none');
|
||||
return;
|
||||
}
|
||||
|
||||
if (emptyCard) emptyCard.style.display = 'none';
|
||||
mainCards.forEach(c => c.style.display = 'block');
|
||||
|
||||
const sorted = [...records].sort((a,b) => new Date(a.timestamp) - new Date(b.timestamp));
|
||||
const latest = sorted[sorted.length - 1];
|
||||
const nowMs = new Date(latest.timestamp).getTime();
|
||||
|
||||
// --- Uptime % over last 24h ---
|
||||
const last24h = sorted.filter(r => nowMs - new Date(r.timestamp).getTime() <= 24*3600*1000);
|
||||
const upCount = last24h.filter(r => r.http && r.http.up).length;
|
||||
const uptimePct = last24h.length ? ((upCount / last24h.length) * 100).toFixed(2) : 'N/A';
|
||||
const uptimeClass = uptimePct === 'N/A' ? '' : (uptimePct >= 99.5 ? 'ok-text' : uptimePct >= 95 ? 'warn-text' : 'danger-text');
|
||||
|
||||
const uptimeEl = document.getElementById('uptimeMetric');
|
||||
if (uptimeEl) {
|
||||
uptimeEl.className = `metric ${uptimeClass}`;
|
||||
uptimeEl.innerHTML = `${uptimePct}${uptimePct !== 'N/A' ? '<small>%</small>' : ''}`;
|
||||
}
|
||||
const uptimeSubEl = document.getElementById('uptimeSub');
|
||||
if (uptimeSubEl) {
|
||||
uptimeSubEl.textContent = `${upCount}/${last24h.length} فحص ناجح آخر 24 ساعة`;
|
||||
}
|
||||
|
||||
// --- SSL ---
|
||||
const ssl = latest.ssl || {};
|
||||
const hasValidDays = ssl.valid && ssl.days_remaining !== null && ssl.days_remaining !== undefined;
|
||||
const sslClass = hasValidDays ? statusColorClass(ssl.days_remaining) : 'danger-text';
|
||||
const sslMetricText = hasValidDays ? ssl.days_remaining : '—';
|
||||
const sslSubText = hasValidDays ? (`تنتهي في ${ssl.expiry_date}`) : (ssl.error ? `غير سارية (${ssl.error})` : 'غير سارية أو تعذّر التحقق');
|
||||
|
||||
const sslMetricEl = document.getElementById('sslMetric');
|
||||
if (sslMetricEl) {
|
||||
sslMetricEl.className = `metric ${sslClass}`;
|
||||
sslMetricEl.innerHTML = `${sslMetricText}${hasValidDays ? '<small>يوم متبقي</small>' : ''}`;
|
||||
}
|
||||
const sslSubEl = document.getElementById('sslSub');
|
||||
if (sslSubEl) {
|
||||
sslSubEl.textContent = sslSubText;
|
||||
}
|
||||
|
||||
// --- DNS avg over last 24h ---
|
||||
const dnsTimes = last24h.map(r => r.dns && r.dns.time_ms).filter(v => v !== null && v !== undefined);
|
||||
const dnsAvg = dnsTimes.length ? (dnsTimes.reduce((a,b)=>a+b,0)/dnsTimes.length).toFixed(1) : 'N/A';
|
||||
|
||||
const dnsMetricEl = document.getElementById('dnsMetric');
|
||||
if (dnsMetricEl) {
|
||||
dnsMetricEl.innerHTML = `${dnsAvg}${dnsAvg !== 'N/A' ? '<small>ms</small>' : ''}`;
|
||||
}
|
||||
const dnsSubEl = document.getElementById('dnsSub');
|
||||
if (dnsSubEl) {
|
||||
dnsSubEl.textContent = `زمن البحث الأخير: ${latest.search && latest.search.latency_ms !== null && latest.search.latency_ms !== undefined ? latest.search.latency_ms + ' ms' : 'فشل الطلب'}`;
|
||||
}
|
||||
|
||||
// --- pulse dot & last updated ---
|
||||
const dot = document.getElementById('pulseDot');
|
||||
if (dot) dot.classList.toggle('down', !latest.http || !latest.http.up);
|
||||
|
||||
const lastUpdatedEl = document.getElementById('lastUpdated');
|
||||
if (lastUpdatedEl) lastUpdatedEl.textContent = 'آخر تحديث: ' + fmtTime(latest.timestamp);
|
||||
|
||||
// --- update Chart smoothly ---
|
||||
const lastHour = sorted.filter(r => nowMs - new Date(r.timestamp).getTime() <= 3600*1000);
|
||||
const chartSource = lastHour.length ? lastHour : sorted.slice(-30);
|
||||
|
||||
const labels = chartSource.map(r => fmtShortTime(r.timestamp));
|
||||
const httpData = chartSource.map(r => r.http ? r.http.latency_ms : null);
|
||||
const searchData = chartSource.map(r => r.search ? r.search.latency_ms : null);
|
||||
|
||||
if (!latencyChart) {
|
||||
initChart();
|
||||
}
|
||||
if (latencyChart) {
|
||||
latencyChart.data.labels = labels;
|
||||
latencyChart.data.datasets[0].data = httpData;
|
||||
latencyChart.data.datasets[1].data = searchData;
|
||||
latencyChart.update();
|
||||
}
|
||||
|
||||
// --- log table (last 10) ---
|
||||
const logBody = document.getElementById('logBody');
|
||||
if (logBody) {
|
||||
const last10 = sorted.slice(-10).reverse();
|
||||
logBody.innerHTML = last10.map(r => {
|
||||
const rSslValid = r.ssl && r.ssl.valid && r.ssl.days_remaining !== null && r.ssl.days_remaining !== undefined;
|
||||
return `
|
||||
<tr>
|
||||
<td>${fmtTime(r.timestamp)}</td>
|
||||
<td><span class="badge ${r.http && r.http.up ? 'up' : 'down'}">${r.http && r.http.up ? 'UP' : 'DOWN'}</span></td>
|
||||
<td>${r.http && r.http.latency_ms !== null && r.http.latency_ms !== undefined ? r.http.latency_ms + ' ms' : '—'}</td>
|
||||
<td>${r.dns && r.dns.time_ms !== null && r.dns.time_ms !== undefined ? r.dns.time_ms + ' ms' : '—'}</td>
|
||||
<td class="${statusColorClass(rSslValid ? r.ssl.days_remaining : null)}">${rSslValid ? r.ssl.days_remaining : '—'}</td>
|
||||
<td>${r.search && r.search.latency_ms !== null && r.search.latency_ms !== undefined ? r.search.latency_ms + ' ms' : '—'}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFromServer(){
|
||||
try{
|
||||
const res = await fetch('data.jsonl', { cache: 'no-store' });
|
||||
if (!res.ok) throw new Error('not found');
|
||||
const text = await res.text();
|
||||
records = parseJsonl(text);
|
||||
render();
|
||||
} catch(e){
|
||||
// Fallback when served directly as file:// or if fetch fails
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('fileInput').addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (evt) => {
|
||||
records = parseJsonl(evt.target.result);
|
||||
render();
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
initChart();
|
||||
loadFromServer();
|
||||
setInterval(loadFromServer, REFRESH_MS);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
327
q5-mithal-monitor/monitor.py
Normal file
327
q5-mithal-monitor/monitor.py
Normal file
@@ -0,0 +1,327 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
مراقب موقع mithal.space (Website Monitor)
|
||||
==========================================
|
||||
يجمع كل دقيقة:
|
||||
- Latency : زمن استجابة HTTP للصفحة الرئيسية
|
||||
- Uptime : هل الموقع متاح (status code 2xx/3xx)
|
||||
- SSL : صلاحية الشهادة وعدد الأيام المتبقية على انتهائها
|
||||
- DNS : زمن تحليل اسم النطاق (DNS resolution time)
|
||||
- Search : زمن الرد على طلب بحث (search query) داخل الموقع
|
||||
|
||||
يخزّن كل فحص كسطر JSON مستقل في data.jsonl (JSON Lines) وأيضاً كصف
|
||||
في data.csv، بحيث يمكن قراءة الملف تراكمياً دون الحاجة لإعادة كتابته.
|
||||
|
||||
الاستخدام:
|
||||
python3 monitor.py # يعمل باستمرار، فحص كل 60 ثانية
|
||||
python3 monitor.py --once # فحص واحد فقط (مناسب لجدولة cron)
|
||||
python3 monitor.py --interval 30 # تغيير الفاصل الزمني (بالثواني)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import concurrent.futures
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import requests
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# الإعدادات (Configuration) — عدّل هذه القيم حسب موقعك
|
||||
# ----------------------------------------------------------------------------
|
||||
SITE_URL = "https://mithal.space"
|
||||
SITE_HOST = "mithal.space"
|
||||
SITE_PORT = 443
|
||||
|
||||
# رابط البحث الذي سيتم اختباره — عدّله ليطابق مسار البحث الفعلي في موقعك
|
||||
# مثال: https://mithal.space/search?q=test
|
||||
SEARCH_URL = "https://mithal.space/search?q=test"
|
||||
|
||||
REQUEST_TIMEOUT = 10 # ثانية
|
||||
CHECK_INTERVAL_SECONDS = 60 # كل دقيقة
|
||||
|
||||
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
JSONL_PATH = os.path.join(DATA_DIR, "data.jsonl")
|
||||
CSV_PATH = os.path.join(DATA_DIR, "data.csv")
|
||||
|
||||
CSV_FIELDS = [
|
||||
"timestamp",
|
||||
"dns_time_ms",
|
||||
"http_up",
|
||||
"http_status_code",
|
||||
"http_latency_ms",
|
||||
"ssl_valid",
|
||||
"ssl_days_remaining",
|
||||
"ssl_expiry_date",
|
||||
"search_up",
|
||||
"search_status_code",
|
||||
"search_latency_ms",
|
||||
"error",
|
||||
]
|
||||
|
||||
|
||||
def now_iso():
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 1) DNS — وقت تحليل اسم النطاق
|
||||
# ----------------------------------------------------------------------------
|
||||
def measure_dns(hostname: str, timeout: float = REQUEST_TIMEOUT):
|
||||
"""
|
||||
ملاحظة مهمة: socket.getaddrinfo() لا يقبل معامل timeout أصلاً، وقد يتجمّد
|
||||
إلى ما لا نهاية إذا كان الـ DNS بطيئاً أو محجوباً (VPN/جدار حماية/شبكة شركة).
|
||||
لتفادي تجميد السكربت بالكامل، ننفّذ البحث في Thread منفصل ونفرض عليه
|
||||
مهلة زمنية يدوياً عبر future.result(timeout=...).
|
||||
"""
|
||||
executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
future = executor.submit(socket.getaddrinfo, hostname, None)
|
||||
future.result(timeout=timeout)
|
||||
elapsed_ms = (time.perf_counter() - start) * 1000
|
||||
return {"time_ms": round(elapsed_ms, 2), "error": None}
|
||||
except concurrent.futures.TimeoutError:
|
||||
return {"time_ms": None, "error": f"DNS timeout after {timeout}s (تحقق من الاتصال بالإنترنت أو VPN/DNS)"}
|
||||
except socket.gaierror as e:
|
||||
return {"time_ms": None, "error": str(e)}
|
||||
finally:
|
||||
# wait=False حتى لا ننتظر انتهاء الـ thread العالق في حال حدوث timeout
|
||||
executor.shutdown(wait=False)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 2) HTTP — Latency + Uptime (status code)
|
||||
# ----------------------------------------------------------------------------
|
||||
def measure_http(url: str):
|
||||
try:
|
||||
start = time.perf_counter()
|
||||
resp = requests.get(url, timeout=REQUEST_TIMEOUT)
|
||||
elapsed_ms = (time.perf_counter() - start) * 1000
|
||||
return {
|
||||
"up": resp.status_code < 400,
|
||||
"status_code": resp.status_code,
|
||||
"latency_ms": round(elapsed_ms, 2),
|
||||
"error": None,
|
||||
}
|
||||
except requests.exceptions.RequestException as e:
|
||||
return {"up": False, "status_code": None, "latency_ms": None, "error": str(e)}
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 3) SSL — حالة الشهادة وتاريخ الانتهاء
|
||||
# ----------------------------------------------------------------------------
|
||||
def _get_ca_context():
|
||||
"""يستخدم حزمة CA من certifi إن كانت مثبتة (أدق وأكثر استقراراً على ويندوز
|
||||
من الاعتماد على مخزن الشهادات الافتراضي)، وإلا يعود للسياق الافتراضي."""
|
||||
try:
|
||||
import certifi
|
||||
return ssl.create_default_context(cafile=certifi.where())
|
||||
except ImportError:
|
||||
return ssl.create_default_context()
|
||||
|
||||
|
||||
def _read_cert_expiry(der_bytes: bytes):
|
||||
"""Read certificate expiry date using Python's built-in SSL module."""
|
||||
try:
|
||||
pem_bytes = ssl.DER_cert_to_PEM_cert(der_bytes)
|
||||
|
||||
# ssl._ssl._test_decode_cert() needs a file,
|
||||
# so we create a temporary certificate file.
|
||||
import tempfile
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w",
|
||||
suffix=".pem",
|
||||
delete=False
|
||||
) as f:
|
||||
f.write(pem_bytes)
|
||||
cert_path = f.name
|
||||
|
||||
try:
|
||||
cert_info = ssl._ssl._test_decode_cert(cert_path)
|
||||
expiry_str = cert_info.get("notAfter")
|
||||
|
||||
if expiry_str:
|
||||
expiry_dt = datetime.strptime(
|
||||
expiry_str,
|
||||
"%b %d %H:%M:%S %Y %Z"
|
||||
).replace(tzinfo=timezone.utc)
|
||||
|
||||
return expiry_dt
|
||||
|
||||
return None
|
||||
|
||||
finally:
|
||||
os.unlink(cert_path)
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def check_ssl(hostname: str, port: int = 443):
|
||||
"""
|
||||
يفصل بين أمرين مختلفين قد يُخلط بينهما:
|
||||
1) trusted : هل تثق سلسلة الشهادات (chain of trust) بالشهادة عبر حزمة
|
||||
CA؟ قد تفشل هذه الخطوة محلياً (خصوصاً على ويندوز) دون أن
|
||||
يكون هناك أي خلل في شهادة الموقع نفسه.
|
||||
2) expiry : تاريخ انتهاء الشهادة الفعلي — نقرأه دائماً من البايتات
|
||||
الخام للشهادة (getpeercert(binary_form=True)) بغض النظر
|
||||
عن نجاح التحقق من الثقة، لأن getpeercert() العادي يعيد
|
||||
قاموساً فارغاً {} إن لم يتم التحقق من الثقة.
|
||||
"""
|
||||
trusted = False
|
||||
trust_error = None
|
||||
days_remaining = None
|
||||
expiry_date = None
|
||||
|
||||
# 1) محاولة اتصال بالتحقق الكامل من الثقة
|
||||
try:
|
||||
context = _get_ca_context()
|
||||
with socket.create_connection((hostname, port), timeout=REQUEST_TIMEOUT) as sock:
|
||||
with context.wrap_socket(sock, server_hostname=hostname):
|
||||
trusted = True
|
||||
except ssl.SSLCertVerificationError as e:
|
||||
trust_error = f"فشل التحقق من سلسلة الثقة محلياً (قد يكون خلل CA bundle وليس عيباً بالشهادة): {e}"
|
||||
except (socket.timeout, socket.gaierror, ConnectionRefusedError, OSError) as e:
|
||||
return {"valid": False, "days_remaining": None, "expiry_date": None, "error": str(e)}
|
||||
|
||||
# 2) قراءة تاريخ الانتهاء بشكل مستقل، حتى لو فشلت خطوة الثقة أعلاه
|
||||
try:
|
||||
unverified_context = ssl._create_unverified_context()
|
||||
with socket.create_connection((hostname, port), timeout=REQUEST_TIMEOUT) as sock:
|
||||
with unverified_context.wrap_socket(sock, server_hostname=hostname) as ssock:
|
||||
der_bytes = ssock.getpeercert(binary_form=True)
|
||||
expiry_dt = _read_cert_expiry(der_bytes) if der_bytes else None
|
||||
if expiry_dt is not None:
|
||||
days_remaining = (expiry_dt - datetime.now(timezone.utc)).days
|
||||
expiry_date = expiry_dt.date().isoformat()
|
||||
elif trust_error is None:
|
||||
trust_error = "تعذّر قراءة تاريخ الانتهاء: ثبّت حزمة cryptography عبر: pip install cryptography"
|
||||
except (socket.timeout, socket.gaierror, ConnectionRefusedError, OSError) as e:
|
||||
if trust_error is None:
|
||||
trust_error = str(e)
|
||||
|
||||
return {
|
||||
"valid": trusted,
|
||||
"days_remaining": days_remaining,
|
||||
"expiry_date": expiry_date,
|
||||
"error": trust_error,
|
||||
}
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 4) Search Response — زمن الرد على طلب بحث
|
||||
# ----------------------------------------------------------------------------
|
||||
def measure_search(url: str):
|
||||
try:
|
||||
start = time.perf_counter()
|
||||
resp = requests.get(url, timeout=REQUEST_TIMEOUT)
|
||||
elapsed_ms = (time.perf_counter() - start) * 1000
|
||||
return {
|
||||
"up": resp.status_code < 400,
|
||||
"status_code": resp.status_code,
|
||||
"latency_ms": round(elapsed_ms, 2),
|
||||
"error": None,
|
||||
}
|
||||
except requests.exceptions.RequestException as e:
|
||||
return {"up": False, "status_code": None, "latency_ms": None, "error": str(e)}
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# تجميع كل المقاييس في فحص واحد
|
||||
# ----------------------------------------------------------------------------
|
||||
def collect_metrics():
|
||||
dns = measure_dns(SITE_HOST)
|
||||
http = measure_http(SITE_URL)
|
||||
ssl_info = check_ssl(SITE_HOST, SITE_PORT)
|
||||
search = measure_search(SEARCH_URL)
|
||||
|
||||
errors = [e for e in (dns.get("error"), http.get("error"), ssl_info.get("error"), search.get("error")) if e]
|
||||
|
||||
return {
|
||||
"timestamp": now_iso(),
|
||||
"dns": dns,
|
||||
"http": http,
|
||||
"ssl": ssl_info,
|
||||
"search": search,
|
||||
"error": "; ".join(errors) if errors else None,
|
||||
}
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# التخزين: JSON Lines + CSV
|
||||
# ----------------------------------------------------------------------------
|
||||
def save_jsonl(record: dict):
|
||||
with open(JSONL_PATH, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
||||
|
||||
|
||||
def save_csv(record: dict):
|
||||
file_exists = os.path.isfile(CSV_PATH)
|
||||
row = {
|
||||
"timestamp": record["timestamp"],
|
||||
"dns_time_ms": record["dns"].get("time_ms"),
|
||||
"http_up": record["http"].get("up"),
|
||||
"http_status_code": record["http"].get("status_code"),
|
||||
"http_latency_ms": record["http"].get("latency_ms"),
|
||||
"ssl_valid": record["ssl"].get("valid"),
|
||||
"ssl_days_remaining": record["ssl"].get("days_remaining"),
|
||||
"ssl_expiry_date": record["ssl"].get("expiry_date"),
|
||||
"search_up": record["search"].get("up"),
|
||||
"search_status_code": record["search"].get("status_code"),
|
||||
"search_latency_ms": record["search"].get("latency_ms"),
|
||||
"error": record.get("error"),
|
||||
}
|
||||
with open(CSV_PATH, "a", newline="", encoding="utf-8") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=CSV_FIELDS)
|
||||
if not file_exists:
|
||||
writer.writeheader()
|
||||
writer.writerow(row)
|
||||
|
||||
|
||||
def run_once():
|
||||
record = collect_metrics()
|
||||
save_jsonl(record)
|
||||
save_csv(record)
|
||||
status = "UP" if record["http"]["up"] else "DOWN"
|
||||
print(
|
||||
f"[{record['timestamp']}] HTTP={status} "
|
||||
f"latency={record['http']['latency_ms']}ms "
|
||||
f"dns={record['dns']['time_ms']}ms "
|
||||
f"ssl_days_left={record['ssl']['days_remaining']} "
|
||||
f"search={record['search']['latency_ms']}ms",
|
||||
flush=True,
|
||||
)
|
||||
return record
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="مراقب موقع mithal.space")
|
||||
parser.add_argument("--once", action="store_true", help="فحص واحد فقط ثم الخروج (مناسب لـ cron)")
|
||||
parser.add_argument(
|
||||
"--interval", type=int, default=CHECK_INTERVAL_SECONDS, help="الفاصل الزمني بالثواني بين الفحوصات"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.once:
|
||||
run_once()
|
||||
return
|
||||
|
||||
print(f"بدء المراقبة المستمرة لـ {SITE_URL} كل {args.interval} ثانية... (Ctrl+C للإيقاف)", flush=True)
|
||||
while True:
|
||||
try:
|
||||
run_once()
|
||||
except Exception as e:
|
||||
# لا نسمح لأي خطأ غير متوقع بإيقاف الحلقة الرئيسية
|
||||
print(f"خطأ غير متوقع أثناء الفحص: {e}", flush=True)
|
||||
time.sleep(args.interval)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
2
q5-mithal-monitor/monitoring_data.csv
Normal file
2
q5-mithal-monitor/monitoring_data.csv
Normal file
@@ -0,0 +1,2 @@
|
||||
timestamp,status_code,latency_ms,uptime,ssl_valid,ssl_expiry,ssl_days_remaining,ssl_check_latency_ms,dns_success,dns_latency_ms,search_status_code,search_latency_ms,search_success
|
||||
2026-07-26T16:24:48.432979+00:00,200,1240.09,True,False,,,,True,0.6,200,1003.56,True
|
||||
|
3
q5-mithal-monitor/requirements.txt
Normal file
3
q5-mithal-monitor/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
requests>=2.28.0
|
||||
certifi>=2023.0.0
|
||||
cryptography>=40.0.0
|
||||
المرجع في مشكلة جديدة
حظر مستخدم