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 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,
@@ -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 Any, Dict, List
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
- self.sessions[target["base_url"]] = aiohttp.ClientSession()
82
- self._task = asyncio.create_task(self._run_loop())
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
- if self._task:
87
- self._task.cancel()
41
+
42
+ for task in self._tasks:
43
+ task.cancel()
88
44
  try:
89
- await self._task
45
+ await task
90
46
  except CancelledError:
91
47
  pass
92
48
 
93
- for sess in self.sessions.values():
94
- await sess.close()
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 WRAPPER
67
+ # FETCH OBSERVABILITY FOR SINGLE TARGET
112
68
  ############################################################################
113
- async def _fetch(self, session, base_url, ep, headers: Dict[str, str], params=None):
114
- url = base_url.rstrip("/") + ep
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, params=params, timeout=10) as resp:
117
- if resp.status == 200:
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
- return await resp.json()
120
- except Exception as err:
121
- LOGGER.debug(err)
122
- return "NO DATA"
123
- parsed = urlparse(url)
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 "NO DATA"
91
+ LOGGER.debug("Exception: %s", err)
92
+ return
129
93
 
130
94
  ############################################################################
131
- # PER-TARGET POLLING
95
+ # STREAM A SPECIFIC TARGET - SEQUENTIAL OVER ALL TARGETS
132
96
  ############################################################################
133
- async def _poll_target(self, target: Dict[str, Any]) -> Dict[str, Any]:
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
- metrics = await self._poll_all()
185
-
186
- payload = {"type": "metrics", "ts": asyncio.get_event_loop().time(), "data": metrics}
187
-
188
- for q in list(self._ws_subscribers):
189
- try:
190
- q.put_nowait(payload)
191
- except asyncio.QueueFull:
192
- try:
193
- _ = q.get_nowait()
194
- q.put_nowait(payload)
195
- except Exception:
196
- pass
197
-
198
- await asyncio.sleep(self.poll_interval)
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)
@@ -12,8 +12,10 @@
12
12
  const nodeSelect = document.getElementById("node-select");
13
13
  const refreshBtn = document.getElementById("refresh-btn");
14
14
 
15
- const ipEl = document.getElementById("ip");
16
- const gpuEl = document.getElementById("gpu");
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 disksTableBody = document.querySelector("#disks-table tbody");
35
- const certsEl = document.getElementById("certificates");
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
- gpuEl.textContent = "—";
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
- certsEl.textContent = "—";
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
- // ------------------- BASIC INFO -------------------
237
- ipEl.textContent = m.ip?.ip || m.ip || "—";
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}`;
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.memory) {
250
- const used = m.memory.ram_used || m.memory.used || "";
251
- const percent = m.memory.ram_usage ?? m.memory.usage ?? m.memory.percent ?? "—";
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.cpu) {
261
- const detail = m.cpu.detail || m.cpu;
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.cpu_load) {
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
- if (typeof load === "object") {
299
- const m1 = load.m1 ?? load[0];
300
- const m5 = load.m5 ?? load[1];
301
- const m15 = load.m15 ?? load[2];
302
-
303
- loadEl.textContent = `${round2(m1)} / ${round2(m5)} / ${round2(m15)}`;
304
- pushPoint(loadChart, num(m1) ?? 0);
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
- if (Array.isArray(m.services)) {
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
- servicesTableBody.innerHTML = "";
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.pid ?? s.PID ?? ""}</td>
437
+ <td>${s.PID ?? ""}</td>
324
438
  <td>${name}</td>
325
- <td>${s.status ?? "—"}</td>
326
- <td>${s.cpu ? JSON.stringify(s.cpu) : "—"}</td>
327
- <td>${s.memory ? (s.memory.rss || s.memory.pfaults || JSON.stringify(s.memory)) : "—"}</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
- // ------------------- DOCKER -------------------
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
- });
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>${d.name || d.device_id || ""}</td>
365
- <td>${d.size || d.total || ""}</td>
366
- <td>${(d.mountpoints || []).join(", ")}</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
- disksTableBody.appendChild(tr);
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
- if (m.certificates) {
374
- certsEl.textContent = JSON.stringify(m.certificates, null, 2);
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:flex; gap:12px; }
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="gpu" class="meta-value pre">—</div>
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><tr><th>Name</th><th>Size</th><th>Mounts</th></tr></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
- <pre id="certificates" class="pre">—</pre>
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>
@@ -1 +1 @@
1
- __version__ = "0.0.2"
1
+ __version__ = "0.1.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyObservability
3
- Version: 0.0.2
3
+ Version: 0.1.0
4
4
  Summary: Lightweight OS-agnostic observability UI for PyNinja
5
5
  Author-email: Vignesh Rao <svignesh1793@gmail.com>
6
6
  License: MIT License
@@ -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,,