PyObservability 0.0.3__py3-none-any.whl → 0.1.0__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 +6 -3
- pyobservability/monitor.py +67 -144
- pyobservability/static/app.js +201 -95
- pyobservability/static/styles.css +4 -1
- pyobservability/templates/index.html +37 -6
- pyobservability/version.py +1 -1
- {pyobservability-0.0.3.dist-info → pyobservability-0.1.0.dist-info}/METADATA +1 -1
- pyobservability-0.1.0.dist-info/RECORD +15 -0
- pyobservability-0.0.3.dist-info/RECORD +0 -15
- {pyobservability-0.0.3.dist-info → pyobservability-0.1.0.dist-info}/WHEEL +0 -0
- {pyobservability-0.0.3.dist-info → pyobservability-0.1.0.dist-info}/entry_points.txt +0 -0
- {pyobservability-0.0.3.dist-info → pyobservability-0.1.0.dist-info}/licenses/LICENSE +0 -0
- {pyobservability-0.0.3.dist-info → pyobservability-0.1.0.dist-info}/top_level.txt +0 -0
pyobservability/main.py
CHANGED
|
@@ -2,6 +2,7 @@ import logging
|
|
|
2
2
|
import pathlib
|
|
3
3
|
import warnings
|
|
4
4
|
|
|
5
|
+
import time
|
|
5
6
|
import uiauth
|
|
6
7
|
import uvicorn
|
|
7
8
|
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
|
|
@@ -47,7 +48,11 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
|
47
48
|
q = monitor.subscribe()
|
|
48
49
|
try:
|
|
49
50
|
while True:
|
|
51
|
+
start = time.time()
|
|
50
52
|
payload = await q.get()
|
|
53
|
+
end = time.time()
|
|
54
|
+
nodes = [d['name'] for d in payload["data"]]
|
|
55
|
+
LOGGER.debug("Payload generated in %s - %d %s", end - start, len(nodes), nodes)
|
|
51
56
|
# send as JSON text
|
|
52
57
|
await websocket.send_json(payload)
|
|
53
58
|
except WebSocketDisconnect:
|
|
@@ -100,9 +105,7 @@ def include_routes():
|
|
|
100
105
|
|
|
101
106
|
def start(**kwargs):
|
|
102
107
|
settings.env = settings.env_loader(**kwargs)
|
|
103
|
-
settings.env.targets = [
|
|
104
|
-
{k: str(v) for k, v in target.model_dump().items()} for target in settings.env.targets
|
|
105
|
-
]
|
|
108
|
+
settings.env.targets = [{k: str(v) for k, v in target.model_dump().items()} for target in settings.env.targets]
|
|
106
109
|
include_routes()
|
|
107
110
|
uvicorn_args = dict(
|
|
108
111
|
host=settings.env.host,
|
pyobservability/monitor.py
CHANGED
|
@@ -1,97 +1,53 @@
|
|
|
1
|
-
# app/monitor.py
|
|
2
|
-
|
|
3
1
|
import asyncio
|
|
2
|
+
import json
|
|
4
3
|
import logging
|
|
5
4
|
from asyncio import CancelledError
|
|
6
|
-
from typing import
|
|
7
|
-
from urllib.parse import urlparse
|
|
5
|
+
from typing import Dict, List
|
|
8
6
|
|
|
9
7
|
import aiohttp
|
|
10
8
|
|
|
11
9
|
LOGGER = logging.getLogger("uvicorn.default")
|
|
12
|
-
|
|
13
|
-
###############################################################################
|
|
14
|
-
# ENDPOINT DEFINITIONS (PyNinja Correct)
|
|
15
|
-
###############################################################################
|
|
16
|
-
|
|
17
|
-
ENDPOINTS = {
|
|
18
|
-
"ip": {
|
|
19
|
-
"path": "/get-ip",
|
|
20
|
-
"params": {"public": "false"},
|
|
21
|
-
},
|
|
22
|
-
"cpu": {
|
|
23
|
-
"path": "/get-cpu",
|
|
24
|
-
"params": {"interval": 2, "per_cpu": "true"},
|
|
25
|
-
},
|
|
26
|
-
"cpu_load": {
|
|
27
|
-
"path": "/get-cpu-load",
|
|
28
|
-
"params": {},
|
|
29
|
-
},
|
|
30
|
-
"gpu": {
|
|
31
|
-
"path": "/get-processor",
|
|
32
|
-
"params": {},
|
|
33
|
-
},
|
|
34
|
-
"memory": {
|
|
35
|
-
"path": "/get-memory",
|
|
36
|
-
"params": {},
|
|
37
|
-
},
|
|
38
|
-
"disk": {
|
|
39
|
-
"path": "/get-disk-utilization",
|
|
40
|
-
"params": {"path": "/"},
|
|
41
|
-
},
|
|
42
|
-
"disks": {
|
|
43
|
-
"path": "/get-all-disks",
|
|
44
|
-
"params": {},
|
|
45
|
-
},
|
|
46
|
-
"services": {
|
|
47
|
-
"path": "/get-all-services",
|
|
48
|
-
"params": {},
|
|
49
|
-
},
|
|
50
|
-
"docker_stats": {
|
|
51
|
-
"path": "/get-docker-stats",
|
|
52
|
-
"params": {},
|
|
53
|
-
},
|
|
54
|
-
"certificates": {
|
|
55
|
-
"path": "/get-certificates",
|
|
56
|
-
"params": {},
|
|
57
|
-
},
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
###############################################################################
|
|
62
|
-
# MONITOR CLASS
|
|
63
|
-
###############################################################################
|
|
10
|
+
OBS_PATH = "/observability"
|
|
64
11
|
|
|
65
12
|
|
|
66
13
|
class Monitor:
|
|
67
|
-
|
|
68
14
|
def __init__(self, targets: List[Dict[str, str]], poll_interval: float):
|
|
69
15
|
self.targets = targets
|
|
70
16
|
self.poll_interval = poll_interval
|
|
71
17
|
self.sessions: Dict[str, aiohttp.ClientSession] = {}
|
|
72
18
|
self._ws_subscribers: List[asyncio.Queue] = []
|
|
73
|
-
self._task = None
|
|
19
|
+
self._task: asyncio.Task | None = None
|
|
74
20
|
self._stop = asyncio.Event()
|
|
75
21
|
|
|
76
22
|
############################################################################
|
|
77
23
|
# LIFECYCLE
|
|
78
24
|
############################################################################
|
|
79
25
|
async def start(self):
|
|
26
|
+
self._tasks = []
|
|
27
|
+
|
|
80
28
|
for target in self.targets:
|
|
81
|
-
|
|
82
|
-
|
|
29
|
+
base = target["base_url"]
|
|
30
|
+
name = target.get("name")
|
|
31
|
+
apikey = target.get("apikey")
|
|
32
|
+
|
|
33
|
+
session = aiohttp.ClientSession()
|
|
34
|
+
self.sessions[base] = session
|
|
35
|
+
|
|
36
|
+
task = asyncio.create_task(self._stream_target(name, base, session, apikey))
|
|
37
|
+
self._tasks.append(task)
|
|
83
38
|
|
|
84
39
|
async def stop(self):
|
|
85
40
|
self._stop.set()
|
|
86
|
-
|
|
87
|
-
|
|
41
|
+
|
|
42
|
+
for task in self._tasks:
|
|
43
|
+
task.cancel()
|
|
88
44
|
try:
|
|
89
|
-
await
|
|
45
|
+
await task
|
|
90
46
|
except CancelledError:
|
|
91
47
|
pass
|
|
92
48
|
|
|
93
|
-
for
|
|
94
|
-
await
|
|
49
|
+
for session in self.sessions.values():
|
|
50
|
+
await session.close()
|
|
95
51
|
|
|
96
52
|
############################################################################
|
|
97
53
|
# SUBSCRIBE / UNSUBSCRIBE
|
|
@@ -108,91 +64,58 @@ class Monitor:
|
|
|
108
64
|
pass
|
|
109
65
|
|
|
110
66
|
############################################################################
|
|
111
|
-
# FETCH
|
|
67
|
+
# FETCH OBSERVABILITY FOR SINGLE TARGET
|
|
112
68
|
############################################################################
|
|
113
|
-
async def
|
|
114
|
-
url = base_url.rstrip("/") +
|
|
69
|
+
async def _fetch_observability(self, session, base_url, apikey):
|
|
70
|
+
url = base_url.rstrip("/") + OBS_PATH + f"?interval={self.poll_interval}"
|
|
71
|
+
headers = {"accept": "application/json"}
|
|
72
|
+
if apikey:
|
|
73
|
+
headers["Authorization"] = f"Bearer {apikey}"
|
|
74
|
+
|
|
115
75
|
try:
|
|
116
|
-
async with session.get(url, headers=headers,
|
|
117
|
-
if resp.status
|
|
76
|
+
async with session.get(url, headers=headers, timeout=None) as resp:
|
|
77
|
+
if resp.status != 200:
|
|
78
|
+
LOGGER.debug("Exception - [%d]: %s", resp.status, await resp.text())
|
|
79
|
+
return
|
|
80
|
+
async for line in resp.content:
|
|
81
|
+
line = line.decode().strip()
|
|
82
|
+
if not line:
|
|
83
|
+
continue
|
|
118
84
|
try:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
LOGGER.debug("Exception on '%s' - [%d]: %s", parsed.path, resp.status, await resp.text())
|
|
125
|
-
return
|
|
85
|
+
payload = json.loads(line)
|
|
86
|
+
# yield each record instead of returning
|
|
87
|
+
yield payload
|
|
88
|
+
except json.JSONDecodeError as err:
|
|
89
|
+
LOGGER.debug("JSON decode error: %s | line=%s", err, line)
|
|
126
90
|
except Exception as err:
|
|
127
|
-
LOGGER.debug(err)
|
|
91
|
+
LOGGER.debug("Exception: %s", err)
|
|
128
92
|
return
|
|
129
93
|
|
|
130
94
|
############################################################################
|
|
131
|
-
#
|
|
95
|
+
# STREAM A SPECIFIC TARGET - SEQUENTIAL OVER ALL TARGETS
|
|
132
96
|
############################################################################
|
|
133
|
-
async def
|
|
134
|
-
base = target["base_url"]
|
|
135
|
-
apikey = target["apikey"]
|
|
136
|
-
session = self.sessions[base]
|
|
137
|
-
headers = {"Accept": "application/json", "Authorization": f"Bearer {apikey}"}
|
|
138
|
-
|
|
139
|
-
result = {"name": target["name"], "base_url": base, "metrics": {}}
|
|
140
|
-
|
|
141
|
-
# Fire ALL requests concurrently
|
|
142
|
-
tasks = {}
|
|
143
|
-
|
|
144
|
-
for key, cfg in ENDPOINTS.items():
|
|
145
|
-
tasks[key] = asyncio.create_task(
|
|
146
|
-
self._fetch(session, base, cfg["path"], headers=headers, params=cfg["params"])
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
# Wait for all endpoints
|
|
150
|
-
raw_results = await asyncio.gather(*tasks.values(), return_exceptions=True)
|
|
151
|
-
|
|
152
|
-
for (key, _), resp in zip(tasks.items(), raw_results):
|
|
153
|
-
if isinstance(resp, Exception):
|
|
154
|
-
result["metrics"][key] = None
|
|
155
|
-
continue
|
|
156
|
-
if isinstance(resp, dict):
|
|
157
|
-
result["metrics"][key] = resp.get("detail", resp)
|
|
158
|
-
else:
|
|
159
|
-
# raw string / number / list / etc
|
|
160
|
-
result["metrics"][key] = resp
|
|
161
|
-
|
|
162
|
-
return result
|
|
163
|
-
|
|
164
|
-
############################################################################
|
|
165
|
-
# POLL ALL HOSTS
|
|
166
|
-
############################################################################
|
|
167
|
-
async def _poll_all(self) -> List[Dict[str, Any]]:
|
|
168
|
-
tasks = [self._poll_target(target) for target in self.targets]
|
|
169
|
-
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
170
|
-
out = []
|
|
171
|
-
for r in results:
|
|
172
|
-
if isinstance(r, Exception):
|
|
173
|
-
LOGGER.error("%s", r)
|
|
174
|
-
out.append({"error": str(r)})
|
|
175
|
-
else:
|
|
176
|
-
out.append(r)
|
|
177
|
-
return out
|
|
178
|
-
|
|
179
|
-
############################################################################
|
|
180
|
-
# MAIN LOOP
|
|
181
|
-
############################################################################
|
|
182
|
-
async def _run_loop(self):
|
|
97
|
+
async def _stream_target(self, name, base, session, apikey):
|
|
183
98
|
while not self._stop.is_set():
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
99
|
+
try:
|
|
100
|
+
async for payload in self._fetch_observability(session, base, apikey):
|
|
101
|
+
result = {
|
|
102
|
+
"type": "metrics",
|
|
103
|
+
"ts": asyncio.get_event_loop().time(),
|
|
104
|
+
"data": [{"name": name, "base_url": base, "metrics": payload}],
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# The queue gets full when subscribers aren’t reading fast enough
|
|
108
|
+
# [OR]
|
|
109
|
+
# The monitor produces new metrics faster than the queue’s fixed maxsize of 10 can be consumed
|
|
110
|
+
for q in list(self._ws_subscribers):
|
|
111
|
+
try:
|
|
112
|
+
q.put_nowait(result)
|
|
113
|
+
except asyncio.QueueFull:
|
|
114
|
+
try:
|
|
115
|
+
_ = q.get_nowait()
|
|
116
|
+
q.put_nowait(result)
|
|
117
|
+
except Exception as debug:
|
|
118
|
+
LOGGER.debug(f"Queue full: {debug}")
|
|
119
|
+
except Exception as debug:
|
|
120
|
+
LOGGER.debug(f"Stream failed for {base}, retrying: {debug}")
|
|
121
|
+
await asyncio.sleep(1)
|
pyobservability/static/app.js
CHANGED
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
const nodeSelect = document.getElementById("node-select");
|
|
13
13
|
const refreshBtn = document.getElementById("refresh-btn");
|
|
14
14
|
|
|
15
|
-
const
|
|
16
|
-
const
|
|
15
|
+
const systemEl = document.getElementById("system");
|
|
16
|
+
const ipEl = document.getElementById("ip-info");
|
|
17
|
+
const processorEl = document.getElementById("processor");
|
|
18
|
+
|
|
17
19
|
const memEl = document.getElementById("memory");
|
|
18
20
|
const diskEl = document.getElementById("disk");
|
|
19
21
|
const loadEl = document.getElementById("cpuload");
|
|
@@ -27,12 +29,24 @@
|
|
|
27
29
|
const servicesTableBody = document.querySelector("#services-table tbody");
|
|
28
30
|
const svcFilter = document.getElementById("svc-filter");
|
|
29
31
|
|
|
32
|
+
const processesTableBody = document.querySelector("#processes-table tbody");
|
|
33
|
+
const procFilter = document.getElementById("proc-filter");
|
|
34
|
+
|
|
30
35
|
const dockerTable = document.getElementById("docker-table");
|
|
31
36
|
const dockerTableHead = dockerTable.querySelector("thead");
|
|
32
37
|
const dockerTableBody = dockerTable.querySelector("tbody");
|
|
33
38
|
|
|
34
|
-
const
|
|
35
|
-
const
|
|
39
|
+
const disksTable = document.getElementById("disks-table");
|
|
40
|
+
const disksTableHead = disksTable.querySelector("thead")
|
|
41
|
+
const disksTableBody = disksTable.querySelector("tbody");
|
|
42
|
+
|
|
43
|
+
const pyudiskTable = document.getElementById("pyudisk-table")
|
|
44
|
+
const pyudiskTableHead = pyudiskTable.querySelector("thead")
|
|
45
|
+
const pyudiskTableBody = pyudiskTable.querySelector("tbody")
|
|
46
|
+
|
|
47
|
+
const certsTable = document.getElementById("certificates-table")
|
|
48
|
+
const certsTableHead = certsTable.querySelector("thead");
|
|
49
|
+
const certsTableBody = certsTable.querySelector("tbody");
|
|
36
50
|
|
|
37
51
|
const showCoresCheckbox = document.getElementById("show-cores");
|
|
38
52
|
|
|
@@ -176,17 +190,27 @@
|
|
|
176
190
|
}
|
|
177
191
|
|
|
178
192
|
// Reset static UI fields
|
|
193
|
+
systemEl.textContent = "-";
|
|
179
194
|
ipEl.textContent = "—";
|
|
180
|
-
|
|
195
|
+
processorEl.textContent = "—";
|
|
181
196
|
memEl.textContent = "—";
|
|
182
197
|
diskEl.textContent = "—";
|
|
183
198
|
loadEl.textContent = "—";
|
|
184
199
|
|
|
185
200
|
servicesTableBody.innerHTML = "";
|
|
201
|
+
processesTableBody.innerHTML = "";
|
|
202
|
+
|
|
186
203
|
dockerTableHead.innerHTML = "";
|
|
187
204
|
dockerTableBody.innerHTML = "";
|
|
205
|
+
|
|
206
|
+
disksTableHead.innerHTML = "";
|
|
188
207
|
disksTableBody.innerHTML = "";
|
|
189
|
-
|
|
208
|
+
|
|
209
|
+
pyudiskTableHead.innerHTML = "";
|
|
210
|
+
pyudiskTableBody.innerHTML = "";
|
|
211
|
+
|
|
212
|
+
certsTableHead.innerHTML = "";
|
|
213
|
+
certsTableBody.innerHTML = "";
|
|
190
214
|
}
|
|
191
215
|
|
|
192
216
|
// ------------------------------------------------------------
|
|
@@ -210,19 +234,56 @@
|
|
|
210
234
|
return Number.isFinite(n) ? n : null;
|
|
211
235
|
}
|
|
212
236
|
|
|
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
237
|
function round2(x) {
|
|
222
238
|
const n = Number(x);
|
|
223
239
|
return Number.isFinite(n) ? n.toFixed(2) : "—";
|
|
224
240
|
}
|
|
225
241
|
|
|
242
|
+
function formatBytes(x) {
|
|
243
|
+
if (x == null) return "—";
|
|
244
|
+
const units = ["B","KB","MB","GB","TB"];
|
|
245
|
+
let i = 0;
|
|
246
|
+
let n = Number(x);
|
|
247
|
+
while (n > 1024 && i < units.length-1) {
|
|
248
|
+
n /= 1024;
|
|
249
|
+
i++;
|
|
250
|
+
}
|
|
251
|
+
return n.toFixed(2) + " " + units[i];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function tableConstructor(dataList, tableHead, tableBody) {
|
|
255
|
+
tableHead.innerHTML = "";
|
|
256
|
+
tableBody.innerHTML = "";
|
|
257
|
+
|
|
258
|
+
if (!Array.isArray(dataList) || dataList.length === 0) {
|
|
259
|
+
tableBody.innerHTML = `<tr><td colspan="10">NO DATA</td></tr>`;
|
|
260
|
+
} else {
|
|
261
|
+
const columns = Object.keys(dataList[0]);
|
|
262
|
+
tableHead.innerHTML =
|
|
263
|
+
"<tr>" + columns.map(c => `<th>${c}</th>`).join("") + "</tr>";
|
|
264
|
+
dataList.forEach(c => {
|
|
265
|
+
const row = "<tr>" +
|
|
266
|
+
columns.map(col => `<td>${c[col] ?? ""}</td>`).join("") +
|
|
267
|
+
"</tr>";
|
|
268
|
+
tableBody.insertAdjacentHTML("beforeend", row);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function objectToString(...vals) {
|
|
274
|
+
for (const v of vals) {
|
|
275
|
+
if (v !== undefined && v !== null) {
|
|
276
|
+
if (typeof v === "object") {
|
|
277
|
+
return Object.entries(v)
|
|
278
|
+
.map(([k, val]) => `${k}: ${val}`)
|
|
279
|
+
.join("<br>");
|
|
280
|
+
}
|
|
281
|
+
return String(v);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return "—";
|
|
285
|
+
}
|
|
286
|
+
|
|
226
287
|
// ------------------------------------------------------------
|
|
227
288
|
// METRICS HANDLER
|
|
228
289
|
// ------------------------------------------------------------
|
|
@@ -233,41 +294,95 @@
|
|
|
233
294
|
if (host.base_url !== selectedBase) continue;
|
|
234
295
|
const m = host.metrics || {};
|
|
235
296
|
|
|
236
|
-
// -------------------
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
297
|
+
// ------------------- System -------------------
|
|
298
|
+
systemEl.textContent =
|
|
299
|
+
`Node: ${m.node || "-"}\n` +
|
|
300
|
+
`OS: ${m.system || "-"}\n` +
|
|
301
|
+
`Architecture: ${m.architecture || "-"}\n\n` +
|
|
302
|
+
`CPU Cores: ${m.cores || "-"}\n` +
|
|
303
|
+
`Up Time: ${m.uptime || "-"}\n`;
|
|
304
|
+
|
|
305
|
+
// ------------------- IP -------------------
|
|
306
|
+
if (m.ip_info) {
|
|
307
|
+
ipEl.textContent =
|
|
308
|
+
`Private: ${m.ip_info.private || "-"}\n\n` +
|
|
309
|
+
`Public: ${m.ip_info.public || "-"}`;
|
|
310
|
+
} else {
|
|
311
|
+
ipEl.textContent = "-";
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ------------------- CPU / GPU -------------------
|
|
315
|
+
processorEl.textContent =
|
|
316
|
+
`CPU: ${m.cpu_name || "-"}\n\n` +
|
|
317
|
+
`GPU: ${m.gpu_name || "-"}`;
|
|
318
|
+
|
|
319
|
+
// ------------------- DISKS (OLD “disk” card) -------------------
|
|
320
|
+
if (Array.isArray(m.disk_info) && m.disk_info.length > 0) {
|
|
321
|
+
const d = m.disk_info[0];
|
|
322
|
+
diskEl.textContent =
|
|
323
|
+
`Total: ${formatBytes(d.total)}\n` +
|
|
324
|
+
`Used: ${formatBytes(d.used)}\n` +
|
|
325
|
+
`Free: ${formatBytes(d.free)}`;
|
|
326
|
+
} else if (m.disk) {
|
|
327
|
+
diskEl.textContent =
|
|
328
|
+
`Total: ${m.disk.total}\nUsed: ${m.disk.used}\nFree: ${m.disk.free}`;
|
|
244
329
|
} else {
|
|
245
330
|
diskEl.textContent = "NO DATA";
|
|
246
331
|
}
|
|
247
332
|
|
|
248
333
|
// ------------------- MEMORY -------------------
|
|
249
|
-
if (m.
|
|
250
|
-
const
|
|
251
|
-
const
|
|
334
|
+
if (m.memory_info) {
|
|
335
|
+
const total = formatBytes(m.memory_info.total);
|
|
336
|
+
const used = formatBytes(m.memory_info.used);
|
|
337
|
+
const percent = round2(m.memory_info.percent);
|
|
338
|
+
|
|
339
|
+
memEl.textContent = `Total: ${total}\nUsed: ${used}\nPercent: ${percent}%`;
|
|
340
|
+
pushPoint(memChart, num(m.memory_info.percent));
|
|
341
|
+
|
|
342
|
+
} else if (m.memory) {
|
|
343
|
+
// fallback to old
|
|
344
|
+
const used = m.memory.ram_used || "";
|
|
345
|
+
const percent = m.memory.ram_usage ?? m.memory.percent ?? "—";
|
|
252
346
|
const totalMem = m.memory.ram_total ?? m.memory.total ?? "—";
|
|
253
347
|
memEl.textContent = `Total: ${totalMem}\nUsed: ${used}\nPercent: ${percent}%`;
|
|
254
348
|
pushPoint(memChart, num(percent));
|
|
255
349
|
} else {
|
|
256
|
-
memEl.textContent = "NO DATA"
|
|
350
|
+
memEl.textContent = "NO DATA";
|
|
257
351
|
}
|
|
258
352
|
|
|
259
|
-
// ------------------- CPU -------------------
|
|
353
|
+
// ------------------- CPU (NEW — cpu_usage[]) -------------------
|
|
260
354
|
let avg = null;
|
|
261
355
|
|
|
262
|
-
if (m.
|
|
263
|
-
const
|
|
356
|
+
if (Array.isArray(m.cpu_usage)) {
|
|
357
|
+
const values = m.cpu_usage.map(num);
|
|
358
|
+
avg = values.reduce((a, b) => a + (b ?? 0), 0) / values.length;
|
|
359
|
+
|
|
360
|
+
pruneOldCores(values.map((_, i) => "cpu" + (i + 1)));
|
|
264
361
|
|
|
362
|
+
values.forEach((v, i) => {
|
|
363
|
+
const coreName = "cpu" + (i + 1);
|
|
364
|
+
const c = getCoreChart(coreName);
|
|
365
|
+
|
|
366
|
+
c.chart.data.labels.push(now);
|
|
367
|
+
c.chart.data.datasets[0].data.push(v ?? 0);
|
|
368
|
+
|
|
369
|
+
if (c.chart.data.labels.length > MAX_POINTS) {
|
|
370
|
+
c.chart.data.labels.shift();
|
|
371
|
+
c.chart.data.datasets[0].data.shift();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
c.chart.update("none");
|
|
375
|
+
c.valEl.textContent = `${(v ?? 0).toFixed(1)}%`;
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
} else if (m.cpu) {
|
|
379
|
+
// fallback to old
|
|
380
|
+
const detail = m.cpu.detail || m.cpu;
|
|
265
381
|
if (typeof detail === "object") {
|
|
266
382
|
const names = Object.keys(detail);
|
|
267
383
|
pruneOldCores(names);
|
|
268
384
|
|
|
269
385
|
const values = [];
|
|
270
|
-
|
|
271
386
|
for (const [core, val] of Object.entries(detail)) {
|
|
272
387
|
const v = num(val);
|
|
273
388
|
values.push(v);
|
|
@@ -284,103 +399,94 @@
|
|
|
284
399
|
c.chart.update("none");
|
|
285
400
|
c.valEl.textContent = `${(v ?? 0).toFixed(1)}%`;
|
|
286
401
|
}
|
|
287
|
-
|
|
288
402
|
avg = values.reduce((a, b) => a + (b ?? 0), 0) / values.length;
|
|
289
403
|
}
|
|
290
|
-
else if (typeof detail === "number") {
|
|
291
|
-
avg = detail;
|
|
292
|
-
}
|
|
293
404
|
}
|
|
294
405
|
|
|
295
406
|
if (avg != null) pushPoint(cpuAvgChart, avg);
|
|
296
407
|
|
|
297
408
|
// ------------------- CPU LOAD -------------------
|
|
298
|
-
if (m.
|
|
409
|
+
if (m.load_averages) {
|
|
410
|
+
const la = m.load_averages;
|
|
411
|
+
loadEl.textContent =
|
|
412
|
+
`${round2(la.m1)} / ${round2(la.m5)} / ${round2(la.m15)}`;
|
|
413
|
+
pushPoint(loadChart, num(la.m1));
|
|
414
|
+
} else if (m.cpu_load) {
|
|
299
415
|
const load = m.cpu_load.detail || m.cpu_load;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
loadEl.textContent = `${round2(m1)} / ${round2(m5)} / ${round2(m15)}`;
|
|
306
|
-
pushPoint(loadChart, num(m1) ?? 0);
|
|
307
|
-
} else {
|
|
308
|
-
loadEl.textContent = load;
|
|
309
|
-
pushPoint(loadChart, num(load));
|
|
310
|
-
}
|
|
416
|
+
const m1 = load.m1 ?? load[0];
|
|
417
|
+
const m5 = load.m5 ?? load[1];
|
|
418
|
+
const m15 = load.m15 ?? load[2];
|
|
419
|
+
loadEl.textContent = `${round2(m1)} / ${round2(m5)} / ${round2(m15)}`;
|
|
420
|
+
pushPoint(loadChart, num(m1));
|
|
311
421
|
} else {
|
|
312
|
-
loadEl.textContent = "NO DATA"
|
|
422
|
+
loadEl.textContent = "NO DATA";
|
|
313
423
|
}
|
|
314
424
|
|
|
315
|
-
// ------------------- SERVICES -------------------
|
|
316
|
-
|
|
425
|
+
// ------------------- SERVICES (NEW → OLD) -------------------
|
|
426
|
+
const services = m.service_stats || m.services || [];
|
|
427
|
+
servicesTableBody.innerHTML = "";
|
|
428
|
+
if (Array.isArray(services)) {
|
|
317
429
|
const filter = svcFilter.value.trim().toLowerCase();
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
for (const s of m.services) {
|
|
321
|
-
const name = s.pname || s.label || s.name || "";
|
|
430
|
+
for (const s of services) {
|
|
431
|
+
const name = s.pname || s.Name || "";
|
|
322
432
|
|
|
323
433
|
if (filter && !name.toLowerCase().includes(filter)) continue;
|
|
324
434
|
|
|
325
435
|
const tr = document.createElement("tr");
|
|
326
436
|
tr.innerHTML = `
|
|
327
|
-
<td>${s.
|
|
437
|
+
<td>${s.PID ?? ""}</td>
|
|
328
438
|
<td>${name}</td>
|
|
329
|
-
<td>${s.status ?? "—"}</td>
|
|
330
|
-
<td>${s.
|
|
331
|
-
<td>${
|
|
439
|
+
<td>${s.Status ?? s.status ?? "—"}</td>
|
|
440
|
+
<td>${objectToString(s.CPU, s.cpu)}</td>
|
|
441
|
+
<td>${objectToString(s.Memory, s.memory)}</td>
|
|
442
|
+
<td>${s.Threads ?? s.threads ?? "—"}</td>
|
|
443
|
+
<td>${s["Open Files"] ?? s.open_files ?? "—"}</td>
|
|
332
444
|
`;
|
|
333
445
|
servicesTableBody.appendChild(tr);
|
|
334
446
|
}
|
|
335
|
-
} else {
|
|
336
|
-
servicesTableBody.innerHTML = `<tr><td colspan="5">NO DATA</td></tr>`;
|
|
337
447
|
}
|
|
338
448
|
|
|
339
|
-
// -------------------
|
|
340
|
-
const
|
|
449
|
+
// ------------------- PROCESSES (NEW → OLD) -------------------
|
|
450
|
+
const processes = m.process_stats || [];
|
|
451
|
+
processesTableBody.innerHTML = "";
|
|
452
|
+
if (Array.isArray(processes)) {
|
|
453
|
+
const filter = procFilter.value.trim().toLowerCase();
|
|
454
|
+
for (const p of processes) {
|
|
455
|
+
const name = p.Name || "";
|
|
341
456
|
|
|
342
|
-
|
|
343
|
-
dockerTableBody.innerHTML = "";
|
|
344
|
-
|
|
345
|
-
if (!Array.isArray(dockerList) || dockerList.length === 0) {
|
|
346
|
-
dockerTableBody.innerHTML = `<tr><td colspan="10">NO DATA</td></tr>`;
|
|
347
|
-
} else {
|
|
348
|
-
// Create header
|
|
349
|
-
const columns = Object.keys(dockerList[0]);
|
|
350
|
-
dockerTableHead.innerHTML =
|
|
351
|
-
"<tr>" + columns.map(c => `<th>${c}</th>`).join("") + "</tr>";
|
|
352
|
-
|
|
353
|
-
// Create rows
|
|
354
|
-
dockerList.forEach(c => {
|
|
355
|
-
const row = "<tr>" +
|
|
356
|
-
columns.map(col => `<td>${c[col] ?? ""}</td>`).join("") +
|
|
357
|
-
"</tr>";
|
|
358
|
-
dockerTableBody.insertAdjacentHTML("beforeend", row);
|
|
359
|
-
});
|
|
360
|
-
}
|
|
457
|
+
if (filter && !name.toLowerCase().includes(filter)) continue;
|
|
361
458
|
|
|
362
|
-
// ------------------- DISKS -------------------
|
|
363
|
-
if (Array.isArray(m.disks)) {
|
|
364
|
-
disksTableBody.innerHTML = "";
|
|
365
|
-
for (const d of m.disks) {
|
|
366
459
|
const tr = document.createElement("tr");
|
|
367
460
|
tr.innerHTML = `
|
|
368
|
-
<td>${
|
|
369
|
-
<td>${
|
|
370
|
-
<td>${
|
|
461
|
+
<td>${p.PID ?? ""}</td>
|
|
462
|
+
<td>${name}</td>
|
|
463
|
+
<td>${p.Status ?? p.status ?? "—"}</td>
|
|
464
|
+
<td>${p.CPU ?? p.cpu ?? "—"}</td>
|
|
465
|
+
<td>${p.Memory ?? p.memory ?? "—"}</td>
|
|
466
|
+
<td>${p.Uptime ?? p.uptime ?? "—"}</td>
|
|
467
|
+
<td>${p.Threads ?? p.threads ?? "—"}</td>
|
|
468
|
+
<td>${p["Open Files"] ?? p.open_files ?? "—"}</td>
|
|
371
469
|
`;
|
|
372
|
-
|
|
470
|
+
processesTableBody.appendChild(tr);
|
|
373
471
|
}
|
|
374
|
-
} else {
|
|
375
|
-
disksTableBody.innerHTML = `<tr><td colspan="3">NO DATA</td></tr>`;
|
|
376
472
|
}
|
|
377
473
|
|
|
474
|
+
// ------------------- DOCKER -------------------
|
|
475
|
+
const dockerList = m.docker_stats || [];
|
|
476
|
+
tableConstructor(dockerList, dockerTableHead, dockerTableBody);
|
|
477
|
+
|
|
478
|
+
// ------------------- DISKS (Tables) -------------------
|
|
479
|
+
// TOOD: Remove disk list when pyudisk is available
|
|
480
|
+
const diskList = m.disks_info || [];
|
|
481
|
+
tableConstructor(diskList, disksTableHead, disksTableBody);
|
|
482
|
+
|
|
483
|
+
// ------------------- PyUdisk (Tables) -------------------
|
|
484
|
+
const pyudiskList = m.pyudisk_stats || [];
|
|
485
|
+
tableConstructor(pyudiskList, pyudiskTableHead, pyudiskTableBody);
|
|
486
|
+
|
|
378
487
|
// ------------------- CERTIFICATES -------------------
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
} else {
|
|
382
|
-
certsEl.textContent = "NO DATA"
|
|
383
|
-
}
|
|
488
|
+
const certsList = m.certificates || [];
|
|
489
|
+
tableConstructor(certsList, certsTableHead, certsTableBody);
|
|
384
490
|
}
|
|
385
491
|
}
|
|
386
492
|
|
|
@@ -21,7 +21,8 @@ html,body { height:100%; margin:0; font-family: Inter, system-ui, -apple-system,
|
|
|
21
21
|
|
|
22
22
|
.container { padding:18px; display:flex; flex-direction:column; gap:16px; max-width:1200px; margin:0 auto; }
|
|
23
23
|
|
|
24
|
-
.meta-row { display:
|
|
24
|
+
.meta-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
|
25
|
+
/* .meta-row { display:flex; gap:12px; } */
|
|
25
26
|
.meta-card { background:var(--panel); padding:12px; border-radius:8px; width:1fr; min-width:120px; flex:1; box-shadow:0 4px 12px rgba(2,6,23,0.6); }
|
|
26
27
|
.meta-title { font-size:12px; color:var(--muted); }
|
|
27
28
|
.meta-value { margin-top:6px; font-weight:600; font-size:16px; }
|
|
@@ -52,9 +53,11 @@ html,body { height:100%; margin:0; font-family: Inter, system-ui, -apple-system,
|
|
|
52
53
|
.footer { padding:12px 20px; font-size:12px; color:var(--muted); text-align:center; border-top:1px solid rgba(255,255,255,0.02); margin-top:20px; }
|
|
53
54
|
.service-controls { display:flex; gap:8px; padding-bottom:8px; }
|
|
54
55
|
input#svc-filter { flex:1; padding:6px 8px; background:var(--panel); border:1px solid rgba(255,255,255,0.04); color:inherit; border-radius:6px; }
|
|
56
|
+
input#proc-filter { flex:1; padding:6px 8px; background:var(--panel); border:1px solid rgba(255,255,255,0.04); color:inherit; border-radius:6px; }
|
|
55
57
|
|
|
56
58
|
@media (max-width:900px) {
|
|
57
59
|
.tables-row { grid-template-columns: 1fr; }
|
|
58
60
|
.charts-row { flex-direction:column; }
|
|
59
61
|
.details-row { flex-direction:column; }
|
|
62
|
+
.meta-row { grid-template-columns: 1fr; }
|
|
60
63
|
}
|
|
@@ -21,19 +21,24 @@
|
|
|
21
21
|
|
|
22
22
|
<main class="container">
|
|
23
23
|
<section class="meta-row">
|
|
24
|
+
<div class="meta-card">
|
|
25
|
+
<div class="meta-title">System</div>
|
|
26
|
+
<div id="system" class="meta-value pre">—</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
24
29
|
<div class="meta-card">
|
|
25
30
|
<div class="meta-title">IP</div>
|
|
26
|
-
<div id="ip" class="meta-value">—</div>
|
|
31
|
+
<div id="ip-info" class="meta-value pre">—</div>
|
|
27
32
|
</div>
|
|
28
33
|
|
|
29
34
|
<div class="meta-card">
|
|
30
35
|
<div class="meta-title">GPU / CPU Model</div>
|
|
31
|
-
<div id="
|
|
36
|
+
<div id="processor" class="meta-value pre">—</div>
|
|
32
37
|
</div>
|
|
33
38
|
|
|
34
39
|
<div class="meta-card">
|
|
35
40
|
<div class="meta-title">CPU Load (1/5/15)</div>
|
|
36
|
-
<div id="cpuload" class="meta-value">—</div>
|
|
41
|
+
<div id="cpuload" class="meta-value pre">—</div>
|
|
37
42
|
</div>
|
|
38
43
|
|
|
39
44
|
<div class="meta-card">
|
|
@@ -83,7 +88,19 @@
|
|
|
83
88
|
<input id="svc-filter" placeholder="filter service name..." />
|
|
84
89
|
</div>
|
|
85
90
|
<table class="table" id="services-table">
|
|
86
|
-
<thead><tr><th>PID</th><th>Name</th><th>Status</th><th>CPU</th><th>Memory</th></tr></thead>
|
|
91
|
+
<thead><tr><th>PID</th><th>Name</th><th>Status</th><th>CPU</th><th>Memory</th><th>Threads</th><th>Open Files</th></tr></thead>
|
|
92
|
+
<tbody></tbody>
|
|
93
|
+
</table>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<section class="tables-row">
|
|
97
|
+
<div class="panel">
|
|
98
|
+
<div class="panel-header"><h3>Processes</h3></div>
|
|
99
|
+
<div class="panel-body service-controls">
|
|
100
|
+
<input id="proc-filter" placeholder="filter process name..." />
|
|
101
|
+
</div>
|
|
102
|
+
<table class="table" id="processes-table">
|
|
103
|
+
<thead><tr><th>PID</th><th>Name</th><th>Status</th><th>CPU</th><th>Memory</th><th>Uptime</th><th>Threads</th><th>Open Files</th></tr></thead>
|
|
87
104
|
<tbody></tbody>
|
|
88
105
|
</table>
|
|
89
106
|
</div>
|
|
@@ -102,7 +119,17 @@
|
|
|
102
119
|
<div class="panel-header"><h3>Disks</h3></div>
|
|
103
120
|
<div class="panel-body">
|
|
104
121
|
<table class="table" id="disks-table">
|
|
105
|
-
<thead
|
|
122
|
+
<thead></thead>
|
|
123
|
+
<tbody></tbody>
|
|
124
|
+
</table>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div class="panel">
|
|
129
|
+
<div class="panel-header"><h3>PyUdisk</h3></div>
|
|
130
|
+
<div class="panel-body">
|
|
131
|
+
<table class="table" id="pyudisk-table">
|
|
132
|
+
<thead></thead>
|
|
106
133
|
<tbody></tbody>
|
|
107
134
|
</table>
|
|
108
135
|
</div>
|
|
@@ -111,12 +138,16 @@
|
|
|
111
138
|
<div class="panel">
|
|
112
139
|
<div class="panel-header"><h3>Certificates</h3></div>
|
|
113
140
|
<div class="panel-body">
|
|
114
|
-
<
|
|
141
|
+
<table class="table" id="certificates-table">
|
|
142
|
+
<thead></thead>
|
|
143
|
+
<tbody></tbody>
|
|
144
|
+
</table>
|
|
115
145
|
</div>
|
|
116
146
|
</div>
|
|
117
147
|
</section>
|
|
118
148
|
</main>
|
|
119
149
|
|
|
150
|
+
<!-- TODO: Remove or provide context -->
|
|
120
151
|
<footer class="footer">
|
|
121
152
|
<div>OpenAPI: <code>/static/openapi.json</code> (uploaded file available at <code>/mnt/data/openapi.json</code>)</div>
|
|
122
153
|
</footer>
|
pyobservability/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.0
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
pyobservability/__init__.py,sha256=rr4udGMbbNPl3yo7l8R3FUUVVahBtYVaW6vSWWgXlv0,2617
|
|
2
|
+
pyobservability/main.py,sha256=HpKYxQ3XgyvxRP6giVOBIpBYTptZ-txL98KV5D5P8uI,3623
|
|
3
|
+
pyobservability/monitor.py,sha256=wu-CYsMCMQ1wZLTBaIyYLdu2FJvQ4rb41Lv2NEJ3yKM,4759
|
|
4
|
+
pyobservability/version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
5
|
+
pyobservability/config/enums.py,sha256=iMIOpa8LYSszkPIYBhupX--KrEXVTTsBurinpAxLvMA,86
|
|
6
|
+
pyobservability/config/settings.py,sha256=HoDRuzwCCbCdNQLdOiy9JUoiM2BqUsRkX6zUyDLordY,3811
|
|
7
|
+
pyobservability/static/app.js,sha256=QQJ-1m7zwKQZz7nWTACS97mF79WGCXl5pNNQDWLmByM,17663
|
|
8
|
+
pyobservability/static/styles.css,sha256=ljK6m-q7GMZkhiYQ9bqqR1NbYu2pRXzlvNTol_PK9go,3521
|
|
9
|
+
pyobservability/templates/index.html,sha256=di7LKLVf3Ri0nxUn6dXVXlVPomQSRzZgWvNfTfxQb8s,5035
|
|
10
|
+
pyobservability-0.1.0.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
|
|
11
|
+
pyobservability-0.1.0.dist-info/METADATA,sha256=nDYtRE5m3gCHwO8LI7F0w1B1qy7sLLjHTg7hQfD8RLA,6822
|
|
12
|
+
pyobservability-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
13
|
+
pyobservability-0.1.0.dist-info/entry_points.txt,sha256=DSGIr_VA8Tb3FYa2iNUYpf55eAvuFCAoInNS4ngXaME,57
|
|
14
|
+
pyobservability-0.1.0.dist-info/top_level.txt,sha256=p20T0EmihDYW1uMintRXr7X9bg3XWYKyoSbBHOVC1xI,16
|
|
15
|
+
pyobservability-0.1.0.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
pyobservability/__init__.py,sha256=rr4udGMbbNPl3yo7l8R3FUUVVahBtYVaW6vSWWgXlv0,2617
|
|
2
|
-
pyobservability/main.py,sha256=QCoALD_vk3S5XKEIwMI_LzfNixzq7xpIS0uA21uMqs0,3414
|
|
3
|
-
pyobservability/monitor.py,sha256=bDbkSOHgtS_9Jv7IqZ9eN1P_-cFeZFlnozRo2N6hh_s,6578
|
|
4
|
-
pyobservability/version.py,sha256=4GZKi13lDTD25YBkGakhZyEQZWTER_OWQMNPoH_UM2c,22
|
|
5
|
-
pyobservability/config/enums.py,sha256=iMIOpa8LYSszkPIYBhupX--KrEXVTTsBurinpAxLvMA,86
|
|
6
|
-
pyobservability/config/settings.py,sha256=HoDRuzwCCbCdNQLdOiy9JUoiM2BqUsRkX6zUyDLordY,3811
|
|
7
|
-
pyobservability/static/app.js,sha256=nXUWAoRDUTKAtRfLmg6GveBQlRtNk-UoVp09AXvSlrA,13743
|
|
8
|
-
pyobservability/static/styles.css,sha256=t6r1C0ueBanipgRRjdu18nmq6RbSGLK5bhpf0BdMOpQ,3245
|
|
9
|
-
pyobservability/templates/index.html,sha256=PsN3aq-7Q6RzGeshoNH5v37G3sHoI2saJq6mfuA6JYs,3977
|
|
10
|
-
pyobservability-0.0.3.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
|
|
11
|
-
pyobservability-0.0.3.dist-info/METADATA,sha256=mdFfoTsAoeh2HlXq6ndwjzwVVM17xT8IwQh1NQEr1QQ,6822
|
|
12
|
-
pyobservability-0.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
13
|
-
pyobservability-0.0.3.dist-info/entry_points.txt,sha256=DSGIr_VA8Tb3FYa2iNUYpf55eAvuFCAoInNS4ngXaME,57
|
|
14
|
-
pyobservability-0.0.3.dist-info/top_level.txt,sha256=p20T0EmihDYW1uMintRXr7X9bg3XWYKyoSbBHOVC1xI,16
|
|
15
|
-
pyobservability-0.0.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|