initial files
هذا الالتزام موجود في:
5
q5-mithal-monitor/Dockerfile
Normal file
5
q5-mithal-monitor/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY monitor.py dashboard.html ./
|
||||||
|
EXPOSE 5000
|
||||||
|
CMD ["python", "monitor.py"]
|
||||||
ثنائية
q5-mithal-monitor/__pycache__/monitor.cpython-313.pyc
Normal file
ثنائية
q5-mithal-monitor/__pycache__/monitor.cpython-313.pyc
Normal file
ملف ثنائي غير معروض.
142
q5-mithal-monitor/dashboard.html
Normal file
142
q5-mithal-monitor/dashboard.html
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<!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 monitor</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background: #1a1a2e;
|
||||||
|
color: #eee;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
h1 { margin-top: 0; }
|
||||||
|
.box {
|
||||||
|
background: #16213e;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.big { font-size: 32px; font-weight: bold; }
|
||||||
|
.ok { color: #4caf50; }
|
||||||
|
.bad { color: #f44336; }
|
||||||
|
table { width: 100%; border-collapse: collapse; }
|
||||||
|
th, td {
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>mithal.space — Monitoring</h1>
|
||||||
|
<p id="status">loading...</p>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<div>Uptime (24h)</div>
|
||||||
|
<div class="big" id="uptime">-</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<div>SSL (days left)</div>
|
||||||
|
<div class="big" id="ssl">-</div>
|
||||||
|
<div id="sslDate"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<div>Latency last hour</div>
|
||||||
|
<canvas id="chart"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<div>Last 10 checks</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>time</th>
|
||||||
|
<th>latency</th>
|
||||||
|
<th>dns</th>
|
||||||
|
<th>search</th>
|
||||||
|
<th>status</th>
|
||||||
|
<th>result</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="rows"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let chart
|
||||||
|
|
||||||
|
function loadData() {
|
||||||
|
fetch('metrics.json')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!data.length) return
|
||||||
|
|
||||||
|
const last = data[data.length - 1]
|
||||||
|
document.getElementById('status').textContent =
|
||||||
|
last.uptime ? 'site is UP' : 'site is DOWN'
|
||||||
|
document.getElementById('status').className = last.uptime ? 'ok' : 'bad'
|
||||||
|
|
||||||
|
const dayAgo = Date.now() - 24 * 60 * 60 * 1000
|
||||||
|
const dayData = data.filter(d => new Date(d.timestamp).getTime() > dayAgo)
|
||||||
|
const upCount = dayData.filter(d => d.uptime).length
|
||||||
|
const uptimePct = dayData.length ? (upCount / dayData.length * 100).toFixed(2) : 0
|
||||||
|
document.getElementById('uptime').textContent = uptimePct + '%'
|
||||||
|
|
||||||
|
document.getElementById('ssl').textContent =
|
||||||
|
last.ssl_days_remaining != null ? last.ssl_days_remaining + ' days' : 'n/a'
|
||||||
|
document.getElementById('sslDate').textContent =
|
||||||
|
last.ssl_expires ? 'expires: ' + last.ssl_expires : ''
|
||||||
|
|
||||||
|
const hourAgo = Date.now() - 60 * 60 * 1000
|
||||||
|
const hourData = data.filter(d => new Date(d.timestamp).getTime() > hourAgo)
|
||||||
|
const labels = hourData.map(d => d.timestamp.slice(11, 19))
|
||||||
|
const values = hourData.map(d => d.latency_ms)
|
||||||
|
|
||||||
|
if (!chart) {
|
||||||
|
chart = new Chart(document.getElementById('chart'), {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'ms',
|
||||||
|
data: values,
|
||||||
|
borderColor: '#4fc3f7',
|
||||||
|
tension: 0.2
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
chart.data.labels = labels
|
||||||
|
chart.data.datasets[0].data = values
|
||||||
|
chart.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
const last10 = data.slice(-10).reverse()
|
||||||
|
document.getElementById('rows').innerHTML = last10.map(d => `
|
||||||
|
<tr>
|
||||||
|
<td>${d.timestamp.replace('T', ' ').replace('Z', '')}</td>
|
||||||
|
<td>${d.latency_ms} ms</td>
|
||||||
|
<td>${d.dns_ms} ms</td>
|
||||||
|
<td>${d.search_ms} ms</td>
|
||||||
|
<td>${d.status_code}</td>
|
||||||
|
<td class="${d.overall ? 'ok' : 'bad'}">${d.overall ? 'ok' : 'fail'}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
document.getElementById('status').textContent = 'cannot load metrics.json'
|
||||||
|
document.getElementById('status').className = 'bad'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
loadData()
|
||||||
|
setInterval(loadData, 30000)
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2
q5-mithal-monitor/metrics.csv
Normal file
2
q5-mithal-monitor/metrics.csv
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
timestamp,latency_ms,uptime,status_code,ssl_valid,ssl_expires,ssl_days_remaining,dns_ms,search_ms,search_status_code,overall
|
||||||
|
2026-07-29T01:55:40.917123+00:00,705.91,True,200,True,2026-09-15T13:10:47+00:00,48,0.54,1684.08,200,True
|
||||||
|
41
q5-mithal-monitor/metrics.json
Normal file
41
q5-mithal-monitor/metrics.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-29T01:55:40.917123+00:00",
|
||||||
|
"latency_ms": 705.91,
|
||||||
|
"uptime": true,
|
||||||
|
"status_code": 200,
|
||||||
|
"ssl_valid": true,
|
||||||
|
"ssl_expires": "2026-09-15T13:10:47+00:00",
|
||||||
|
"ssl_days_remaining": 48,
|
||||||
|
"dns_ms": 0.54,
|
||||||
|
"search_ms": 1684.08,
|
||||||
|
"search_status_code": 200,
|
||||||
|
"overall": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-29T02:06:20.105132Z",
|
||||||
|
"latency_ms": 515.49,
|
||||||
|
"uptime": true,
|
||||||
|
"status_code": 200,
|
||||||
|
"dns_ms": 10.14,
|
||||||
|
"ssl_valid": false,
|
||||||
|
"ssl_expires": "Sep 15 13:10:47 2026 GMT",
|
||||||
|
"ssl_days_remaining": null,
|
||||||
|
"search_ms": 476.59,
|
||||||
|
"search_status_code": 200,
|
||||||
|
"overall": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-29T02:06:35.031662Z",
|
||||||
|
"latency_ms": 502.26,
|
||||||
|
"uptime": true,
|
||||||
|
"status_code": 200,
|
||||||
|
"dns_ms": 0.51,
|
||||||
|
"ssl_valid": false,
|
||||||
|
"ssl_expires": "Sep 15 13:10:47 2026 GMT",
|
||||||
|
"ssl_days_remaining": null,
|
||||||
|
"search_ms": 496.97,
|
||||||
|
"search_status_code": 200,
|
||||||
|
"overall": false
|
||||||
|
}
|
||||||
|
]
|
||||||
122
q5-mithal-monitor/monitor.py
Normal file
122
q5-mithal-monitor/monitor.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||||
|
from urllib.error import HTTPError
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
URL = "https://mithal.space"
|
||||||
|
SEARCH_URL = "https://mithal.space/search?q=test"
|
||||||
|
HOST = "mithal.space"
|
||||||
|
METRICS_FILE = "metrics.json"
|
||||||
|
|
||||||
|
|
||||||
|
def check_http(url):
|
||||||
|
start = time.time()
|
||||||
|
status = 0
|
||||||
|
ok = False
|
||||||
|
try:
|
||||||
|
res = urlopen(url, timeout=15)
|
||||||
|
status = res.status
|
||||||
|
ok = status == 200
|
||||||
|
res.read(1024)
|
||||||
|
except HTTPError as e:
|
||||||
|
status = e.code
|
||||||
|
ok = status == 200
|
||||||
|
except Exception:
|
||||||
|
ok = False
|
||||||
|
ms = round((time.time() - start) * 1000, 2)
|
||||||
|
return ms, status, ok
|
||||||
|
|
||||||
|
|
||||||
|
def check_dns():
|
||||||
|
start = time.time()
|
||||||
|
ok = False
|
||||||
|
try:
|
||||||
|
socket.gethostbyname(HOST)
|
||||||
|
ok = True
|
||||||
|
except Exception:
|
||||||
|
ok = False
|
||||||
|
ms = round((time.time() - start) * 1000, 2)
|
||||||
|
return ms, ok
|
||||||
|
|
||||||
|
|
||||||
|
def check_ssl():
|
||||||
|
valid = False
|
||||||
|
expires = None
|
||||||
|
days_left = None
|
||||||
|
try:
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
with socket.create_connection((HOST, 443), timeout=10) as s:
|
||||||
|
with ctx.wrap_socket(s, server_hostname=HOST) as ss:
|
||||||
|
cert = ss.getpeercert()
|
||||||
|
expires = cert["notAfter"]
|
||||||
|
exp_date = datetime.strptime(expires, "%b %d %H:%M:%S %Y %GMT")
|
||||||
|
days_left = (exp_date - datetime.utcnow()).days
|
||||||
|
valid = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return valid, expires, days_left
|
||||||
|
|
||||||
|
|
||||||
|
def run_check():
|
||||||
|
latency, status_code, uptime = check_http(URL)
|
||||||
|
dns_ms, dns_ok = check_dns()
|
||||||
|
ssl_ok, ssl_expires, ssl_days = check_ssl()
|
||||||
|
search_ms, search_code, search_ok = check_http(SEARCH_URL)
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"timestamp": datetime.utcnow().isoformat() + "Z",
|
||||||
|
"latency_ms": latency,
|
||||||
|
"uptime": uptime,
|
||||||
|
"status_code": status_code,
|
||||||
|
"dns_ms": dns_ms,
|
||||||
|
"ssl_valid": ssl_ok,
|
||||||
|
"ssl_expires": ssl_expires,
|
||||||
|
"ssl_days_remaining": ssl_days,
|
||||||
|
"search_ms": search_ms,
|
||||||
|
"search_status_code": search_code,
|
||||||
|
"overall": uptime and dns_ok and ssl_ok and search_ok,
|
||||||
|
}
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def save_row(row):
|
||||||
|
data = []
|
||||||
|
if os.path.exists(METRICS_FILE):
|
||||||
|
with open(METRICS_FILE, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
data.append(row)
|
||||||
|
|
||||||
|
# keep last 24 hours (1440 checks if every 1 min)
|
||||||
|
if len(data) > 1440:
|
||||||
|
data = data[-1440:]
|
||||||
|
|
||||||
|
with open(METRICS_FILE, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def monitor_loop():
|
||||||
|
while True:
|
||||||
|
row = run_check()
|
||||||
|
save_row(row)
|
||||||
|
print(row)
|
||||||
|
time.sleep(60)
|
||||||
|
|
||||||
|
|
||||||
|
def start_server():
|
||||||
|
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
server = HTTPServer(("0.0.0.0", 5000), SimpleHTTPRequestHandler)
|
||||||
|
print("dashboard: http://0.0.0.0:5000/dashboard.html")
|
||||||
|
server.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
save_row(run_check())
|
||||||
|
threading.Thread(target=monitor_loop, daemon=True).start()
|
||||||
|
start_server()
|
||||||
المرجع في مشكلة جديدة
حظر مستخدم