PyObservability 0.0.0a0__py3-none-any.whl → 0.0.0a1__py3-none-any.whl

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