PyObservability 0.0.2__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 +68 -145
- pyobservability/static/app.js +205 -91
- pyobservability/static/styles.css +4 -1
- pyobservability/templates/index.html +37 -6
- pyobservability/version.py +1 -1
- {pyobservability-0.0.2.dist-info → pyobservability-0.1.0.dist-info}/METADATA +1 -1
- pyobservability-0.1.0.dist-info/RECORD +15 -0
- pyobservability-0.0.2.dist-info/RECORD +0 -15
- {pyobservability-0.0.2.dist-info → pyobservability-0.1.0.dist-info}/WHEEL +0 -0
- {pyobservability-0.0.2.dist-info → pyobservability-0.1.0.dist-info}/entry_points.txt +0 -0
- {pyobservability-0.0.2.dist-info → pyobservability-0.1.0.dist-info}/licenses/LICENSE +0 -0
- {pyobservability-0.0.2.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 "NO DATA"
|
|
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)
|
|
128
|
-
return
|
|
91
|
+
LOGGER.debug("Exception: %s", err)
|
|
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] = "NO DATA"
|
|
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,39 +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
|
-
diskEl.textContent = "
|
|
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));
|
|
349
|
+
} else {
|
|
350
|
+
memEl.textContent = "NO DATA";
|
|
255
351
|
}
|
|
256
352
|
|
|
257
|
-
// ------------------- CPU -------------------
|
|
353
|
+
// ------------------- CPU (NEW — cpu_usage[]) -------------------
|
|
258
354
|
let avg = null;
|
|
259
355
|
|
|
260
|
-
if (m.
|
|
261
|
-
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)));
|
|
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
|
+
});
|
|
262
377
|
|
|
378
|
+
} else if (m.cpu) {
|
|
379
|
+
// fallback to old
|
|
380
|
+
const detail = m.cpu.detail || m.cpu;
|
|
263
381
|
if (typeof detail === "object") {
|
|
264
382
|
const names = Object.keys(detail);
|
|
265
383
|
pruneOldCores(names);
|
|
266
384
|
|
|
267
385
|
const values = [];
|
|
268
|
-
|
|
269
386
|
for (const [core, val] of Object.entries(detail)) {
|
|
270
387
|
const v = num(val);
|
|
271
388
|
values.push(v);
|
|
@@ -282,97 +399,94 @@
|
|
|
282
399
|
c.chart.update("none");
|
|
283
400
|
c.valEl.textContent = `${(v ?? 0).toFixed(1)}%`;
|
|
284
401
|
}
|
|
285
|
-
|
|
286
402
|
avg = values.reduce((a, b) => a + (b ?? 0), 0) / values.length;
|
|
287
403
|
}
|
|
288
|
-
else if (typeof detail === "number") {
|
|
289
|
-
avg = detail;
|
|
290
|
-
}
|
|
291
404
|
}
|
|
292
405
|
|
|
293
406
|
if (avg != null) pushPoint(cpuAvgChart, avg);
|
|
294
407
|
|
|
295
408
|
// ------------------- CPU LOAD -------------------
|
|
296
|
-
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) {
|
|
297
415
|
const load = m.cpu_load.detail || m.cpu_load;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
} else {
|
|
306
|
-
loadEl.textContent = load;
|
|
307
|
-
pushPoint(loadChart, num(load));
|
|
308
|
-
}
|
|
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));
|
|
421
|
+
} else {
|
|
422
|
+
loadEl.textContent = "NO DATA";
|
|
309
423
|
}
|
|
310
424
|
|
|
311
|
-
// ------------------- SERVICES -------------------
|
|
312
|
-
|
|
425
|
+
// ------------------- SERVICES (NEW → OLD) -------------------
|
|
426
|
+
const services = m.service_stats || m.services || [];
|
|
427
|
+
servicesTableBody.innerHTML = "";
|
|
428
|
+
if (Array.isArray(services)) {
|
|
313
429
|
const filter = svcFilter.value.trim().toLowerCase();
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
for (const s of m.services) {
|
|
317
|
-
const name = s.pname || s.label || s.name || "";
|
|
430
|
+
for (const s of services) {
|
|
431
|
+
const name = s.pname || s.Name || "";
|
|
318
432
|
|
|
319
433
|
if (filter && !name.toLowerCase().includes(filter)) continue;
|
|
320
434
|
|
|
321
435
|
const tr = document.createElement("tr");
|
|
322
436
|
tr.innerHTML = `
|
|
323
|
-
<td>${s.
|
|
437
|
+
<td>${s.PID ?? ""}</td>
|
|
324
438
|
<td>${name}</td>
|
|
325
|
-
<td>${s.status ?? "—"}</td>
|
|
326
|
-
<td>${s.
|
|
327
|
-
<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>
|
|
328
444
|
`;
|
|
329
445
|
servicesTableBody.appendChild(tr);
|
|
330
446
|
}
|
|
331
447
|
}
|
|
332
448
|
|
|
333
|
-
// -------------------
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
if (Array.isArray(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
});
|
|
353
|
-
}
|
|
354
|
-
} else {
|
|
355
|
-
console.warn("Cannot render Docker table:", dockerList, dockerTableHead, dockerTableBody);
|
|
356
|
-
}
|
|
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 || "";
|
|
456
|
+
|
|
457
|
+
if (filter && !name.toLowerCase().includes(filter)) continue;
|
|
357
458
|
|
|
358
|
-
// ------------------- DISKS -------------------
|
|
359
|
-
if (Array.isArray(m.disks)) {
|
|
360
|
-
disksTableBody.innerHTML = "";
|
|
361
|
-
for (const d of m.disks) {
|
|
362
459
|
const tr = document.createElement("tr");
|
|
363
460
|
tr.innerHTML = `
|
|
364
|
-
<td>${
|
|
365
|
-
<td>${
|
|
366
|
-
<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>
|
|
367
469
|
`;
|
|
368
|
-
|
|
470
|
+
processesTableBody.appendChild(tr);
|
|
369
471
|
}
|
|
370
472
|
}
|
|
371
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
|
+
|
|
372
487
|
// ------------------- CERTIFICATES -------------------
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
488
|
+
const certsList = m.certificates || [];
|
|
489
|
+
tableConstructor(certsList, certsTableHead, certsTableBody);
|
|
376
490
|
}
|
|
377
491
|
}
|
|
378
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=s2sVp97sLjkkdtL6be82bX5ydu_gBdMSoWxDlmUtpgE,6613
|
|
4
|
-
pyobservability/version.py,sha256=QvlVh4JTl3JL7jQAja76yKtT-IvF4631ASjWY1wS6AQ,22
|
|
5
|
-
pyobservability/config/enums.py,sha256=iMIOpa8LYSszkPIYBhupX--KrEXVTTsBurinpAxLvMA,86
|
|
6
|
-
pyobservability/config/settings.py,sha256=HoDRuzwCCbCdNQLdOiy9JUoiM2BqUsRkX6zUyDLordY,3811
|
|
7
|
-
pyobservability/static/app.js,sha256=poc7eReoiRUbyI5JKnPwxSqmSNCuOge4aZKCITFy7eo,13494
|
|
8
|
-
pyobservability/static/styles.css,sha256=t6r1C0ueBanipgRRjdu18nmq6RbSGLK5bhpf0BdMOpQ,3245
|
|
9
|
-
pyobservability/templates/index.html,sha256=PsN3aq-7Q6RzGeshoNH5v37G3sHoI2saJq6mfuA6JYs,3977
|
|
10
|
-
pyobservability-0.0.2.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
|
|
11
|
-
pyobservability-0.0.2.dist-info/METADATA,sha256=EDg-mzHBxxLTq93gbc1WhIKE3d0B_d_nXhSgiuO6sJI,6822
|
|
12
|
-
pyobservability-0.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
13
|
-
pyobservability-0.0.2.dist-info/entry_points.txt,sha256=DSGIr_VA8Tb3FYa2iNUYpf55eAvuFCAoInNS4ngXaME,57
|
|
14
|
-
pyobservability-0.0.2.dist-info/top_level.txt,sha256=p20T0EmihDYW1uMintRXr7X9bg3XWYKyoSbBHOVC1xI,16
|
|
15
|
-
pyobservability-0.0.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|