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 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
123
- parsed = urlparse(url)
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
- # 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] = 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
- 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,41 +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
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));
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.cpu) {
263
- 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)));
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.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) {
299
415
  const load = m.cpu_load.detail || m.cpu_load;
300
- if (typeof load === "object") {
301
- const m1 = load.m1 ?? load[0];
302
- const m5 = load.m5 ?? load[1];
303
- const m15 = load.m15 ?? load[2];
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
- 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)) {
317
429
  const filter = svcFilter.value.trim().toLowerCase();
318
- servicesTableBody.innerHTML = "";
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.pid ?? s.PID ?? ""}</td>
437
+ <td>${s.PID ?? ""}</td>
328
438
  <td>${name}</td>
329
- <td>${s.status ?? "—"}</td>
330
- <td>${s.cpu ? JSON.stringify(s.cpu) : "—"}</td>
331
- <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>
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
- // ------------------- DOCKER -------------------
340
- const dockerList = m.docker_stats;
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
- dockerTableHead.innerHTML = "";
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>${d.name || d.device_id || ""}</td>
369
- <td>${d.size || d.total || ""}</td>
370
- <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>
371
469
  `;
372
- disksTableBody.appendChild(tr);
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
- if (m.certificates) {
380
- certsEl.textContent = JSON.stringify(m.certificates, null, 2);
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: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.3"
1
+ __version__ = "0.1.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyObservability
3
- Version: 0.0.3
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=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,,