Complete Ghaymah cloud assessment
هذا الالتزام موجود في:
625
q1-deploy-monitor/monitor/data/checks.json
Normal file
625
q1-deploy-monitor/monitor/data/checks.json
Normal file
@@ -0,0 +1,625 @@
|
||||
[
|
||||
{
|
||||
"ts": "2026-07-26T16:00:54.022563+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 48.44,
|
||||
"requests": 5
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:01:24.088718+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 12.02,
|
||||
"requests": 8
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:01:54.237407+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 24.42,
|
||||
"requests": 11
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:02:24.326513+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 10.23,
|
||||
"requests": 14
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:02:54.359082+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 8.32,
|
||||
"requests": 19
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:03:24.389873+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 7.83,
|
||||
"requests": 22
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:03:54.430952+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 11.57,
|
||||
"requests": 25
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:04:24.463587+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 8.01,
|
||||
"requests": 28
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:04:54.502315+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 11.91,
|
||||
"requests": 31
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:05:24.545377+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 7.72,
|
||||
"requests": 34
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:05:54.596001+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 8.86,
|
||||
"requests": 37
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:06:24.623029+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 8.53,
|
||||
"requests": 40
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:06:54.659187+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 8.9,
|
||||
"requests": 43
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:07:24.697242+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 11.24,
|
||||
"requests": 46
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:07:54.732633+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 8.15,
|
||||
"requests": 49
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:08:24.764311+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 7.73,
|
||||
"requests": 52
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:08:54.794053+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 7.53,
|
||||
"requests": 55
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:09:24.830576+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 10.88,
|
||||
"requests": 58
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:09:55.392240+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 9.02,
|
||||
"requests": 61
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:10:25.432620+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 7.75,
|
||||
"requests": 64
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:10:55.469947+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 9.34,
|
||||
"requests": 67
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:11:25.502913+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 7.92,
|
||||
"requests": 70
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:11:55.542608+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 11.26,
|
||||
"requests": 73
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:12:25.579631+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 10.91,
|
||||
"requests": 76
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:12:55.621565+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 8.02,
|
||||
"requests": 79
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:13:25.651738+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 7.58,
|
||||
"requests": 82
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:13:55.683853+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 9.19,
|
||||
"requests": 85
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:14:25.720913+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 10.67,
|
||||
"requests": 88
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:14:55.754438+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 8.68,
|
||||
"requests": 91
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:15:25.795149+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 9.94,
|
||||
"requests": 94
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:15:55.835606+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 8.67,
|
||||
"requests": 97
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:16:25.894224+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 21.72,
|
||||
"requests": 100
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:16:55.947740+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 8.81,
|
||||
"requests": 103
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:17:25.993670+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 14.76,
|
||||
"requests": 106
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:17:56.027955+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 9.95,
|
||||
"requests": 109
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:18:26.076150+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 16.53,
|
||||
"requests": 112
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:18:56.130454+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 13.91,
|
||||
"requests": 115
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:19:26.181118+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 14.55,
|
||||
"requests": 118
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:19:56.239466+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 15.54,
|
||||
"requests": 121
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:20:26.273327+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 12.98,
|
||||
"requests": 124
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:20:56.326056+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 10.54,
|
||||
"requests": 127
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:21:26.360754+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 8.28,
|
||||
"requests": 130
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:21:56.419699+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 24.33,
|
||||
"requests": 133
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:22:26.474809+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 16.0,
|
||||
"requests": 136
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:22:56.537278+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 25.59,
|
||||
"requests": 139
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:23:26.588750+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 11.74,
|
||||
"requests": 142
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:23:56.625763+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 11.76,
|
||||
"requests": 145
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:24:26.678666+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 21.03,
|
||||
"requests": 148
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:24:56.718058+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 9.81,
|
||||
"requests": 151
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:25:26.781135+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 26.99,
|
||||
"requests": 154
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:25:56.837517+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 17.22,
|
||||
"requests": 157
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:26:26.885310+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 15.99,
|
||||
"requests": 160
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:26:56.931050+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 12.71,
|
||||
"requests": 163
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:27:26.978657+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 15.71,
|
||||
"requests": 166
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:27:57.027222+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 15.87,
|
||||
"requests": 169
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:28:27.076324+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 11.98,
|
||||
"requests": 172
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:28:57.144076+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 24.72,
|
||||
"requests": 175
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:29:27.203425+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 20.56,
|
||||
"requests": 178
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:29:57.243755+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 7.91,
|
||||
"requests": 181
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:30:27.305659+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 19.38,
|
||||
"requests": 184
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:30:57.380343+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 37.99,
|
||||
"requests": 187
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:31:27.412862+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 8.02,
|
||||
"requests": 190
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:31:57.458486+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 16.76,
|
||||
"requests": 193
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:32:27.506190+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 10.54,
|
||||
"requests": 196
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:32:57.558316+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 19.22,
|
||||
"requests": 199
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:33:27.636370+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 18.47,
|
||||
"requests": 202
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:33:57.692190+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 20.24,
|
||||
"requests": 205
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:34:27.737228+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 10.93,
|
||||
"requests": 208
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:34:57.800304+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 22.88,
|
||||
"requests": 211
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:35:27.861991+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 18.19,
|
||||
"requests": 214
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:35:57.925683+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 19.93,
|
||||
"requests": 217
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:36:27.992496+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 18.07,
|
||||
"requests": 220
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:36:58.067518+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 18.62,
|
||||
"requests": 223
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:37:28.127809+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 20.59,
|
||||
"requests": 226
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:37:58.166210+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 9.57,
|
||||
"requests": 229
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:38:28.201786+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 7.95,
|
||||
"requests": 232
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:38:58.237767+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 8.12,
|
||||
"requests": 235
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:39:28.279617+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 11.27,
|
||||
"requests": 238
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:39:58.317037+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 7.61,
|
||||
"requests": 241
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:40:28.355043+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 9.57,
|
||||
"requests": 244
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:40:58.391288+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 9.27,
|
||||
"requests": 247
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:41:28.432715+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 13.3,
|
||||
"requests": 250
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:41:58.503792+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 26.98,
|
||||
"requests": 253
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:42:28.608440+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 19.86,
|
||||
"requests": 256
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:42:58.693278+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 12.8,
|
||||
"requests": 259
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:43:28.797562+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 42.28,
|
||||
"requests": 262
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:43:58.874830+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 8.97,
|
||||
"requests": 265
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:44:28.919903+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 13.56,
|
||||
"requests": 268
|
||||
},
|
||||
{
|
||||
"ts": "2026-07-26T16:44:59.037760+00:00",
|
||||
"status": "up",
|
||||
"code": 200,
|
||||
"latency_ms": 34.65,
|
||||
"requests": 271
|
||||
}
|
||||
]
|
||||
204
q1-deploy-monitor/monitor/monitor.py
Normal file
204
q1-deploy-monitor/monitor/monitor.py
Normal file
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Uptime monitor for the API deployed on Ghaymah.
|
||||
|
||||
Polls ``$APP_URL/health`` (and ``/metrics``) every 30 seconds and appends one
|
||||
record per check to ``monitor/data/checks.json`` — the file the dashboard reads.
|
||||
|
||||
Record schema:
|
||||
{"ts": "<iso8601>", "status": "up"|"down", "code": <int|null>,
|
||||
"latency_ms": <float|null>, "requests": <int|null>}
|
||||
|
||||
Usage:
|
||||
APP_URL=https://my-app.example python monitor.py
|
||||
APP_URL=https://my-app.example python monitor.py --once
|
||||
|
||||
Standard library only — no pip install needed to run the monitor.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
# --- Configuration (env-overridable) -----------------------------------------
|
||||
|
||||
APP_URL = os.getenv("APP_URL", "").rstrip("/")
|
||||
INTERVAL_S = float(os.getenv("INTERVAL_S", "30"))
|
||||
TIMEOUT_S = float(os.getenv("TIMEOUT_S", "5"))
|
||||
ALERT_AFTER = int(os.getenv("ALERT_AFTER", "3"))
|
||||
|
||||
DEFAULT_DATA_FILE = Path(__file__).resolve().parent / "data" / "checks.json"
|
||||
DATA_FILE = Path(os.getenv("DATA_FILE", str(DEFAULT_DATA_FILE)))
|
||||
|
||||
# Keep the JSON array bounded so the dashboard stays fast and the file small.
|
||||
MAX_RECORDS = int(os.getenv("MAX_RECORDS", "2880")) # 24h at one check / 30s
|
||||
|
||||
|
||||
def now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def fetch_json(url: str, timeout: float):
|
||||
"""GET a URL and parse JSON. Returns (status_code, parsed_body).
|
||||
|
||||
Raises on timeout, connection failure, or non-2xx status.
|
||||
"""
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "ghaymah-monitor/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
raw = resp.read().decode("utf-8", errors="replace")
|
||||
try:
|
||||
body = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
body = None
|
||||
return resp.status, body
|
||||
|
||||
|
||||
def check_once(app_url: str) -> dict:
|
||||
"""Run one health + metrics check and return the record to persist."""
|
||||
health_url = f"{app_url}/health"
|
||||
metrics_url = f"{app_url}/metrics"
|
||||
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
code, _body = fetch_json(health_url, TIMEOUT_S)
|
||||
latency_ms = round((time.perf_counter() - started) * 1000, 2)
|
||||
status = "up"
|
||||
except urllib.error.HTTPError as exc:
|
||||
# The app answered, just not with a healthy status — record the code.
|
||||
return {
|
||||
"ts": now_iso(),
|
||||
"status": "down",
|
||||
"code": exc.code,
|
||||
"latency_ms": round((time.perf_counter() - started) * 1000, 2),
|
||||
"requests": None,
|
||||
}
|
||||
except Exception:
|
||||
# Timeout, DNS failure, connection refused, TLS error, ...
|
||||
return {
|
||||
"ts": now_iso(),
|
||||
"status": "down",
|
||||
"code": None,
|
||||
"latency_ms": None,
|
||||
"requests": None,
|
||||
}
|
||||
|
||||
# /metrics is best-effort: a failure there must not turn a healthy app "down".
|
||||
requests_total = None
|
||||
try:
|
||||
_mcode, mbody = fetch_json(metrics_url, TIMEOUT_S)
|
||||
if isinstance(mbody, dict):
|
||||
value = mbody.get("requests_total")
|
||||
if isinstance(value, int):
|
||||
requests_total = value
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"ts": now_iso(),
|
||||
"status": status,
|
||||
"code": code,
|
||||
"latency_ms": latency_ms,
|
||||
"requests": requests_total,
|
||||
}
|
||||
|
||||
|
||||
def load_records(path: Path) -> list:
|
||||
"""Read the existing JSON array; tolerate a missing or corrupt file."""
|
||||
if not path.exists():
|
||||
return []
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
return data if isinstance(data, list) else []
|
||||
except (json.JSONDecodeError, OSError):
|
||||
print(f"[warn] {path} unreadable or corrupt — starting a fresh array", file=sys.stderr)
|
||||
return []
|
||||
|
||||
|
||||
def append_record(path: Path, record: dict) -> None:
|
||||
"""Append one record to the JSON array, writing atomically."""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
records = load_records(path)
|
||||
records.append(record)
|
||||
if len(records) > MAX_RECORDS:
|
||||
records = records[-MAX_RECORDS:]
|
||||
|
||||
tmp = path.with_suffix(path.suffix + ".tmp")
|
||||
with tmp.open("w", encoding="utf-8") as fh:
|
||||
json.dump(records, fh, indent=2)
|
||||
fh.write("\n")
|
||||
tmp.replace(path)
|
||||
|
||||
|
||||
def format_line(record: dict) -> str:
|
||||
latency = f"{record['latency_ms']:.1f}ms" if record["latency_ms"] is not None else " - "
|
||||
code = record["code"] if record["code"] is not None else "---"
|
||||
reqs = record["requests"] if record["requests"] is not None else "-"
|
||||
return (
|
||||
f"{record['ts']} {record['status'].upper():<4} "
|
||||
f"code={code:<4} latency={latency:<9} requests={reqs}"
|
||||
)
|
||||
|
||||
|
||||
def run(app_url: str, once: bool) -> int:
|
||||
consecutive_failures = 0
|
||||
alerted = False
|
||||
|
||||
while True:
|
||||
record = check_once(app_url)
|
||||
append_record(DATA_FILE, record)
|
||||
print(format_line(record), flush=True)
|
||||
|
||||
if record["status"] == "down":
|
||||
consecutive_failures += 1
|
||||
if consecutive_failures >= ALERT_AFTER and not alerted:
|
||||
print(
|
||||
f"ALERT: {app_url} has failed {consecutive_failures} consecutive "
|
||||
f"health checks (since {record['ts']})",
|
||||
flush=True,
|
||||
)
|
||||
alerted = True
|
||||
else:
|
||||
if alerted:
|
||||
print(f"RECOVERED: {app_url} is responding again at {record['ts']}", flush=True)
|
||||
consecutive_failures = 0
|
||||
alerted = False
|
||||
|
||||
if once:
|
||||
return 0 if record["status"] == "up" else 1
|
||||
|
||||
time.sleep(INTERVAL_S)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Monitor the Ghaymah-deployed API.")
|
||||
parser.add_argument("--once", action="store_true", help="run a single check and exit")
|
||||
parser.add_argument("--url", default=APP_URL, help="app base URL (default: $APP_URL)")
|
||||
args = parser.parse_args()
|
||||
|
||||
app_url = (args.url or "").rstrip("/")
|
||||
if not app_url:
|
||||
print(
|
||||
"error: no app URL. Set APP_URL, e.g.\n"
|
||||
" APP_URL=https://my-app.ghaymah.example python monitor.py",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
||||
print(f"Monitoring {app_url} every {INTERVAL_S:g}s (timeout {TIMEOUT_S:g}s)", flush=True)
|
||||
print(f"Writing checks to {DATA_FILE}", flush=True)
|
||||
|
||||
try:
|
||||
return run(app_url, args.once)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
المرجع في مشكلة جديدة
حظر مستخدم