PyObservability 0.0.0a0__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/__init__.py +0 -0
- pyobservability/main.py +44 -0
- pyobservability/monitor.py +234 -0
- pyobservability/static/app.js +392 -0
- pyobservability/static/styles.css +60 -0
- pyobservability/templates/index.html +129 -0
- pyobservability/version.py +1 -0
- pyobservability-0.0.0a0.dist-info/METADATA +56 -0
- pyobservability-0.0.0a0.dist-info/RECORD +12 -0
- pyobservability-0.0.0a0.dist-info/WHEEL +5 -0
- pyobservability-0.0.0a0.dist-info/licenses/LICENSE +21 -0
- pyobservability-0.0.0a0.dist-info/top_level.txt +1 -0
|
File without changes
|
pyobservability/main.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# app/main.py
|
|
2
|
+
import asyncio
|
|
3
|
+
import pathlib
|
|
4
|
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
|
|
5
|
+
from fastapi.staticfiles import StaticFiles
|
|
6
|
+
from fastapi.templating import Jinja2Templates
|
|
7
|
+
from pyobservability.monitor import Monitor
|
|
8
|
+
import os
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
load_dotenv()
|
|
11
|
+
|
|
12
|
+
app = FastAPI(title="Monitor UI")
|
|
13
|
+
root = pathlib.Path(__file__).parent
|
|
14
|
+
templates_dir = root / "templates"
|
|
15
|
+
static_dir = root / "static"
|
|
16
|
+
templates = Jinja2Templates(directory=templates_dir)
|
|
17
|
+
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
|
18
|
+
|
|
19
|
+
monitor = Monitor(poll_interval=float(os.getenv("POLL_INTERVAL", 2)))
|
|
20
|
+
|
|
21
|
+
@app.get("/")
|
|
22
|
+
async def index(request: Request):
|
|
23
|
+
# pass configured targets to the template so frontend can prebuild UI
|
|
24
|
+
return templates.TemplateResponse("index.html", {"request": request, "targets": monitor.targets})
|
|
25
|
+
|
|
26
|
+
@app.websocket("/ws")
|
|
27
|
+
async def websocket_endpoint(websocket: WebSocket):
|
|
28
|
+
await websocket.accept()
|
|
29
|
+
await monitor.start()
|
|
30
|
+
q = monitor.subscribe()
|
|
31
|
+
try:
|
|
32
|
+
while True:
|
|
33
|
+
payload = await q.get()
|
|
34
|
+
# send as JSON text
|
|
35
|
+
await websocket.send_json(payload)
|
|
36
|
+
except WebSocketDisconnect:
|
|
37
|
+
monitor.unsubscribe(q)
|
|
38
|
+
except Exception:
|
|
39
|
+
monitor.unsubscribe(q)
|
|
40
|
+
try:
|
|
41
|
+
await websocket.close()
|
|
42
|
+
except:
|
|
43
|
+
pass
|
|
44
|
+
await monitor.stop()
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# app/monitor.py
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Dict, List
|
|
7
|
+
import aiohttp
|
|
8
|
+
from asyncio import CancelledError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
###############################################################################
|
|
12
|
+
# ENDPOINT DEFINITIONS (PyNinja Correct)
|
|
13
|
+
###############################################################################
|
|
14
|
+
|
|
15
|
+
ENDPOINTS = {
|
|
16
|
+
"ip": {
|
|
17
|
+
"path": "/get-ip",
|
|
18
|
+
"params": {"public": "false"},
|
|
19
|
+
},
|
|
20
|
+
"cpu": {
|
|
21
|
+
"path": "/get-cpu",
|
|
22
|
+
"params": {"interval": 2, "per_cpu": "true"},
|
|
23
|
+
},
|
|
24
|
+
"cpu_load": {
|
|
25
|
+
"path": "/get-cpu-load",
|
|
26
|
+
"params": {},
|
|
27
|
+
},
|
|
28
|
+
"gpu": {
|
|
29
|
+
"path": "/get-processor",
|
|
30
|
+
"params": {},
|
|
31
|
+
},
|
|
32
|
+
"memory": {
|
|
33
|
+
"path": "/get-memory",
|
|
34
|
+
"params": {},
|
|
35
|
+
},
|
|
36
|
+
"disk": {
|
|
37
|
+
"path": "/get-disk-utilization",
|
|
38
|
+
"params": {"path": "/"},
|
|
39
|
+
},
|
|
40
|
+
"disks": {
|
|
41
|
+
"path": "/get-all-disks",
|
|
42
|
+
"params": {},
|
|
43
|
+
},
|
|
44
|
+
"services": {
|
|
45
|
+
"path": "/get-all-services",
|
|
46
|
+
"params": {},
|
|
47
|
+
},
|
|
48
|
+
"docker_stats": {
|
|
49
|
+
"path": "/get-docker-stats",
|
|
50
|
+
"params": {},
|
|
51
|
+
},
|
|
52
|
+
"certificates": {
|
|
53
|
+
"path": "/get-certificates",
|
|
54
|
+
"params": {},
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
###############################################################################
|
|
60
|
+
# LOAD TARGETS FROM ENV
|
|
61
|
+
###############################################################################
|
|
62
|
+
|
|
63
|
+
def load_targets_from_env() -> List[Dict[str, Any]]:
|
|
64
|
+
raw = os.getenv("MONITOR_TARGETS", "[]")
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
data = json.loads(raw)
|
|
68
|
+
except Exception:
|
|
69
|
+
data = [raw] if raw else []
|
|
70
|
+
|
|
71
|
+
parsed = []
|
|
72
|
+
|
|
73
|
+
for entry in data:
|
|
74
|
+
if isinstance(entry, str):
|
|
75
|
+
parsed.append({"name": entry, "base_url": entry, "apikey": None})
|
|
76
|
+
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
|
+
})
|
|
82
|
+
|
|
83
|
+
return parsed
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
###############################################################################
|
|
87
|
+
# MONITOR CLASS
|
|
88
|
+
###############################################################################
|
|
89
|
+
|
|
90
|
+
class Monitor:
|
|
91
|
+
|
|
92
|
+
def __init__(self, poll_interval: float = 2.0):
|
|
93
|
+
self.targets = load_targets_from_env()
|
|
94
|
+
self.poll_interval = float(os.getenv("POLL_INTERVAL", poll_interval))
|
|
95
|
+
self.sessions: Dict[str, aiohttp.ClientSession] = {}
|
|
96
|
+
self._ws_subscribers: List[asyncio.Queue] = []
|
|
97
|
+
self._task = None
|
|
98
|
+
self._stop = asyncio.Event()
|
|
99
|
+
|
|
100
|
+
############################################################################
|
|
101
|
+
# LIFECYCLE
|
|
102
|
+
############################################################################
|
|
103
|
+
async def start(self):
|
|
104
|
+
for t in self.targets:
|
|
105
|
+
self.sessions[t["base_url"]] = aiohttp.ClientSession()
|
|
106
|
+
self._task = asyncio.create_task(self._run_loop())
|
|
107
|
+
|
|
108
|
+
async def stop(self):
|
|
109
|
+
self._stop.set()
|
|
110
|
+
if self._task:
|
|
111
|
+
self._task.cancel()
|
|
112
|
+
try:
|
|
113
|
+
await self._task
|
|
114
|
+
except CancelledError:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
for sess in self.sessions.values():
|
|
118
|
+
await sess.close()
|
|
119
|
+
|
|
120
|
+
############################################################################
|
|
121
|
+
# SUBSCRIBE / UNSUBSCRIBE
|
|
122
|
+
############################################################################
|
|
123
|
+
def subscribe(self) -> asyncio.Queue:
|
|
124
|
+
q = asyncio.Queue(maxsize=10)
|
|
125
|
+
self._ws_subscribers.append(q)
|
|
126
|
+
return q
|
|
127
|
+
|
|
128
|
+
def unsubscribe(self, q: asyncio.Queue):
|
|
129
|
+
try:
|
|
130
|
+
self._ws_subscribers.remove(q)
|
|
131
|
+
except ValueError:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
############################################################################
|
|
135
|
+
# FETCH WRAPPER
|
|
136
|
+
############################################################################
|
|
137
|
+
async def _fetch(self, session, base_url, ep, apikey=None, params=None):
|
|
138
|
+
url = base_url.rstrip("/") + ep
|
|
139
|
+
headers = {"accept": "application/json"}
|
|
140
|
+
if apikey:
|
|
141
|
+
headers["Authorization"] = f"Bearer {apikey}"
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
async with session.get(url, headers=headers, params=params, timeout=10) as resp:
|
|
145
|
+
if resp.status == 200:
|
|
146
|
+
try:
|
|
147
|
+
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)}
|
|
153
|
+
|
|
154
|
+
############################################################################
|
|
155
|
+
# PER-TARGET POLLING
|
|
156
|
+
############################################################################
|
|
157
|
+
async def _poll_target(self, target: Dict[str, Any]) -> Dict[str, Any]:
|
|
158
|
+
base = target["base_url"]
|
|
159
|
+
apikey = target.get("apikey")
|
|
160
|
+
session = self.sessions[base]
|
|
161
|
+
|
|
162
|
+
result = {
|
|
163
|
+
"name": target["name"],
|
|
164
|
+
"base_url": base,
|
|
165
|
+
"metrics": {}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Fire ALL requests concurrently
|
|
169
|
+
tasks = {}
|
|
170
|
+
|
|
171
|
+
for key, cfg in ENDPOINTS.items():
|
|
172
|
+
tasks[key] = asyncio.create_task(
|
|
173
|
+
self._fetch(
|
|
174
|
+
session,
|
|
175
|
+
base,
|
|
176
|
+
cfg["path"],
|
|
177
|
+
apikey=apikey,
|
|
178
|
+
params=cfg["params"]
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Wait for all endpoints
|
|
183
|
+
raw_results = await asyncio.gather(*tasks.values(), return_exceptions=True)
|
|
184
|
+
|
|
185
|
+
for (key, _), resp in zip(tasks.items(), raw_results):
|
|
186
|
+
if isinstance(resp, Exception):
|
|
187
|
+
result["metrics"][key] = {"error": str(resp)}
|
|
188
|
+
continue
|
|
189
|
+
if isinstance(resp, dict):
|
|
190
|
+
result["metrics"][key] = resp.get("detail", resp)
|
|
191
|
+
else:
|
|
192
|
+
# raw string / number / list / etc
|
|
193
|
+
result["metrics"][key] = resp
|
|
194
|
+
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
############################################################################
|
|
198
|
+
# POLL ALL HOSTS
|
|
199
|
+
############################################################################
|
|
200
|
+
async def _poll_all(self) -> List[Dict[str, Any]]:
|
|
201
|
+
tasks = [self._poll_target(t) for t in self.targets]
|
|
202
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
203
|
+
out = []
|
|
204
|
+
for r in results:
|
|
205
|
+
if isinstance(r, Exception):
|
|
206
|
+
out.append({"error": str(r)})
|
|
207
|
+
else:
|
|
208
|
+
out.append(r)
|
|
209
|
+
return out
|
|
210
|
+
|
|
211
|
+
############################################################################
|
|
212
|
+
# MAIN LOOP
|
|
213
|
+
############################################################################
|
|
214
|
+
async def _run_loop(self):
|
|
215
|
+
while not self._stop.is_set():
|
|
216
|
+
metrics = await self._poll_all()
|
|
217
|
+
|
|
218
|
+
payload = {
|
|
219
|
+
"type": "metrics",
|
|
220
|
+
"ts": asyncio.get_event_loop().time(),
|
|
221
|
+
"data": metrics
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for q in list(self._ws_subscribers):
|
|
225
|
+
try:
|
|
226
|
+
q.put_nowait(payload)
|
|
227
|
+
except asyncio.QueueFull:
|
|
228
|
+
try:
|
|
229
|
+
_ = q.get_nowait()
|
|
230
|
+
q.put_nowait(payload)
|
|
231
|
+
except Exception:
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
await asyncio.sleep(self.poll_interval)
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
// app/static/app.js
|
|
2
|
+
(function () {
|
|
3
|
+
// ------------------------------------------------------------
|
|
4
|
+
// CONFIG
|
|
5
|
+
// ------------------------------------------------------------
|
|
6
|
+
const MAX_POINTS = 60;
|
|
7
|
+
const targets = window.MONITOR_TARGETS || [];
|
|
8
|
+
|
|
9
|
+
// ------------------------------------------------------------
|
|
10
|
+
// DOM REFERENCES
|
|
11
|
+
// ------------------------------------------------------------
|
|
12
|
+
const nodeSelect = document.getElementById("node-select");
|
|
13
|
+
const refreshBtn = document.getElementById("refresh-btn");
|
|
14
|
+
|
|
15
|
+
const ipEl = document.getElementById("ip");
|
|
16
|
+
const gpuEl = document.getElementById("gpu");
|
|
17
|
+
const memEl = document.getElementById("memory");
|
|
18
|
+
const diskEl = document.getElementById("disk");
|
|
19
|
+
const loadEl = document.getElementById("cpuload");
|
|
20
|
+
|
|
21
|
+
const cpuAvgCtx = document.getElementById("cpu-avg-chart").getContext("2d");
|
|
22
|
+
const memCtx = document.getElementById("mem-chart").getContext("2d");
|
|
23
|
+
const loadCtx = document.getElementById("load-chart").getContext("2d");
|
|
24
|
+
|
|
25
|
+
const coresGrid = document.getElementById("cores-grid");
|
|
26
|
+
|
|
27
|
+
const servicesTableBody = document.querySelector("#services-table tbody");
|
|
28
|
+
const svcFilter = document.getElementById("svc-filter");
|
|
29
|
+
|
|
30
|
+
const dockerStatsEl = document.getElementById("docker-stats");
|
|
31
|
+
const containersList = document.getElementById("containers-list");
|
|
32
|
+
|
|
33
|
+
const disksTableBody = document.querySelector("#disks-table tbody");
|
|
34
|
+
const certsEl = document.getElementById("certificates");
|
|
35
|
+
|
|
36
|
+
const showCoresCheckbox = document.getElementById("show-cores");
|
|
37
|
+
|
|
38
|
+
// ------------------------------------------------------------
|
|
39
|
+
// CHART HELPERS
|
|
40
|
+
// ------------------------------------------------------------
|
|
41
|
+
function makeMainChart(ctx, label) {
|
|
42
|
+
const EMPTY = Array(MAX_POINTS).fill(null);
|
|
43
|
+
const LABELS = Array(MAX_POINTS).fill("");
|
|
44
|
+
|
|
45
|
+
return new Chart(ctx, {
|
|
46
|
+
type: "line",
|
|
47
|
+
data: {
|
|
48
|
+
labels: [...LABELS],
|
|
49
|
+
datasets: [
|
|
50
|
+
{
|
|
51
|
+
label,
|
|
52
|
+
data: [...EMPTY],
|
|
53
|
+
fill: true,
|
|
54
|
+
tension: 0.2,
|
|
55
|
+
cubicInterpolationMode: "monotone",
|
|
56
|
+
pointRadius: 0
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
options: {
|
|
61
|
+
animation: false,
|
|
62
|
+
responsive: true,
|
|
63
|
+
maintainAspectRatio: false,
|
|
64
|
+
spanGaps: false,
|
|
65
|
+
scales: {
|
|
66
|
+
x: { display: false },
|
|
67
|
+
y: {
|
|
68
|
+
beginAtZero: true,
|
|
69
|
+
suggestedMax: 100
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
plugins: {
|
|
73
|
+
legend: { display: false }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function makeCoreSparkline(ctx, coreName) {
|
|
80
|
+
const EMPTY_LABELS = Array(MAX_POINTS).fill("");
|
|
81
|
+
const EMPTY_DATA = Array(MAX_POINTS).fill(null);
|
|
82
|
+
|
|
83
|
+
return new Chart(ctx, {
|
|
84
|
+
type: "line",
|
|
85
|
+
data: {
|
|
86
|
+
labels: [...EMPTY_LABELS],
|
|
87
|
+
datasets: [{
|
|
88
|
+
label: coreName,
|
|
89
|
+
data: [...EMPTY_DATA],
|
|
90
|
+
fill: false,
|
|
91
|
+
tension: 0.2,
|
|
92
|
+
pointRadius: 0
|
|
93
|
+
}]
|
|
94
|
+
},
|
|
95
|
+
options: {
|
|
96
|
+
animation: false,
|
|
97
|
+
responsive: false,
|
|
98
|
+
interaction: false,
|
|
99
|
+
events: [],
|
|
100
|
+
spanGaps: false,
|
|
101
|
+
plugins: { legend: { display: false } },
|
|
102
|
+
scales: {
|
|
103
|
+
x: { display: false },
|
|
104
|
+
y: { display: false, suggestedMax: 100 }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const cpuAvgChart = makeMainChart(cpuAvgCtx, "CPU Avg");
|
|
111
|
+
const memChart = makeMainChart(memCtx, "Memory %");
|
|
112
|
+
const loadChart = makeMainChart(loadCtx, "CPU Load");
|
|
113
|
+
|
|
114
|
+
// ------------------------------------------------------------
|
|
115
|
+
// CORE SPARKLINE STATE
|
|
116
|
+
// ------------------------------------------------------------
|
|
117
|
+
const coreMini = {};
|
|
118
|
+
|
|
119
|
+
function createCoreChart(coreName) {
|
|
120
|
+
const wrapper = document.createElement("div");
|
|
121
|
+
wrapper.className = "core-mini";
|
|
122
|
+
wrapper.innerHTML = `
|
|
123
|
+
<div class="label">${coreName}</div>
|
|
124
|
+
<canvas width="120" height="40"></canvas>
|
|
125
|
+
<div class="value">—</div>
|
|
126
|
+
`;
|
|
127
|
+
coresGrid.appendChild(wrapper);
|
|
128
|
+
|
|
129
|
+
const canvas = wrapper.querySelector("canvas");
|
|
130
|
+
const valEl = wrapper.querySelector(".value");
|
|
131
|
+
const chart = makeCoreSparkline(canvas.getContext("2d"), coreName);
|
|
132
|
+
|
|
133
|
+
coreMini[coreName] = { chart, el: wrapper, valEl };
|
|
134
|
+
return coreMini[coreName];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getCoreChart(coreName) {
|
|
138
|
+
return coreMini[coreName] || createCoreChart(coreName);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function pruneOldCores(latest) {
|
|
142
|
+
for (const name of Object.keys(coreMini)) {
|
|
143
|
+
if (!latest.includes(name)) {
|
|
144
|
+
try { coreMini[name].chart.destroy(); } catch {}
|
|
145
|
+
coreMini[name].el.remove();
|
|
146
|
+
delete coreMini[name];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ------------------------------------------------------------
|
|
152
|
+
// RESET UI
|
|
153
|
+
// ------------------------------------------------------------
|
|
154
|
+
function resetUI() {
|
|
155
|
+
// Pre-fill charts with right-anchored null buffers
|
|
156
|
+
const EMPTY_DATA = Array(MAX_POINTS).fill(null);
|
|
157
|
+
const EMPTY_LABELS = Array(MAX_POINTS).fill("");
|
|
158
|
+
|
|
159
|
+
function resetChart(chart) {
|
|
160
|
+
chart.data.labels = [...EMPTY_LABELS];
|
|
161
|
+
chart.data.datasets[0].data = [...EMPTY_DATA];
|
|
162
|
+
chart.update();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Reset main charts (CPU Avg, Memory %, CPU Load)
|
|
166
|
+
resetChart(cpuAvgChart);
|
|
167
|
+
resetChart(memChart);
|
|
168
|
+
resetChart(loadChart);
|
|
169
|
+
|
|
170
|
+
// Remove all per-core mini charts
|
|
171
|
+
for (const name of Object.keys(coreMini)) {
|
|
172
|
+
try { coreMini[name].chart.destroy(); } catch {}
|
|
173
|
+
coreMini[name].el.remove();
|
|
174
|
+
delete coreMini[name];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Reset static UI fields
|
|
178
|
+
ipEl.textContent = "—";
|
|
179
|
+
gpuEl.textContent = "—";
|
|
180
|
+
memEl.textContent = "—";
|
|
181
|
+
diskEl.textContent = "—";
|
|
182
|
+
loadEl.textContent = "—";
|
|
183
|
+
|
|
184
|
+
servicesTableBody.innerHTML = "";
|
|
185
|
+
dockerStatsEl.textContent = "—";
|
|
186
|
+
containersList.innerHTML = "";
|
|
187
|
+
disksTableBody.innerHTML = "";
|
|
188
|
+
certsEl.textContent = "—";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ------------------------------------------------------------
|
|
192
|
+
// HELPERS
|
|
193
|
+
// ------------------------------------------------------------
|
|
194
|
+
function pushPoint(chart, value) {
|
|
195
|
+
const ts = new Date().toLocaleTimeString();
|
|
196
|
+
chart.data.labels.push(ts);
|
|
197
|
+
chart.data.datasets[0].data.push(isFinite(value) ? Number(value) : NaN);
|
|
198
|
+
|
|
199
|
+
if (chart.data.labels.length > MAX_POINTS) {
|
|
200
|
+
chart.data.labels.shift();
|
|
201
|
+
chart.data.datasets[0].data.shift();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
chart.update("none");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function num(x) {
|
|
208
|
+
const n = Number(x);
|
|
209
|
+
return Number.isFinite(n) ? n : null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ------------------------------------------------------------
|
|
213
|
+
// METRICS HANDLER
|
|
214
|
+
// ------------------------------------------------------------
|
|
215
|
+
function handleMetrics(list) {
|
|
216
|
+
const now = new Date().toLocaleTimeString();
|
|
217
|
+
|
|
218
|
+
for (const host of list) {
|
|
219
|
+
if (host.base_url !== selectedBase) continue;
|
|
220
|
+
const m = host.metrics || {};
|
|
221
|
+
|
|
222
|
+
// ------------------- BASIC INFO -------------------
|
|
223
|
+
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) : "—";
|
|
226
|
+
|
|
227
|
+
// ------------------- MEMORY -------------------
|
|
228
|
+
if (m.memory) {
|
|
229
|
+
const used = m.memory.ram_used || m.memory.used || "";
|
|
230
|
+
const percent = m.memory.ram_usage ?? m.memory.usage ?? m.memory.percent ?? "—";
|
|
231
|
+
memEl.textContent = `used: ${used} (${percent}%)`;
|
|
232
|
+
pushPoint(memChart, num(percent));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ------------------- CPU -------------------
|
|
236
|
+
let avg = null;
|
|
237
|
+
|
|
238
|
+
if (m.cpu) {
|
|
239
|
+
const detail = m.cpu.detail || m.cpu;
|
|
240
|
+
|
|
241
|
+
if (typeof detail === "object") {
|
|
242
|
+
const names = Object.keys(detail);
|
|
243
|
+
pruneOldCores(names);
|
|
244
|
+
|
|
245
|
+
const values = [];
|
|
246
|
+
|
|
247
|
+
for (const [core, val] of Object.entries(detail)) {
|
|
248
|
+
const v = num(val);
|
|
249
|
+
values.push(v);
|
|
250
|
+
|
|
251
|
+
const c = getCoreChart(core);
|
|
252
|
+
c.chart.data.labels.push(now);
|
|
253
|
+
c.chart.data.datasets[0].data.push(v ?? 0);
|
|
254
|
+
|
|
255
|
+
if (c.chart.data.labels.length > MAX_POINTS) {
|
|
256
|
+
c.chart.data.labels.shift();
|
|
257
|
+
c.chart.data.datasets[0].data.shift();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
c.chart.update("none");
|
|
261
|
+
c.valEl.textContent = `${(v ?? 0).toFixed(1)}%`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
avg = values.reduce((a, b) => a + (b ?? 0), 0) / values.length;
|
|
265
|
+
}
|
|
266
|
+
else if (typeof detail === "number") {
|
|
267
|
+
avg = detail;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (avg != null) pushPoint(cpuAvgChart, avg);
|
|
272
|
+
|
|
273
|
+
// ------------------- CPU LOAD -------------------
|
|
274
|
+
if (m.cpu_load) {
|
|
275
|
+
const load = m.cpu_load.detail || m.cpu_load;
|
|
276
|
+
if (typeof load === "object") {
|
|
277
|
+
const m1 = load.m1 ?? load[0];
|
|
278
|
+
const m5 = load.m5 ?? load[1];
|
|
279
|
+
const m15 = load.m15 ?? load[2];
|
|
280
|
+
|
|
281
|
+
loadEl.textContent = `${m1} / ${m5} / ${m15}`;
|
|
282
|
+
pushPoint(loadChart, num(m1) ?? 0);
|
|
283
|
+
} else {
|
|
284
|
+
loadEl.textContent = load;
|
|
285
|
+
pushPoint(loadChart, num(load));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ------------------- SERVICES -------------------
|
|
290
|
+
if (Array.isArray(m.services)) {
|
|
291
|
+
const filter = svcFilter.value.trim().toLowerCase();
|
|
292
|
+
servicesTableBody.innerHTML = "";
|
|
293
|
+
|
|
294
|
+
for (const s of m.services) {
|
|
295
|
+
const name = s.pname || s.label || s.name || "";
|
|
296
|
+
|
|
297
|
+
if (filter && !name.toLowerCase().includes(filter)) continue;
|
|
298
|
+
|
|
299
|
+
const tr = document.createElement("tr");
|
|
300
|
+
tr.innerHTML = `
|
|
301
|
+
<td>${s.pid ?? s.PID ?? ""}</td>
|
|
302
|
+
<td>${name}</td>
|
|
303
|
+
<td>${s.status ?? "—"}</td>
|
|
304
|
+
<td>${s.cpu ? JSON.stringify(s.cpu) : "—"}</td>
|
|
305
|
+
<td>${s.memory ? (s.memory.rss || s.memory.pfaults || JSON.stringify(s.memory)) : "—"}</td>
|
|
306
|
+
`;
|
|
307
|
+
servicesTableBody.appendChild(tr);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ------------------- 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);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ------------------- DISKS -------------------
|
|
325
|
+
if (Array.isArray(m.disks)) {
|
|
326
|
+
disksTableBody.innerHTML = "";
|
|
327
|
+
for (const d of m.disks) {
|
|
328
|
+
const tr = document.createElement("tr");
|
|
329
|
+
tr.innerHTML = `
|
|
330
|
+
<td>${d.name || d.device_id || ""}</td>
|
|
331
|
+
<td>${d.size || d.total || ""}</td>
|
|
332
|
+
<td>${(d.mountpoints || []).join(", ")}</td>
|
|
333
|
+
`;
|
|
334
|
+
disksTableBody.appendChild(tr);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ------------------- CERTIFICATES -------------------
|
|
339
|
+
if (m.certificates) {
|
|
340
|
+
certsEl.textContent = JSON.stringify(m.certificates, null, 2);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ------------------------------------------------------------
|
|
346
|
+
// EVENT BINDINGS
|
|
347
|
+
// ------------------------------------------------------------
|
|
348
|
+
targets.forEach(t => {
|
|
349
|
+
const opt = document.createElement("option");
|
|
350
|
+
opt.value = t.base_url;
|
|
351
|
+
opt.textContent = t.name || t.base_url;
|
|
352
|
+
nodeSelect.appendChild(opt);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
let selectedBase =
|
|
356
|
+
nodeSelect.value || (targets[0] && targets[0].base_url);
|
|
357
|
+
nodeSelect.value = selectedBase;
|
|
358
|
+
|
|
359
|
+
nodeSelect.addEventListener("change", () => {
|
|
360
|
+
selectedBase = nodeSelect.value;
|
|
361
|
+
resetUI();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
refreshBtn.addEventListener("click", resetUI);
|
|
365
|
+
|
|
366
|
+
showCoresCheckbox.addEventListener("change", () => {
|
|
367
|
+
const visible = showCoresCheckbox.checked;
|
|
368
|
+
Object.values(coreMini).forEach(c => {
|
|
369
|
+
c.el.style.display = visible ? "block" : "none";
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// ------------------------------------------------------------
|
|
374
|
+
// WEBSOCKET
|
|
375
|
+
// ------------------------------------------------------------
|
|
376
|
+
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
|
377
|
+
const ws = new WebSocket(`${protocol}://${location.host}/ws`);
|
|
378
|
+
|
|
379
|
+
ws.onmessage = evt => {
|
|
380
|
+
try {
|
|
381
|
+
const msg = JSON.parse(evt.data);
|
|
382
|
+
if (msg.type === "metrics") handleMetrics(msg.data);
|
|
383
|
+
} catch (err) {
|
|
384
|
+
console.error("WS parse error:", err);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// ------------------------------------------------------------
|
|
389
|
+
// INIT
|
|
390
|
+
// ------------------------------------------------------------
|
|
391
|
+
resetUI();
|
|
392
|
+
})();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #0b1220;
|
|
3
|
+
--panel: #0f1724;
|
|
4
|
+
--muted: #9fb0d7;
|
|
5
|
+
--accent: #63b3ff;
|
|
6
|
+
--glass: rgba(255,255,255,0.03);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
* { box-sizing: border-box; }
|
|
10
|
+
html,body { height:100%; margin:0; font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; background:var(--bg); color:#e6eef8; }
|
|
11
|
+
|
|
12
|
+
.topbar {
|
|
13
|
+
display:flex; align-items:center; justify-content:space-between;
|
|
14
|
+
padding:12px 20px; background:linear-gradient(90deg,#071028,#0b1530); border-bottom:1px solid rgba(255,255,255,0.02);
|
|
15
|
+
}
|
|
16
|
+
.brand { font-weight:700; font-size:18px; }
|
|
17
|
+
.controls { display:flex; gap:8px; align-items:center; }
|
|
18
|
+
|
|
19
|
+
.controls select, .controls button { background:var(--panel); color:inherit; border:1px solid rgba(255,255,255,0.04); padding:6px 8px; border-radius:6px; }
|
|
20
|
+
.controls label { font-size:14px; color:var(--muted); margin-right:6px; }
|
|
21
|
+
|
|
22
|
+
.container { padding:18px; display:flex; flex-direction:column; gap:16px; max-width:1200px; margin:0 auto; }
|
|
23
|
+
|
|
24
|
+
.meta-row { display:flex; gap:12px; }
|
|
25
|
+
.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
|
+
.meta-title { font-size:12px; color:var(--muted); }
|
|
27
|
+
.meta-value { margin-top:6px; font-weight:600; font-size:16px; }
|
|
28
|
+
.meta-value.pre { white-space:pre-wrap; font-family:monospace; font-size:13px; }
|
|
29
|
+
|
|
30
|
+
.charts-row { display:flex; gap:12px; }
|
|
31
|
+
.panel { background:var(--panel); border-radius:8px; padding:12px; box-shadow:0 6px 18px rgba(2,6,23,0.6); flex:1; }
|
|
32
|
+
.panel.wide { flex:2; }
|
|
33
|
+
.panel.narrow { flex:0.8; }
|
|
34
|
+
.panel-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:6px; }
|
|
35
|
+
.panel-header h3 { margin:0; font-size:14px; }
|
|
36
|
+
.panel-actions { font-size:13px; color:var(--muted); }
|
|
37
|
+
|
|
38
|
+
.chart { height:220px !important; width:100% !important; }
|
|
39
|
+
.chart-small { height:120px !important; width:100% !important; }
|
|
40
|
+
|
|
41
|
+
.details-row { display:flex; gap:12px; }
|
|
42
|
+
.cores-grid { display:grid; grid-template-columns: repeat(auto-fit,minmax(120px,1fr)); gap:8px; width:100%; overflow:hidden; }
|
|
43
|
+
.core-mini { background:var(--glass); padding:6px; border-radius:6px; text-align:center; font-size:12px; }
|
|
44
|
+
.core-mini .label { color:var(--muted); font-size:11px; }
|
|
45
|
+
.core-mini .value { font-weight:700; margin-top:6px; }
|
|
46
|
+
|
|
47
|
+
.tables-row { display:grid; grid-template-columns: repeat(2, 1fr); gap:12px; }
|
|
48
|
+
.table { width:100%; border-collapse:collapse; font-size:13px; }
|
|
49
|
+
.table th, .table td { padding:8px; border-bottom:1px solid rgba(255,255,255,0.03); text-align:left; }
|
|
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; }
|
|
51
|
+
.list { padding-left:14px; margin:8px 0; }
|
|
52
|
+
.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
|
+
.service-controls { display:flex; gap:8px; padding-bottom:8px; }
|
|
54
|
+
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; }
|
|
55
|
+
|
|
56
|
+
@media (max-width:900px) {
|
|
57
|
+
.tables-row { grid-template-columns: 1fr; }
|
|
58
|
+
.charts-row { flex-direction:column; }
|
|
59
|
+
.details-row { flex-direction:column; }
|
|
60
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
|
+
<title>Node Monitor — Single Node View</title>
|
|
7
|
+
|
|
8
|
+
<link rel="stylesheet" href="/static/styles.css" />
|
|
9
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<header class="topbar">
|
|
13
|
+
<div class="brand">Node Monitor</div>
|
|
14
|
+
|
|
15
|
+
<div class="controls">
|
|
16
|
+
<label for="node-select">Node:</label>
|
|
17
|
+
<select id="node-select" aria-label="Select node"></select>
|
|
18
|
+
<button id="refresh-btn" title="Force refresh">Refresh</button>
|
|
19
|
+
</div>
|
|
20
|
+
</header>
|
|
21
|
+
|
|
22
|
+
<main class="container">
|
|
23
|
+
<section class="meta-row">
|
|
24
|
+
<div class="meta-card">
|
|
25
|
+
<div class="meta-title">IP</div>
|
|
26
|
+
<div id="ip" class="meta-value">—</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="meta-card">
|
|
30
|
+
<div class="meta-title">GPU / CPU Model</div>
|
|
31
|
+
<div id="gpu" class="meta-value pre">—</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="meta-card">
|
|
35
|
+
<div class="meta-title">CPU Load (1/5/15)</div>
|
|
36
|
+
<div id="cpuload" class="meta-value">—</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="meta-card">
|
|
40
|
+
<div class="meta-title">Disk ( / )</div>
|
|
41
|
+
<div id="disk" class="meta-value pre">—</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="meta-card">
|
|
45
|
+
<div class="meta-title">Memory</div>
|
|
46
|
+
<div id="memory" class="meta-value pre">—</div>
|
|
47
|
+
</div>
|
|
48
|
+
</section>
|
|
49
|
+
|
|
50
|
+
<section class="charts-row">
|
|
51
|
+
<div class="panel">
|
|
52
|
+
<div class="panel-header">
|
|
53
|
+
<h3>CPU (Average)</h3>
|
|
54
|
+
<div class="panel-actions">
|
|
55
|
+
<label><input type="checkbox" id="show-cores" /> Show per-core</label>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
<canvas id="cpu-avg-chart" class="chart"></canvas>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div class="panel">
|
|
62
|
+
<div class="panel-header"><h3>Memory (%)</h3></div>
|
|
63
|
+
<canvas id="mem-chart" class="chart"></canvas>
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
66
|
+
|
|
67
|
+
<section class="details-row">
|
|
68
|
+
<div class="panel wide">
|
|
69
|
+
<div class="panel-header"><h3>Per-Core (sparklines)</h3></div>
|
|
70
|
+
<div id="cores-grid" class="cores-grid"></div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div class="panel narrow">
|
|
74
|
+
<div class="panel-header"><h3>CPU Load History</h3></div>
|
|
75
|
+
<canvas id="load-chart" class="chart-small"></canvas>
|
|
76
|
+
</div>
|
|
77
|
+
</section>
|
|
78
|
+
|
|
79
|
+
<section class="tables-row">
|
|
80
|
+
<div class="panel">
|
|
81
|
+
<div class="panel-header"><h3>Services</h3></div>
|
|
82
|
+
<div class="panel-body service-controls">
|
|
83
|
+
<input id="svc-filter" placeholder="filter service name..." />
|
|
84
|
+
</div>
|
|
85
|
+
<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>
|
|
87
|
+
<tbody></tbody>
|
|
88
|
+
</table>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div class="panel">
|
|
92
|
+
<div class="panel-header"><h3>Docker / Containers</h3></div>
|
|
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>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div class="panel">
|
|
101
|
+
<div class="panel-header"><h3>Disks</h3></div>
|
|
102
|
+
<div class="panel-body">
|
|
103
|
+
<table class="table" id="disks-table">
|
|
104
|
+
<thead><tr><th>Name</th><th>Size</th><th>Mounts</th></tr></thead>
|
|
105
|
+
<tbody></tbody>
|
|
106
|
+
</table>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div class="panel">
|
|
111
|
+
<div class="panel-header"><h3>Certificates</h3></div>
|
|
112
|
+
<div class="panel-body">
|
|
113
|
+
<pre id="certificates" class="pre">—</pre>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</section>
|
|
117
|
+
</main>
|
|
118
|
+
|
|
119
|
+
<footer class="footer">
|
|
120
|
+
<div>OpenAPI: <code>/static/openapi.json</code> (uploaded file available at <code>/mnt/data/openapi.json</code>)</div>
|
|
121
|
+
</footer>
|
|
122
|
+
|
|
123
|
+
<script>
|
|
124
|
+
// inject server-side targets
|
|
125
|
+
window.MONITOR_TARGETS = {{ targets | tojson }};
|
|
126
|
+
</script>
|
|
127
|
+
<script src="/static/app.js"></script>
|
|
128
|
+
</body>
|
|
129
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.0a0"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PyObservability
|
|
3
|
+
Version: 0.0.0a0
|
|
4
|
+
Summary: Lightweight OS-agnostic observability UI for PyNinja
|
|
5
|
+
Author-email: Vignesh Rao <svignesh1793@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 TheVickypedia
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/thevickypedia/PyObservability
|
|
29
|
+
Project-URL: Docs, https://thevickypedia.github.io/PyObservability
|
|
30
|
+
Project-URL: Source, https://github.com/thevickypedia/PyObservability
|
|
31
|
+
Project-URL: Bug Tracker, https://github.com/thevickypedia/PyObservability/issues
|
|
32
|
+
Project-URL: Release Notes, https://github.com/thevickypedia/PyObservability/blob/main/release_notes.rst
|
|
33
|
+
Keywords: PyObservability,observability,system-monitor,PyNinja
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Development Status :: 3 - Alpha
|
|
37
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
38
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
39
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
40
|
+
Classifier: Topic :: System :: Monitoring
|
|
41
|
+
Requires-Python: >=3.11
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
License-File: LICENSE
|
|
44
|
+
Requires-Dist: fastapi
|
|
45
|
+
Requires-Dist: uvicorn[standard]
|
|
46
|
+
Requires-Dist: aiohttp
|
|
47
|
+
Requires-Dist: jinja2
|
|
48
|
+
Requires-Dist: python-dotenv
|
|
49
|
+
Provides-Extra: dev
|
|
50
|
+
Requires-Dist: sphinx==5.1.1; extra == "dev"
|
|
51
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
52
|
+
Requires-Dist: recommonmark; extra == "dev"
|
|
53
|
+
Requires-Dist: gitverse; extra == "dev"
|
|
54
|
+
Dynamic: license-file
|
|
55
|
+
|
|
56
|
+
# PyObservability
|
|
@@ -0,0 +1,12 @@
|
|
|
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,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 TheVickypedia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyobservability
|