PyObservability 0.0.0a0__py3-none-any.whl → 0.0.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyobservability/main.py +24 -8
- pyobservability/monitor.py +36 -31
- pyobservability/static/app.js +51 -17
- pyobservability/static/styles.css +1 -1
- pyobservability/templates/index.html +4 -3
- pyobservability/version.py +1 -1
- {pyobservability-0.0.0a0.dist-info → pyobservability-0.0.0a1.dist-info}/METADATA +3 -3
- pyobservability-0.0.0a1.dist-info/RECORD +12 -0
- pyobservability-0.0.0a0.dist-info/RECORD +0 -12
- {pyobservability-0.0.0a0.dist-info → pyobservability-0.0.0a1.dist-info}/WHEEL +0 -0
- {pyobservability-0.0.0a0.dist-info → pyobservability-0.0.0a1.dist-info}/licenses/LICENSE +0 -0
- {pyobservability-0.0.0a0.dist-info → pyobservability-0.0.0a1.dist-info}/top_level.txt +0 -0
pyobservability/main.py
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
3
|
import pathlib
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
import dotenv
|
|
6
|
+
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
|
|
5
7
|
from fastapi.staticfiles import StaticFiles
|
|
6
8
|
from fastapi.templating import Jinja2Templates
|
|
9
|
+
|
|
7
10
|
from pyobservability.monitor import Monitor
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
dotenv.load_dotenv()
|
|
13
|
+
|
|
14
|
+
LOGGER = logging.getLogger("uvicorn.default")
|
|
11
15
|
|
|
12
16
|
app = FastAPI(title="Monitor UI")
|
|
13
17
|
root = pathlib.Path(__file__).parent
|
|
@@ -18,13 +22,24 @@ app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
|
|
18
22
|
|
|
19
23
|
monitor = Monitor(poll_interval=float(os.getenv("POLL_INTERVAL", 2)))
|
|
20
24
|
|
|
25
|
+
|
|
21
26
|
@app.get("/")
|
|
22
27
|
async def index(request: Request):
|
|
23
|
-
|
|
28
|
+
"""Pass configured targets to the template so frontend can prebuild UI.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
request: FastAPI request object.
|
|
32
|
+
"""
|
|
24
33
|
return templates.TemplateResponse("index.html", {"request": request, "targets": monitor.targets})
|
|
25
34
|
|
|
35
|
+
|
|
26
36
|
@app.websocket("/ws")
|
|
27
37
|
async def websocket_endpoint(websocket: WebSocket):
|
|
38
|
+
"""Websocket endpoint to render the metrics.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
websocket: FastAPI websocket object.
|
|
42
|
+
"""
|
|
28
43
|
await websocket.accept()
|
|
29
44
|
await monitor.start()
|
|
30
45
|
q = monitor.subscribe()
|
|
@@ -39,6 +54,7 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
|
39
54
|
monitor.unsubscribe(q)
|
|
40
55
|
try:
|
|
41
56
|
await websocket.close()
|
|
42
|
-
except:
|
|
57
|
+
except Exception as err:
|
|
58
|
+
LOGGER.warning(err)
|
|
43
59
|
pass
|
|
44
60
|
await monitor.stop()
|
pyobservability/monitor.py
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# app/monitor.py
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import os
|
|
5
4
|
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from asyncio import CancelledError
|
|
6
8
|
from typing import Any, Dict, List
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
7
11
|
import aiohttp
|
|
8
|
-
from asyncio import CancelledError
|
|
9
12
|
|
|
13
|
+
LOGGER = logging.getLogger("uvicorn.default")
|
|
10
14
|
|
|
11
15
|
###############################################################################
|
|
12
16
|
# ENDPOINT DEFINITIONS (PyNinja Correct)
|
|
@@ -52,7 +56,7 @@ ENDPOINTS = {
|
|
|
52
56
|
"certificates": {
|
|
53
57
|
"path": "/get-certificates",
|
|
54
58
|
"params": {},
|
|
55
|
-
}
|
|
59
|
+
},
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
|
|
@@ -60,7 +64,14 @@ ENDPOINTS = {
|
|
|
60
64
|
# LOAD TARGETS FROM ENV
|
|
61
65
|
###############################################################################
|
|
62
66
|
|
|
67
|
+
|
|
63
68
|
def load_targets_from_env() -> List[Dict[str, Any]]:
|
|
69
|
+
"""Loads monitor targets from environment variables and parses it.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
List[Dict[str, Any]]:
|
|
73
|
+
Returns the parsed list of the monitor targets.
|
|
74
|
+
"""
|
|
64
75
|
raw = os.getenv("MONITOR_TARGETS", "[]")
|
|
65
76
|
|
|
66
77
|
try:
|
|
@@ -74,11 +85,13 @@ def load_targets_from_env() -> List[Dict[str, Any]]:
|
|
|
74
85
|
if isinstance(entry, str):
|
|
75
86
|
parsed.append({"name": entry, "base_url": entry, "apikey": None})
|
|
76
87
|
elif isinstance(entry, dict):
|
|
77
|
-
parsed.append(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
parsed.append(
|
|
89
|
+
{
|
|
90
|
+
"name": entry.get("name") or entry["base_url"],
|
|
91
|
+
"base_url": entry["base_url"],
|
|
92
|
+
"apikey": entry.get("apikey"),
|
|
93
|
+
}
|
|
94
|
+
)
|
|
82
95
|
|
|
83
96
|
return parsed
|
|
84
97
|
|
|
@@ -87,6 +100,7 @@ def load_targets_from_env() -> List[Dict[str, Any]]:
|
|
|
87
100
|
# MONITOR CLASS
|
|
88
101
|
###############################################################################
|
|
89
102
|
|
|
103
|
+
|
|
90
104
|
class Monitor:
|
|
91
105
|
|
|
92
106
|
def __init__(self, poll_interval: float = 2.0):
|
|
@@ -145,11 +159,15 @@ class Monitor:
|
|
|
145
159
|
if resp.status == 200:
|
|
146
160
|
try:
|
|
147
161
|
return await resp.json()
|
|
148
|
-
except Exception:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
162
|
+
except Exception as err:
|
|
163
|
+
LOGGER.debug(err)
|
|
164
|
+
return "NO DATA"
|
|
165
|
+
parsed = urlparse(url)
|
|
166
|
+
LOGGER.debug("Exception on '%s' - [%d]: %s", parsed.path, resp.status, await resp.text())
|
|
167
|
+
return "NO DATA"
|
|
168
|
+
except Exception as err:
|
|
169
|
+
LOGGER.debug(err)
|
|
170
|
+
return "NO DATA"
|
|
153
171
|
|
|
154
172
|
############################################################################
|
|
155
173
|
# PER-TARGET POLLING
|
|
@@ -159,24 +177,14 @@ class Monitor:
|
|
|
159
177
|
apikey = target.get("apikey")
|
|
160
178
|
session = self.sessions[base]
|
|
161
179
|
|
|
162
|
-
result = {
|
|
163
|
-
"name": target["name"],
|
|
164
|
-
"base_url": base,
|
|
165
|
-
"metrics": {}
|
|
166
|
-
}
|
|
180
|
+
result = {"name": target["name"], "base_url": base, "metrics": {}}
|
|
167
181
|
|
|
168
182
|
# Fire ALL requests concurrently
|
|
169
183
|
tasks = {}
|
|
170
184
|
|
|
171
185
|
for key, cfg in ENDPOINTS.items():
|
|
172
186
|
tasks[key] = asyncio.create_task(
|
|
173
|
-
self._fetch(
|
|
174
|
-
session,
|
|
175
|
-
base,
|
|
176
|
-
cfg["path"],
|
|
177
|
-
apikey=apikey,
|
|
178
|
-
params=cfg["params"]
|
|
179
|
-
)
|
|
187
|
+
self._fetch(session, base, cfg["path"], apikey=apikey, params=cfg["params"])
|
|
180
188
|
)
|
|
181
189
|
|
|
182
190
|
# Wait for all endpoints
|
|
@@ -184,7 +192,7 @@ class Monitor:
|
|
|
184
192
|
|
|
185
193
|
for (key, _), resp in zip(tasks.items(), raw_results):
|
|
186
194
|
if isinstance(resp, Exception):
|
|
187
|
-
result["metrics"][key] =
|
|
195
|
+
result["metrics"][key] = "NO DATA"
|
|
188
196
|
continue
|
|
189
197
|
if isinstance(resp, dict):
|
|
190
198
|
result["metrics"][key] = resp.get("detail", resp)
|
|
@@ -203,6 +211,7 @@ class Monitor:
|
|
|
203
211
|
out = []
|
|
204
212
|
for r in results:
|
|
205
213
|
if isinstance(r, Exception):
|
|
214
|
+
LOGGER.error("%s", r)
|
|
206
215
|
out.append({"error": str(r)})
|
|
207
216
|
else:
|
|
208
217
|
out.append(r)
|
|
@@ -215,11 +224,7 @@ class Monitor:
|
|
|
215
224
|
while not self._stop.is_set():
|
|
216
225
|
metrics = await self._poll_all()
|
|
217
226
|
|
|
218
|
-
payload = {
|
|
219
|
-
"type": "metrics",
|
|
220
|
-
"ts": asyncio.get_event_loop().time(),
|
|
221
|
-
"data": metrics
|
|
222
|
-
}
|
|
227
|
+
payload = {"type": "metrics", "ts": asyncio.get_event_loop().time(), "data": metrics}
|
|
223
228
|
|
|
224
229
|
for q in list(self._ws_subscribers):
|
|
225
230
|
try:
|
pyobservability/static/app.js
CHANGED
|
@@ -27,8 +27,9 @@
|
|
|
27
27
|
const servicesTableBody = document.querySelector("#services-table tbody");
|
|
28
28
|
const svcFilter = document.getElementById("svc-filter");
|
|
29
29
|
|
|
30
|
-
const
|
|
31
|
-
const
|
|
30
|
+
const dockerTable = document.getElementById("docker-table");
|
|
31
|
+
const dockerTableHead = dockerTable.querySelector("thead");
|
|
32
|
+
const dockerTableBody = dockerTable.querySelector("tbody");
|
|
32
33
|
|
|
33
34
|
const disksTableBody = document.querySelector("#disks-table tbody");
|
|
34
35
|
const certsEl = document.getElementById("certificates");
|
|
@@ -182,8 +183,8 @@
|
|
|
182
183
|
loadEl.textContent = "—";
|
|
183
184
|
|
|
184
185
|
servicesTableBody.innerHTML = "";
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
dockerTableHead.innerHTML = "";
|
|
187
|
+
dockerTableBody.innerHTML = "";
|
|
187
188
|
disksTableBody.innerHTML = "";
|
|
188
189
|
certsEl.textContent = "—";
|
|
189
190
|
}
|
|
@@ -209,6 +210,19 @@
|
|
|
209
210
|
return Number.isFinite(n) ? n : null;
|
|
210
211
|
}
|
|
211
212
|
|
|
213
|
+
function formatStringOrObject(x) {
|
|
214
|
+
if (x == null) return "—";
|
|
215
|
+
if (typeof x === "string" || typeof x === "number") return x;
|
|
216
|
+
return Object.entries(x)
|
|
217
|
+
.map(([k,v]) => `${k}: ${v}`)
|
|
218
|
+
.join("\n");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function round2(x) {
|
|
222
|
+
const n = Number(x);
|
|
223
|
+
return Number.isFinite(n) ? n.toFixed(2) : "—";
|
|
224
|
+
}
|
|
225
|
+
|
|
212
226
|
// ------------------------------------------------------------
|
|
213
227
|
// METRICS HANDLER
|
|
214
228
|
// ------------------------------------------------------------
|
|
@@ -221,14 +235,22 @@
|
|
|
221
235
|
|
|
222
236
|
// ------------------- BASIC INFO -------------------
|
|
223
237
|
ipEl.textContent = m.ip?.ip || m.ip || "—";
|
|
224
|
-
gpuEl.textContent =
|
|
225
|
-
|
|
238
|
+
gpuEl.textContent = formatStringOrObject(m.gpu ?? m.cpu ?? "—");
|
|
239
|
+
if (m.disk) {
|
|
240
|
+
const total = m.disk.total ?? "—";
|
|
241
|
+
const used = m.disk.used ?? "—";
|
|
242
|
+
const free = m.disk.free ?? "—";
|
|
243
|
+
diskEl.textContent = `Total: ${total}\nUsed: ${used}\nFree: ${free}`;
|
|
244
|
+
} else {
|
|
245
|
+
diskEl.textContent = "—";
|
|
246
|
+
}
|
|
226
247
|
|
|
227
248
|
// ------------------- MEMORY -------------------
|
|
228
249
|
if (m.memory) {
|
|
229
250
|
const used = m.memory.ram_used || m.memory.used || "";
|
|
230
251
|
const percent = m.memory.ram_usage ?? m.memory.usage ?? m.memory.percent ?? "—";
|
|
231
|
-
|
|
252
|
+
const totalMem = m.memory.ram_total ?? m.memory.total ?? "—";
|
|
253
|
+
memEl.textContent = `Total: ${totalMem}\nUsed: ${used}\nPercent: ${percent}%`;
|
|
232
254
|
pushPoint(memChart, num(percent));
|
|
233
255
|
}
|
|
234
256
|
|
|
@@ -278,7 +300,7 @@
|
|
|
278
300
|
const m5 = load.m5 ?? load[1];
|
|
279
301
|
const m15 = load.m15 ?? load[2];
|
|
280
302
|
|
|
281
|
-
loadEl.textContent = `${m1} / ${m5} / ${m15}`;
|
|
303
|
+
loadEl.textContent = `${round2(m1)} / ${round2(m5)} / ${round2(m15)}`;
|
|
282
304
|
pushPoint(loadChart, num(m1) ?? 0);
|
|
283
305
|
} else {
|
|
284
306
|
loadEl.textContent = load;
|
|
@@ -309,16 +331,28 @@
|
|
|
309
331
|
}
|
|
310
332
|
|
|
311
333
|
// ------------------- DOCKER -------------------
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
334
|
+
const dockerList = m.docker_stats;
|
|
335
|
+
|
|
336
|
+
if (Array.isArray(dockerList) && dockerTableHead && dockerTableBody) {
|
|
337
|
+
dockerTableHead.innerHTML = "";
|
|
338
|
+
dockerTableBody.innerHTML = "";
|
|
339
|
+
|
|
340
|
+
if (dockerList.length > 0) {
|
|
341
|
+
// Table header
|
|
342
|
+
const columns = Object.keys(dockerList[0]);
|
|
343
|
+
dockerTableHead.innerHTML =
|
|
344
|
+
"<tr>" + columns.map(c => `<th>${c}</th>`).join("") + "</tr>";
|
|
345
|
+
|
|
346
|
+
// Table rows
|
|
347
|
+
dockerList.forEach(c => {
|
|
348
|
+
const row = "<tr>" +
|
|
349
|
+
columns.map(col => `<td>${c[col] ?? ""}</td>`).join("") +
|
|
350
|
+
"</tr>";
|
|
351
|
+
dockerTableBody.insertAdjacentHTML("beforeend", row);
|
|
352
|
+
});
|
|
321
353
|
}
|
|
354
|
+
} else {
|
|
355
|
+
console.warn("Cannot render Docker table:", dockerList, dockerTableHead, dockerTableBody);
|
|
322
356
|
}
|
|
323
357
|
|
|
324
358
|
// ------------------- DISKS -------------------
|
|
@@ -44,7 +44,7 @@ html,body { height:100%; margin:0; font-family: Inter, system-ui, -apple-system,
|
|
|
44
44
|
.core-mini .label { color:var(--muted); font-size:11px; }
|
|
45
45
|
.core-mini .value { font-weight:700; margin-top:6px; }
|
|
46
46
|
|
|
47
|
-
.tables-row { display:grid; grid-template-columns:
|
|
47
|
+
.tables-row { display:grid; grid-template-columns:1fr; grid-auto-rows:auto; gap:12px; }
|
|
48
48
|
.table { width:100%; border-collapse:collapse; font-size:13px; }
|
|
49
49
|
.table th, .table td { padding:8px; border-bottom:1px solid rgba(255,255,255,0.03); text-align:left; }
|
|
50
50
|
.pre { background:rgba(255,255,255,0.02); padding:8px; border-radius:6px; overflow:auto; white-space:pre-wrap; font-family:monospace; font-size:13px; }
|
|
@@ -91,9 +91,10 @@
|
|
|
91
91
|
<div class="panel">
|
|
92
92
|
<div class="panel-header"><h3>Docker / Containers</h3></div>
|
|
93
93
|
<div class="panel-body">
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
<table class="table" id="docker-table">
|
|
95
|
+
<thead></thead>
|
|
96
|
+
<tbody></tbody>
|
|
97
|
+
</table>
|
|
97
98
|
</div>
|
|
98
99
|
</div>
|
|
99
100
|
|
pyobservability/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.0.
|
|
1
|
+
__version__ = "0.0.0a1"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyObservability
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.0a1
|
|
4
4
|
Summary: Lightweight OS-agnostic observability UI for PyNinja
|
|
5
5
|
Author-email: Vignesh Rao <svignesh1793@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -41,11 +41,11 @@ Classifier: Topic :: System :: Monitoring
|
|
|
41
41
|
Requires-Python: >=3.11
|
|
42
42
|
Description-Content-Type: text/markdown
|
|
43
43
|
License-File: LICENSE
|
|
44
|
-
Requires-Dist: fastapi
|
|
45
|
-
Requires-Dist: uvicorn[standard]
|
|
46
44
|
Requires-Dist: aiohttp
|
|
45
|
+
Requires-Dist: fastapi
|
|
47
46
|
Requires-Dist: jinja2
|
|
48
47
|
Requires-Dist: python-dotenv
|
|
48
|
+
Requires-Dist: uvicorn[standard]
|
|
49
49
|
Provides-Extra: dev
|
|
50
50
|
Requires-Dist: sphinx==5.1.1; extra == "dev"
|
|
51
51
|
Requires-Dist: pre-commit; extra == "dev"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pyobservability/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
pyobservability/main.py,sha256=XzH2XNuxU9lbFSnQXsJqJ2ytrd6nnzbTf7B0n0UQuW0,1637
|
|
3
|
+
pyobservability/monitor.py,sha256=nnL6odqO3BF-XH1wek0leyMKXw7NB8qrwp6aAh-xJyA,7694
|
|
4
|
+
pyobservability/version.py,sha256=PvT0JaYWREcshCnUiHC2CMpUMuTV359GmoET7cuhPiU,24
|
|
5
|
+
pyobservability/static/app.js,sha256=poc7eReoiRUbyI5JKnPwxSqmSNCuOge4aZKCITFy7eo,13494
|
|
6
|
+
pyobservability/static/styles.css,sha256=t6r1C0ueBanipgRRjdu18nmq6RbSGLK5bhpf0BdMOpQ,3245
|
|
7
|
+
pyobservability/templates/index.html,sha256=PsN3aq-7Q6RzGeshoNH5v37G3sHoI2saJq6mfuA6JYs,3977
|
|
8
|
+
pyobservability-0.0.0a1.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
|
|
9
|
+
pyobservability-0.0.0a1.dist-info/METADATA,sha256=Ze1eq09JsbqSTcpPFIrvMQxKJubLqKxzAxcMhLIzbzw,2663
|
|
10
|
+
pyobservability-0.0.0a1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
+
pyobservability-0.0.0a1.dist-info/top_level.txt,sha256=p20T0EmihDYW1uMintRXr7X9bg3XWYKyoSbBHOVC1xI,16
|
|
12
|
+
pyobservability-0.0.0a1.dist-info/RECORD,,
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
pyobservability/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
pyobservability/main.py,sha256=yegGcq_OTHGIeJOiJecxnNSBYQ_Hf2MRjJs4KgNZsrQ,1385
|
|
3
|
-
pyobservability/monitor.py,sha256=H_8-c5_S0QNWlGnMktEm1Z64T8HK2nXQ803V1scr6Ok,7393
|
|
4
|
-
pyobservability/version.py,sha256=MVtVCT-b01z6f8XUlOsWWZynQ9Wv001Mk-_erJHty6Y,24
|
|
5
|
-
pyobservability/static/app.js,sha256=dLxyB25ttKaSE1RLpCaqeLXRRwcWyre0icMDZUV6CvU,12351
|
|
6
|
-
pyobservability/static/styles.css,sha256=lUQ74-frlBuNcf6u_8KHUKcNsdMmb0vSxBBaDLPwGEI,3236
|
|
7
|
-
pyobservability/templates/index.html,sha256=6x_fCqTcgcA7eInq49VMsoAGW-Ks80k0by8LeP1rkvI,3988
|
|
8
|
-
pyobservability-0.0.0a0.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
|
|
9
|
-
pyobservability-0.0.0a0.dist-info/METADATA,sha256=E_g2OFzx8C103vvcVThDjaVvJjpBsj86Y9caIH4EZ6E,2663
|
|
10
|
-
pyobservability-0.0.0a0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
-
pyobservability-0.0.0a0.dist-info/top_level.txt,sha256=p20T0EmihDYW1uMintRXr7X9bg3XWYKyoSbBHOVC1xI,16
|
|
12
|
-
pyobservability-0.0.0a0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|