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()
|
||||
المرجع في مشكلة جديدة
حظر مستخدم