PyObservability 1.4.1__py3-none-any.whl → 2.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/config/enums.py +1 -0
- pyobservability/config/settings.py +5 -0
- pyobservability/kuma.py +111 -0
- pyobservability/main.py +57 -16
- pyobservability/static/app.js +179 -14
- pyobservability/static/styles.css +72 -0
- pyobservability/templates/index.html +37 -2
- pyobservability/version.py +1 -1
- {pyobservability-1.4.1.dist-info → pyobservability-2.1.0.dist-info}/METADATA +14 -4
- pyobservability-2.1.0.dist-info/RECORD +17 -0
- {pyobservability-1.4.1.dist-info → pyobservability-2.1.0.dist-info}/WHEEL +1 -1
- pyobservability-1.4.1.dist-info/RECORD +0 -16
- {pyobservability-1.4.1.dist-info → pyobservability-2.1.0.dist-info}/entry_points.txt +0 -0
- {pyobservability-1.4.1.dist-info → pyobservability-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {pyobservability-1.4.1.dist-info → pyobservability-2.1.0.dist-info}/top_level.txt +0 -0
pyobservability/config/enums.py
CHANGED
|
@@ -132,6 +132,11 @@ class EnvConfig(PydanticEnvConfig):
|
|
|
132
132
|
password: str | None = Field(None, validation_alias=alias_choices("PASSWORD"))
|
|
133
133
|
timeout: PositiveInt = Field(300, validation_alias=alias_choices("TIMEOUT"))
|
|
134
134
|
|
|
135
|
+
kuma_url: str | None = None
|
|
136
|
+
kuma_username: str | None = None
|
|
137
|
+
kuma_password: str | None = None
|
|
138
|
+
kuma_timeout: PositiveInt = 5
|
|
139
|
+
|
|
135
140
|
class Config:
|
|
136
141
|
"""Environment variables configuration."""
|
|
137
142
|
|
pyobservability/kuma.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from collections.abc import Generator
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
|
|
7
|
+
import socketio
|
|
8
|
+
|
|
9
|
+
from pyobservability.config import settings
|
|
10
|
+
|
|
11
|
+
LOGGER = logging.getLogger("uvicorn.default")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UptimeKumaClient:
|
|
15
|
+
"""Client to interact with Uptime Kuma server via Socket.IO.
|
|
16
|
+
|
|
17
|
+
>>> UptimeKumaClient
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
"""Initialize the Uptime Kuma client."""
|
|
23
|
+
self.sio = socketio.Client()
|
|
24
|
+
self.monitors = {}
|
|
25
|
+
self.logged_in = False
|
|
26
|
+
|
|
27
|
+
self.sio.on("monitorList", self._on_monitor_list)
|
|
28
|
+
|
|
29
|
+
def _on_monitor_list(self, data):
|
|
30
|
+
"""Handle incoming monitor list from Uptime Kuma server."""
|
|
31
|
+
LOGGER.debug("Received monitor list from Uptime Kuma server.")
|
|
32
|
+
self.monitors = data
|
|
33
|
+
|
|
34
|
+
def connect(self):
|
|
35
|
+
"""Connect to the Uptime Kuma server via Socket.IO."""
|
|
36
|
+
LOGGER.debug("Connecting to Uptime Kuma server at %s", settings.env.kuma_url)
|
|
37
|
+
self.sio.connect(settings.env.kuma_url)
|
|
38
|
+
|
|
39
|
+
def login(self):
|
|
40
|
+
"""Log in to the Uptime Kuma server."""
|
|
41
|
+
result = {"ok": False}
|
|
42
|
+
|
|
43
|
+
def callback(resp):
|
|
44
|
+
"""Callback to handle login response."""
|
|
45
|
+
result.update(resp or {"ok": False})
|
|
46
|
+
|
|
47
|
+
self.sio.emit(
|
|
48
|
+
"login",
|
|
49
|
+
{
|
|
50
|
+
"username": settings.env.kuma_username,
|
|
51
|
+
"password": settings.env.kuma_password,
|
|
52
|
+
"token": "",
|
|
53
|
+
},
|
|
54
|
+
callback=callback,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
end = time.time() + settings.env.kuma_timeout
|
|
58
|
+
while not result.get("ok") and time.time() < end:
|
|
59
|
+
time.sleep(0.05)
|
|
60
|
+
|
|
61
|
+
if not result.get("ok"):
|
|
62
|
+
raise RuntimeError("Uptime Kuma login failed")
|
|
63
|
+
|
|
64
|
+
self.logged_in = True
|
|
65
|
+
|
|
66
|
+
def get_monitors(self):
|
|
67
|
+
"""Retrieve monitors from the Uptime Kuma server."""
|
|
68
|
+
if not self.sio.connected:
|
|
69
|
+
self.connect()
|
|
70
|
+
|
|
71
|
+
if not self.logged_in:
|
|
72
|
+
self.login()
|
|
73
|
+
|
|
74
|
+
end = time.time() + settings.env.kuma_timeout
|
|
75
|
+
while not self.monitors and time.time() < end:
|
|
76
|
+
time.sleep(0.05)
|
|
77
|
+
|
|
78
|
+
if not self.monitors:
|
|
79
|
+
raise RuntimeError("No monitors received")
|
|
80
|
+
|
|
81
|
+
return self.monitors
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def extract_monitors(payload: Dict[int, Dict[str, Any]]) -> Generator[Dict[str, Any]]:
|
|
85
|
+
"""Convert raw API payload into a list of dicts with name, url, tags, host.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
payload: Raw payload from Uptime Kuma server.
|
|
89
|
+
|
|
90
|
+
Yields:
|
|
91
|
+
Dict[str, Any]:
|
|
92
|
+
Monitors with relevant fields.
|
|
93
|
+
"""
|
|
94
|
+
grouped = {}
|
|
95
|
+
for monitor in payload.values():
|
|
96
|
+
if children_ids := monitor.get("childrenIDs"):
|
|
97
|
+
for child in children_ids:
|
|
98
|
+
grouped[child] = monitor.get("name")
|
|
99
|
+
|
|
100
|
+
for monitor in payload.values():
|
|
101
|
+
url = monitor.get("url").replace("host.docker.internal", urlparse(settings.env.kuma_url).hostname)
|
|
102
|
+
host = urlparse(url).hostname if url else None
|
|
103
|
+
if not host:
|
|
104
|
+
continue
|
|
105
|
+
yield {
|
|
106
|
+
"name": monitor.get("name"),
|
|
107
|
+
"parent": grouped.get(monitor.get("id")),
|
|
108
|
+
"url": url,
|
|
109
|
+
"host": host,
|
|
110
|
+
"tags": [tag.get("name") for tag in monitor.get("tags", []) if "name" in tag],
|
|
111
|
+
}
|
pyobservability/main.py
CHANGED
|
@@ -2,16 +2,18 @@ import logging
|
|
|
2
2
|
import pathlib
|
|
3
3
|
import warnings
|
|
4
4
|
from datetime import datetime
|
|
5
|
-
from
|
|
5
|
+
from http import HTTPStatus
|
|
6
|
+
from typing import Dict
|
|
6
7
|
|
|
7
8
|
import uiauth
|
|
8
9
|
import uvicorn
|
|
9
|
-
from fastapi import FastAPI, Request
|
|
10
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
10
11
|
from fastapi.routing import APIRoute, APIWebSocketRoute
|
|
11
12
|
from fastapi.staticfiles import StaticFiles
|
|
12
13
|
from fastapi.templating import Jinja2Templates
|
|
13
14
|
|
|
14
15
|
from pyobservability.config import enums, settings
|
|
16
|
+
from pyobservability.kuma import UptimeKumaClient, extract_monitors
|
|
15
17
|
from pyobservability.transport import websocket_endpoint
|
|
16
18
|
from pyobservability.version import __version__
|
|
17
19
|
|
|
@@ -39,12 +41,31 @@ async def index(request: Request):
|
|
|
39
41
|
TemplateResponse:
|
|
40
42
|
Rendered HTML template with targets and version.
|
|
41
43
|
"""
|
|
42
|
-
|
|
44
|
+
kuma_data = [{}] if all((settings.env.kuma_url, settings.env.kuma_username, settings.env.kuma_password)) else None
|
|
45
|
+
args = dict(request=request, kuma_data=kuma_data, targets=settings.env.targets, version=__version__)
|
|
43
46
|
if settings.env.username and settings.env.password:
|
|
44
47
|
args["logout"] = uiauth.enums.APIEndpoints.fastapi_logout.value
|
|
45
48
|
return templates.TemplateResponse("index.html", args)
|
|
46
49
|
|
|
47
50
|
|
|
51
|
+
async def kuma():
|
|
52
|
+
"""Kuma endpoint to retrieve monitors from Kuma server.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Dict[str, Any]:
|
|
56
|
+
Grouped monitors by host from Kuma server.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
kuma_data = UptimeKumaClient().get_monitors()
|
|
60
|
+
LOGGER.info("Retrieved payload from kuma server.")
|
|
61
|
+
except RuntimeError:
|
|
62
|
+
raise HTTPException(
|
|
63
|
+
status_code=HTTPStatus.SERVICE_UNAVAILABLE.real,
|
|
64
|
+
detail="Unable to retrieve data from kuma server.",
|
|
65
|
+
)
|
|
66
|
+
return list(extract_monitors(kuma_data))
|
|
67
|
+
|
|
68
|
+
|
|
48
69
|
async def health() -> Dict[str, str]:
|
|
49
70
|
"""Health check endpoint.
|
|
50
71
|
|
|
@@ -65,25 +86,35 @@ def include_routes() -> None:
|
|
|
65
86
|
include_in_schema=False,
|
|
66
87
|
),
|
|
67
88
|
)
|
|
89
|
+
kuma_enabled = all((settings.env.kuma_url, settings.env.kuma_username, settings.env.kuma_password))
|
|
68
90
|
if all((settings.env.username, settings.env.password)):
|
|
91
|
+
auth_endpoints = [
|
|
92
|
+
uiauth.Parameters(
|
|
93
|
+
path=enums.APIEndpoints.root,
|
|
94
|
+
function=index,
|
|
95
|
+
methods=[uiauth.enums.APIMethods.GET],
|
|
96
|
+
),
|
|
97
|
+
uiauth.Parameters(
|
|
98
|
+
path=enums.APIEndpoints.ws,
|
|
99
|
+
function=websocket_endpoint,
|
|
100
|
+
route=APIWebSocketRoute,
|
|
101
|
+
),
|
|
102
|
+
]
|
|
103
|
+
if kuma_enabled:
|
|
104
|
+
auth_endpoints.append(
|
|
105
|
+
uiauth.Parameters(
|
|
106
|
+
path=enums.APIEndpoints.kuma,
|
|
107
|
+
function=kuma,
|
|
108
|
+
methods=[uiauth.enums.APIMethods.GET],
|
|
109
|
+
)
|
|
110
|
+
)
|
|
69
111
|
uiauth.protect(
|
|
70
112
|
app=PyObservability,
|
|
71
113
|
username=settings.env.username,
|
|
72
114
|
password=settings.env.password,
|
|
73
115
|
timeout=settings.env.timeout,
|
|
74
116
|
custom_logger=LOGGER,
|
|
75
|
-
params=
|
|
76
|
-
uiauth.Parameters(
|
|
77
|
-
path=enums.APIEndpoints.root,
|
|
78
|
-
function=index,
|
|
79
|
-
methods=[uiauth.enums.APIMethods.GET],
|
|
80
|
-
),
|
|
81
|
-
uiauth.Parameters(
|
|
82
|
-
path=enums.APIEndpoints.ws,
|
|
83
|
-
function=websocket_endpoint,
|
|
84
|
-
route=APIWebSocketRoute,
|
|
85
|
-
),
|
|
86
|
-
],
|
|
117
|
+
params=auth_endpoints,
|
|
87
118
|
)
|
|
88
119
|
else:
|
|
89
120
|
warnings.warn("\n\tRunning PyObservability without any protection.", UserWarning)
|
|
@@ -99,10 +130,20 @@ def include_routes() -> None:
|
|
|
99
130
|
APIWebSocketRoute(
|
|
100
131
|
path=enums.APIEndpoints.ws,
|
|
101
132
|
endpoint=websocket_endpoint,
|
|
102
|
-
)
|
|
133
|
+
),
|
|
103
134
|
)
|
|
135
|
+
if kuma_enabled:
|
|
136
|
+
PyObservability.routes.append(
|
|
137
|
+
APIRoute(
|
|
138
|
+
path=enums.APIEndpoints.kuma,
|
|
139
|
+
endpoint=kuma,
|
|
140
|
+
methods=["GET"],
|
|
141
|
+
include_in_schema=False,
|
|
142
|
+
),
|
|
143
|
+
)
|
|
104
144
|
|
|
105
145
|
|
|
146
|
+
# noinspection PyTypeChecker
|
|
106
147
|
def start(**kwargs) -> None:
|
|
107
148
|
"""Start the FastAPI app with Uvicorn server."""
|
|
108
149
|
settings.env = settings.env_loader(**kwargs)
|
pyobservability/static/app.js
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
const DEFAULT_PAGE_SIZE = 10;
|
|
9
9
|
const panelSpinners = {};
|
|
10
10
|
|
|
11
|
+
// Tab management
|
|
12
|
+
let currentTab = 'nodes';
|
|
13
|
+
let ws = null;
|
|
14
|
+
let kumaMapData = null;
|
|
15
|
+
let kumaMapLoaded = false;
|
|
16
|
+
|
|
11
17
|
// ------------------------------------------------------------
|
|
12
18
|
// VISUAL SPINNERS
|
|
13
19
|
// ------------------------------------------------------------
|
|
@@ -88,6 +94,10 @@
|
|
|
88
94
|
const certsTableHead = certsTable.querySelector("thead");
|
|
89
95
|
const certsTableBody = certsTable.querySelector("tbody");
|
|
90
96
|
|
|
97
|
+
const endpointsTable = document.getElementById("endpoints-table");
|
|
98
|
+
const endpointsTableHead = endpointsTable.querySelector("thead");
|
|
99
|
+
const endpointsTableBody = endpointsTable.querySelector("tbody");
|
|
100
|
+
|
|
91
101
|
const showCoresCheckbox = document.getElementById("show-cores");
|
|
92
102
|
|
|
93
103
|
// ------------------------------------------------------------
|
|
@@ -320,6 +330,11 @@
|
|
|
320
330
|
const PAG_CERTS = createPaginatedTable(
|
|
321
331
|
certsTable, certsTableHead, certsTableBody
|
|
322
332
|
);
|
|
333
|
+
const PAG_ENDPOINTS = createPaginatedTable(
|
|
334
|
+
endpointsTable,
|
|
335
|
+
endpointsTableHead,
|
|
336
|
+
endpointsTableBody
|
|
337
|
+
);
|
|
323
338
|
|
|
324
339
|
// ------------------------------------------------------------
|
|
325
340
|
// CHART HELPERS
|
|
@@ -351,6 +366,30 @@
|
|
|
351
366
|
});
|
|
352
367
|
}
|
|
353
368
|
|
|
369
|
+
function normalizeKumaMap(monitors) {
|
|
370
|
+
const rows = [];
|
|
371
|
+
monitors.forEach(monitor => {
|
|
372
|
+
rows.push({
|
|
373
|
+
Node: monitor.host,
|
|
374
|
+
Name: monitor.name,
|
|
375
|
+
Parent: monitor.parent || "—",
|
|
376
|
+
Tags: (monitor.tags || []).join(", "),
|
|
377
|
+
URL: `<a href="${monitor.url}" target="_blank">${monitor.url}</a>`
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
return rows;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function renderEndpoints() {
|
|
384
|
+
if (!window.KUMA_DATA) return;
|
|
385
|
+
|
|
386
|
+
const rows = normalizeKumaMap(window.KUMA_DATA);
|
|
387
|
+
const columns = ["Node", "Name", "Parent", "Tags", "URL"];
|
|
388
|
+
|
|
389
|
+
PAG_ENDPOINTS.setData(rows, columns);
|
|
390
|
+
hideSpinner("endpoints-table");
|
|
391
|
+
}
|
|
392
|
+
|
|
354
393
|
function makeCoreSparkline(ctx, coreName) {
|
|
355
394
|
const EMPTY_LABELS = Array(MAX_POINTS).fill("");
|
|
356
395
|
const EMPTY_DATA = Array(MAX_POINTS).fill(null);
|
|
@@ -438,8 +477,16 @@
|
|
|
438
477
|
);
|
|
439
478
|
|
|
440
479
|
let unifiedNodes = [];
|
|
441
|
-
|
|
442
|
-
|
|
480
|
+
const colorPalette = [
|
|
481
|
+
"#ff0000",
|
|
482
|
+
"#ffff00",
|
|
483
|
+
"#00ff00",
|
|
484
|
+
"#0066ff",
|
|
485
|
+
"#b300ff",
|
|
486
|
+
"#ff7f00",
|
|
487
|
+
"#8b4513",
|
|
488
|
+
"#7f7f7f"
|
|
489
|
+
];
|
|
443
490
|
const nodeColor = {};
|
|
444
491
|
const unifiedCharts = {memory: null, cpu: null, disk: null};
|
|
445
492
|
|
|
@@ -1082,29 +1129,147 @@
|
|
|
1082
1129
|
});
|
|
1083
1130
|
|
|
1084
1131
|
// ------------------------------------------------------------
|
|
1085
|
-
//
|
|
1132
|
+
// TAB MANAGEMENT
|
|
1086
1133
|
// ------------------------------------------------------------
|
|
1087
|
-
const
|
|
1088
|
-
const
|
|
1134
|
+
const nodesTab = document.getElementById("nodes-tab");
|
|
1135
|
+
const kumaTab = document.getElementById("kuma-tab");
|
|
1136
|
+
const kumaMainTable = document.getElementById("kuma-main-table");
|
|
1137
|
+
const kumaMainThead = kumaMainTable.querySelector("thead");
|
|
1138
|
+
const kumaMainTbody = kumaMainTable.querySelector("tbody");
|
|
1139
|
+
const kumaSearchInput = document.getElementById("kuma-search-input");
|
|
1140
|
+
const controlsDiv = document.querySelector(".controls");
|
|
1141
|
+
|
|
1142
|
+
// Create paginated table for kuma using existing infrastructure
|
|
1143
|
+
const PAG_KUMA_TAB = createPaginatedTable(
|
|
1144
|
+
kumaMainTable, kumaMainThead, kumaMainTbody, 20
|
|
1145
|
+
);
|
|
1089
1146
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1147
|
+
let allKumaRows = [];
|
|
1148
|
+
|
|
1149
|
+
function initWebSocket() {
|
|
1150
|
+
if (ws) return;
|
|
1151
|
+
|
|
1152
|
+
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
|
1153
|
+
ws = new WebSocket(`${protocol}://${location.host}/ws`);
|
|
1154
|
+
|
|
1155
|
+
ws.onopen = () => {
|
|
1156
|
+
ws.send(JSON.stringify({type: "select_target", base_url: selectedBase}));
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
ws.onmessage = evt => {
|
|
1160
|
+
try {
|
|
1161
|
+
const msg = JSON.parse(evt.data);
|
|
1162
|
+
if (msg.type === "metrics") handleMetrics(msg.data);
|
|
1163
|
+
if (msg.type === "error") alert(msg.message);
|
|
1164
|
+
} catch (err) {
|
|
1165
|
+
console.error("WS parse error:", err);
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
ws.onerror = (err) => {
|
|
1170
|
+
console.error("WebSocket error:", err);
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
ws.onclose = () => {
|
|
1174
|
+
console.log("WebSocket closed");
|
|
1175
|
+
ws = null;
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function closeWebSocket() {
|
|
1180
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1181
|
+
ws.close();
|
|
1182
|
+
ws = null;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
async function loadKumaMap() {
|
|
1187
|
+
if (kumaMapLoaded) {
|
|
1188
|
+
PAG_KUMA_TAB.setData(allKumaRows, ["Node", "Name", "Parent", "Tags", "URL"]);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Show loading state
|
|
1193
|
+
kumaMainThead.innerHTML = '<tr><th colspan="5">Loading...</th></tr>';
|
|
1194
|
+
kumaMainTbody.innerHTML = '';
|
|
1093
1195
|
|
|
1094
|
-
ws.onmessage = evt => {
|
|
1095
1196
|
try {
|
|
1096
|
-
const
|
|
1097
|
-
if (
|
|
1098
|
-
|
|
1197
|
+
const response = await fetch('/kuma');
|
|
1198
|
+
if (!response.ok) throw new Error('Failed to fetch kuma map');
|
|
1199
|
+
|
|
1200
|
+
kumaMapData = await response.json();
|
|
1201
|
+
kumaMapLoaded = true;
|
|
1202
|
+
|
|
1203
|
+
allKumaRows = normalizeKumaMap(kumaMapData);
|
|
1204
|
+
PAG_KUMA_TAB.setData(allKumaRows, ["Node", "Name", "Parent", "Tags", "URL"]);
|
|
1099
1205
|
} catch (err) {
|
|
1100
|
-
console.error("
|
|
1206
|
+
console.error("Error loading Kuma map:", err);
|
|
1207
|
+
kumaMainThead.innerHTML = '<tr><th>Error</th></tr>';
|
|
1208
|
+
kumaMainTbody.innerHTML = '<tr><td>Error loading Kuma endpoints. Please try again.</td></tr>';
|
|
1101
1209
|
}
|
|
1102
|
-
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
kumaSearchInput.addEventListener('input', (e) => {
|
|
1213
|
+
const searchTerm = e.target.value.toLowerCase();
|
|
1214
|
+
|
|
1215
|
+
if (!searchTerm) {
|
|
1216
|
+
PAG_KUMA_TAB.setData(allKumaRows, ["Node", "Name", "Parent", "Tags", "URL"]);
|
|
1217
|
+
} else {
|
|
1218
|
+
const filtered = allKumaRows.filter(row =>
|
|
1219
|
+
row.Node.toLowerCase().includes(searchTerm) ||
|
|
1220
|
+
row.Name.toLowerCase().includes(searchTerm) ||
|
|
1221
|
+
row.Parent.toLowerCase().includes(searchTerm) ||
|
|
1222
|
+
row.Tags.toLowerCase().includes(searchTerm) ||
|
|
1223
|
+
row.URL.toLowerCase().includes(searchTerm)
|
|
1224
|
+
);
|
|
1225
|
+
PAG_KUMA_TAB.setData(filtered, ["Node", "Name", "Parent", "Tags", "URL"]);
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
function switchToNodesTab() {
|
|
1230
|
+
currentTab = 'nodes';
|
|
1231
|
+
nodesTab.classList.add('active');
|
|
1232
|
+
kumaTab.classList.remove('active');
|
|
1233
|
+
document.body.classList.remove('kuma-view');
|
|
1234
|
+
document.body.classList.add('nodes-view');
|
|
1235
|
+
|
|
1236
|
+
controlsDiv.classList.remove('invisible');
|
|
1237
|
+
|
|
1238
|
+
closeWebSocket();
|
|
1239
|
+
initWebSocket();
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function switchToKumaTab() {
|
|
1243
|
+
currentTab = 'kuma';
|
|
1244
|
+
kumaTab.classList.add('active');
|
|
1245
|
+
nodesTab.classList.remove('active');
|
|
1246
|
+
document.body.classList.add('kuma-view');
|
|
1247
|
+
document.body.classList.remove('nodes-view');
|
|
1248
|
+
|
|
1249
|
+
controlsDiv.classList.add('invisible');
|
|
1250
|
+
|
|
1251
|
+
closeWebSocket();
|
|
1252
|
+
loadKumaMap();
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
if (nodesTab && kumaTab) {
|
|
1256
|
+
nodesTab.addEventListener('click', switchToNodesTab);
|
|
1257
|
+
kumaTab.addEventListener('click', switchToKumaTab);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// ------------------------------------------------------------
|
|
1261
|
+
// WEBSOCKET (Managed by tabs)
|
|
1262
|
+
// ------------------------------------------------------------
|
|
1103
1263
|
|
|
1104
1264
|
// ------------------------------------------------------------
|
|
1105
1265
|
// INIT
|
|
1106
1266
|
// ------------------------------------------------------------
|
|
1267
|
+
renderEndpoints();
|
|
1107
1268
|
attachSpinners();
|
|
1108
1269
|
resetUI(); // reset UI, keep spinners visible
|
|
1109
1270
|
showAllSpinners(); // show spinners until first metrics arrive
|
|
1271
|
+
|
|
1272
|
+
// Initialize nodes view by default
|
|
1273
|
+
document.body.classList.add('nodes-view');
|
|
1274
|
+
initWebSocket();
|
|
1110
1275
|
})();
|
|
@@ -32,6 +32,34 @@ html, body {
|
|
|
32
32
|
border-bottom: 1px solid rgba(255, 255, 255, 0.02);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
.tab-navigation {
|
|
36
|
+
display: flex;
|
|
37
|
+
gap: 12px;
|
|
38
|
+
flex: 1;
|
|
39
|
+
justify-content: center;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.tab-btn {
|
|
43
|
+
background: transparent;
|
|
44
|
+
color: var(--muted);
|
|
45
|
+
border: none;
|
|
46
|
+
padding: 10px 32px;
|
|
47
|
+
font-size: 16px;
|
|
48
|
+
font-weight: 600;
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
border-bottom: 3px solid transparent;
|
|
51
|
+
transition: all 0.2s;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.tab-btn:hover {
|
|
55
|
+
color: #e6eef8;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.tab-btn.active {
|
|
59
|
+
color: var(--accent);
|
|
60
|
+
border-bottom-color: var(--accent);
|
|
61
|
+
}
|
|
62
|
+
|
|
35
63
|
.brand {
|
|
36
64
|
font-weight: 700;
|
|
37
65
|
font-size: 18px;
|
|
@@ -41,6 +69,11 @@ html, body {
|
|
|
41
69
|
display: flex;
|
|
42
70
|
gap: 8px;
|
|
43
71
|
align-items: center;
|
|
72
|
+
min-width: 300px; /* Maintain space even when hidden */
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.controls.invisible {
|
|
76
|
+
visibility: hidden;
|
|
44
77
|
}
|
|
45
78
|
|
|
46
79
|
.controls select, .controls button {
|
|
@@ -472,3 +505,42 @@ body.unified-mode footer {
|
|
|
472
505
|
body.unified-mode #unified-panel {
|
|
473
506
|
flex: 1;
|
|
474
507
|
}
|
|
508
|
+
|
|
509
|
+
/* Tab view controls */
|
|
510
|
+
body.kuma-view .meta-row,
|
|
511
|
+
body.kuma-view .charts-row,
|
|
512
|
+
body.kuma-view .details-row,
|
|
513
|
+
body.kuma-view .tables-row {
|
|
514
|
+
display: none !important;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
body.kuma-view #kuma-content {
|
|
518
|
+
display: block !important;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
body.nodes-view #kuma-content {
|
|
522
|
+
display: none !important;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
#kuma-content {
|
|
526
|
+
display: none;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
#kuma-content .panel {
|
|
530
|
+
max-width: 1400px;
|
|
531
|
+
margin: 0 auto;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.kuma-search {
|
|
535
|
+
margin-bottom: 12px;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.kuma-search input {
|
|
539
|
+
width: 100%;
|
|
540
|
+
padding: 10px 16px;
|
|
541
|
+
background: var(--glass);
|
|
542
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
543
|
+
color: inherit;
|
|
544
|
+
border-radius: 6px;
|
|
545
|
+
font-size: 14px;
|
|
546
|
+
}
|
|
@@ -19,8 +19,15 @@
|
|
|
19
19
|
<header class="topbar">
|
|
20
20
|
<div class="brand">Node Monitor</div>
|
|
21
21
|
|
|
22
|
+
{% if kuma_data is not none %}
|
|
23
|
+
<div class="tab-navigation">
|
|
24
|
+
<button id="nodes-tab" class="tab-btn active">Nodes</button>
|
|
25
|
+
<button id="kuma-tab" class="tab-btn">Uptime Kuma</button>
|
|
26
|
+
</div>
|
|
27
|
+
{% endif %}
|
|
28
|
+
|
|
22
29
|
<div class="controls">
|
|
23
|
-
<label for="node-select">Node:</label>
|
|
30
|
+
<label for="node-select" id="node-select-label">Node:</label>
|
|
24
31
|
<select id="node-select" aria-label="Select node"></select>
|
|
25
32
|
<button id="refresh-btn" title="Force refresh">Refresh</button>
|
|
26
33
|
<button id="logout" title="Logout" onclick="logout()">Logout</button>
|
|
@@ -98,7 +105,6 @@
|
|
|
98
105
|
<canvas id="unified-disk-chart" class="chart"></canvas>
|
|
99
106
|
</div>
|
|
100
107
|
</div>
|
|
101
|
-
{# TODO: Make the following tables into multiple div containers #}
|
|
102
108
|
<div id="unified-legend" class="unified-legend"></div>
|
|
103
109
|
<div class="unified-tables">
|
|
104
110
|
<div class="unified-tables-grid">
|
|
@@ -250,10 +256,32 @@
|
|
|
250
256
|
</table>
|
|
251
257
|
</div>
|
|
252
258
|
</div>
|
|
259
|
+
|
|
260
|
+
<div class="panel">
|
|
261
|
+
<div class="panel-header"><h3>Kuma Endpoints</h3></div>
|
|
262
|
+
<div class="panel-body">
|
|
263
|
+
<table class="table" id="endpoints-table">
|
|
264
|
+
<thead></thead>
|
|
265
|
+
<tbody></tbody>
|
|
266
|
+
</table>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
253
269
|
</section>
|
|
254
270
|
</section>
|
|
255
271
|
</main>
|
|
256
272
|
|
|
273
|
+
<div id="kuma-content" class="container">
|
|
274
|
+
<section class="panel">
|
|
275
|
+
<div class="kuma-search">
|
|
276
|
+
<input type="text" id="kuma-search-input" placeholder="Search Kuma endpoints by node, name, parent, or tags...">
|
|
277
|
+
</div>
|
|
278
|
+
<table class="table" id="kuma-main-table">
|
|
279
|
+
<thead></thead>
|
|
280
|
+
<tbody></tbody>
|
|
281
|
+
</table>
|
|
282
|
+
</section>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
257
285
|
<footer class="footer">
|
|
258
286
|
<div><code>PyObservability: v{{ version }}</code><br>
|
|
259
287
|
<a href="https://github.com/thevickypedia/PyObservability" target="_blank">https://github.com/thevickypedia/PyObservability</a>
|
|
@@ -264,6 +292,13 @@
|
|
|
264
292
|
// inject server-side targets
|
|
265
293
|
window.MONITOR_TARGETS = {{ targets | tojson }};
|
|
266
294
|
</script>
|
|
295
|
+
|
|
296
|
+
{% if kuma_data is not none %}
|
|
297
|
+
<script>
|
|
298
|
+
window.KUMA_DATA = {{ kuma_data | tojson }};
|
|
299
|
+
</script>
|
|
300
|
+
{% endif %}
|
|
301
|
+
|
|
267
302
|
<script>
|
|
268
303
|
const logoutEndpoint = "{{ logout }}";
|
|
269
304
|
|
pyobservability/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.
|
|
1
|
+
__version__ = "2.1.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyObservability
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 2.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
|
|
@@ -42,13 +42,16 @@ Requires-Python: >=3.11
|
|
|
42
42
|
Description-Content-Type: text/markdown
|
|
43
43
|
License-File: LICENSE
|
|
44
44
|
Requires-Dist: aiohttp==3.13.*
|
|
45
|
-
Requires-Dist: fastapi==0.
|
|
46
|
-
Requires-Dist: FastAPI-UI-Auth==0.2
|
|
45
|
+
Requires-Dist: fastapi==0.128.*
|
|
46
|
+
Requires-Dist: FastAPI-UI-Auth==0.2.*
|
|
47
47
|
Requires-Dist: Jinja2==3.1.*
|
|
48
48
|
Requires-Dist: pydantic==2.12.*
|
|
49
49
|
Requires-Dist: pydantic-settings==2.12.*
|
|
50
50
|
Requires-Dist: python-dotenv==1.2.*
|
|
51
|
-
Requires-Dist:
|
|
51
|
+
Requires-Dist: python-socketio==5.16.*
|
|
52
|
+
Requires-Dist: requests==2.32.*
|
|
53
|
+
Requires-Dist: uvicorn[standard]==0.40.*
|
|
54
|
+
Requires-Dist: websocket-client==1.9.*
|
|
52
55
|
Provides-Extra: dev
|
|
53
56
|
Requires-Dist: pre-commit; extra == "dev"
|
|
54
57
|
Dynamic: license-file
|
|
@@ -140,6 +143,13 @@ docker run \
|
|
|
140
143
|
- **LOGS_PATH** - Directory path to store log files if `LOG` is set to `file`.
|
|
141
144
|
- **LOG_CONFIG** - Path to a custom logging configuration file.
|
|
142
145
|
|
|
146
|
+
**Uptime Kuma**
|
|
147
|
+
> Uptime Kuma integration can be enabled by setting the following environment variables.
|
|
148
|
+
- **KUMA_URL** - Base URL of the Uptime Kuma server.
|
|
149
|
+
- **KUMA_USERNAME** - Username to authenticate with Uptime Kuma.
|
|
150
|
+
- **KUMA_PASSWORD** - Password to authenticate with Uptime Kuma.
|
|
151
|
+
- **KUMA_TIMEOUT** - Timeout (in seconds) for Uptime Kuma authentication. Defaults to 5s.
|
|
152
|
+
|
|
143
153
|
## License & copyright
|
|
144
154
|
|
|
145
155
|
© Vignesh Rao
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
pyobservability/__init__.py,sha256=yVBLyTohBiBKp0Otyl04IggPh8mhg3Er25u6eFyxMto,2618
|
|
2
|
+
pyobservability/kuma.py,sha256=4sB1M7-exzrObGGIh1yYQ22P40kHwsTfv4G1Xj9YYWw,3260
|
|
3
|
+
pyobservability/main.py,sha256=c2sA-gGuAS9flO7FgeyQyzTMSHmZqhaeQAm1mWCUi9E,5992
|
|
4
|
+
pyobservability/monitor.py,sha256=i_Xf_DB-qLOp1b9wryekjwHIM8AnMrGTkuEg7e08bcM,7539
|
|
5
|
+
pyobservability/transport.py,sha256=S-84mgf-9yMj0H7VSAmueW9yosX_1XxdyNJC2EuQHQQ,8493
|
|
6
|
+
pyobservability/version.py,sha256=Xybt2skBZamGMNlLuOX1IG-h4uIxqUDGAO8MIGWrJac,22
|
|
7
|
+
pyobservability/config/enums.py,sha256=rQZh2Q4-9ItQTQgxsYjqw-jNePpWHhvQUrbrQJBG5CI,313
|
|
8
|
+
pyobservability/config/settings.py,sha256=aCz1tZEg8fUA5gHCCDN_m3fgrH1axz2fvdalj4bszGs,6121
|
|
9
|
+
pyobservability/static/app.js,sha256=XTy4lE2WxYDnJCjaigAOXde8-wZ36Wt_580HWlE3JFo,48557
|
|
10
|
+
pyobservability/static/styles.css,sha256=P6Xg-IAXO3WNeBLGH9Q5HAdNeDMRbFcM5P_cq60Jf00,9405
|
|
11
|
+
pyobservability/templates/index.html,sha256=aqvHrxP_D2gSxYmsUkNMi1QH07pQNXh4VuXc51KBo5c,11826
|
|
12
|
+
pyobservability-2.1.0.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
|
|
13
|
+
pyobservability-2.1.0.dist-info/METADATA,sha256=aUlk5iphyiJ9WYIUXQQVrzWBvgFrp2Q1OnSYvv7Xsug,7026
|
|
14
|
+
pyobservability-2.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
15
|
+
pyobservability-2.1.0.dist-info/entry_points.txt,sha256=DSGIr_VA8Tb3FYa2iNUYpf55eAvuFCAoInNS4ngXaME,57
|
|
16
|
+
pyobservability-2.1.0.dist-info/top_level.txt,sha256=p20T0EmihDYW1uMintRXr7X9bg3XWYKyoSbBHOVC1xI,16
|
|
17
|
+
pyobservability-2.1.0.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
pyobservability/__init__.py,sha256=yVBLyTohBiBKp0Otyl04IggPh8mhg3Er25u6eFyxMto,2618
|
|
2
|
-
pyobservability/main.py,sha256=EJ49ENDnKy07jKHQ0IYEVmsyc_4O3uxCHpn1faQD5VM,4534
|
|
3
|
-
pyobservability/monitor.py,sha256=i_Xf_DB-qLOp1b9wryekjwHIM8AnMrGTkuEg7e08bcM,7539
|
|
4
|
-
pyobservability/transport.py,sha256=S-84mgf-9yMj0H7VSAmueW9yosX_1XxdyNJC2EuQHQQ,8493
|
|
5
|
-
pyobservability/version.py,sha256=BqOI5y46o1G1RWC9bF1DPL-YM68lGYPmZt1pn6FZFZs,22
|
|
6
|
-
pyobservability/config/enums.py,sha256=EhvD9kB5EMW3ARxr5KmISmf-rP3D4IKqOIjw6Tb8SB8,294
|
|
7
|
-
pyobservability/config/settings.py,sha256=ylhiT0SARHuzkyon_1otgsO74AfA6aiUKp5uczZQj08,5980
|
|
8
|
-
pyobservability/static/app.js,sha256=l6m8mwEU4z5d2wQF7Fa6tFITW3HX3M5RaLhBJ0jFXrM,43322
|
|
9
|
-
pyobservability/static/styles.css,sha256=DRJ4kw-LDlXMcQXxFd8cEDuDC_ZfwZgARAjn0zDWwRk,8172
|
|
10
|
-
pyobservability/templates/index.html,sha256=Z_r1Gq0QNxEwTL4_2NPQ1cKqLpbQoJC88E5leyjo07s,10786
|
|
11
|
-
pyobservability-1.4.1.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
|
|
12
|
-
pyobservability-1.4.1.dist-info/METADATA,sha256=QUIDftx5D229DYD03NsspRYP2Imp0u_rVVlzW4-UPHo,6537
|
|
13
|
-
pyobservability-1.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
-
pyobservability-1.4.1.dist-info/entry_points.txt,sha256=DSGIr_VA8Tb3FYa2iNUYpf55eAvuFCAoInNS4ngXaME,57
|
|
15
|
-
pyobservability-1.4.1.dist-info/top_level.txt,sha256=p20T0EmihDYW1uMintRXr7X9bg3XWYKyoSbBHOVC1xI,16
|
|
16
|
-
pyobservability-1.4.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|