feat: grouped server cards with progress bars and sparklines
- Agent sends group name (defaults to hostname) and richer metric notes (memory/disk include absolute GB values; CPU note includes system uptime) - push() accepts group param; server stores it as agent_group on deployments - Upsert key is now (company_id, agent_group, name) so two agents on the same company but different servers don't overwrite each other's CPU/Memory - Add widget config packs: linux-base, web-nginx, database-postgres/redis, docker-host, custom-cmd examples
This commit is contained in:
+43
-10
@@ -211,19 +211,48 @@ def run_widget(w: dict) -> tuple:
|
||||
|
||||
if wtype == "system_cpu":
|
||||
pct = cpu_percent()
|
||||
uptime_note = ""
|
||||
if SYSTEM == "Linux":
|
||||
try:
|
||||
with open("/proc/uptime") as f:
|
||||
secs = float(f.readline().split()[0])
|
||||
days = int(secs // 86400)
|
||||
hours = int((secs % 86400) // 3600)
|
||||
uptime_note = f" up {days}d{hours}h"
|
||||
except Exception:
|
||||
pass
|
||||
status = _pct_status(pct, w.get("degrade_threshold", 80), w.get("offline_threshold", 95))
|
||||
return name, kind, status, None, "", f"cpu {pct}%"
|
||||
return name, kind, status, None, "", f"cpu {pct}%{uptime_note}"
|
||||
|
||||
if wtype == "system_memory":
|
||||
pct = mem_percent()
|
||||
abs_note = ""
|
||||
if SYSTEM == "Linux":
|
||||
try:
|
||||
info: dict = {}
|
||||
with open("/proc/meminfo") as f:
|
||||
for line in f:
|
||||
k, _, v = line.partition(":")
|
||||
info[k.strip()] = int(v.split()[0])
|
||||
total_kb = info.get("MemTotal", 0)
|
||||
avail_kb = info.get("MemAvailable", 0)
|
||||
used_gb = round((total_kb - avail_kb) / 1048576, 0)
|
||||
total_gb = round(total_kb / 1048576, 0)
|
||||
abs_note = f" {int(used_gb)}/{int(total_gb)}GB"
|
||||
except Exception:
|
||||
pass
|
||||
status = _pct_status(pct, w.get("degrade_threshold", 85), w.get("offline_threshold", 97))
|
||||
return name, kind, status, None, "", f"mem {pct}%"
|
||||
return name, kind, status, None, "", f"mem {pct}%{abs_note}"
|
||||
|
||||
if wtype == "system_disk":
|
||||
path = w.get("path", "/")
|
||||
pct = disk_percent(path)
|
||||
u = shutil.disk_usage(path)
|
||||
pct = round(100.0 * u.used / u.total, 1)
|
||||
total_gb = round(u.total / 1e9, 0)
|
||||
used_gb = round(u.used / 1e9, 0)
|
||||
abs_note = f" {int(used_gb)}/{int(total_gb)}GB"
|
||||
status = _pct_status(pct, w.get("degrade_threshold", 80), w.get("offline_threshold", 95))
|
||||
return name, "system", status, None, "", f"disk {pct}% ({path})"
|
||||
return name, "system", status, None, "", f"disk {pct}%{abs_note} ({path})"
|
||||
|
||||
if wtype == "http_check":
|
||||
url = w["url"]
|
||||
@@ -276,12 +305,14 @@ def run_widget(w: dict) -> tuple:
|
||||
# ── Push ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
def push(home: str, token: str, name: str, kind: str, status: str,
|
||||
latency_ms, version: str, note: str) -> int:
|
||||
latency_ms, version: str, note: str, group: str = "") -> int:
|
||||
url = home.rstrip("/") + "/api/company/metrics"
|
||||
payload: dict = {"name": name, "kind": kind, "status": status,
|
||||
"version": version, "note": note}
|
||||
if latency_ms is not None:
|
||||
payload["latency_ms"] = latency_ms
|
||||
if group:
|
||||
payload["group"] = group
|
||||
data = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(
|
||||
url, data=data, method="POST",
|
||||
@@ -313,10 +344,12 @@ def main():
|
||||
# ETHICA_TOKEN : company API token (required)
|
||||
# ETHICA_HOME : home URL (optional, default https://ethica.no)
|
||||
# ETHICA_INTERVAL: collection interval in seconds (optional)
|
||||
token = os.environ.get("ETHICA_TOKEN") or cfg.get("token", "")
|
||||
home = os.environ.get("ETHICA_HOME") or cfg.get("home", "https://ethica.no")
|
||||
interval = int(os.environ.get("ETHICA_INTERVAL") or cfg.get("interval", 60))
|
||||
widgets = cfg.get("widgets", [])
|
||||
# ETHICA_GROUP : server/group label shown in the dashboard card (default: hostname)
|
||||
token = os.environ.get("ETHICA_TOKEN") or cfg.get("token", "")
|
||||
home = os.environ.get("ETHICA_HOME") or cfg.get("home", "https://ethica.no")
|
||||
interval = int(os.environ.get("ETHICA_INTERVAL") or cfg.get("interval", 60))
|
||||
agent_group = os.environ.get("ETHICA_GROUP") or cfg.get("group", platform.node())
|
||||
widgets = cfg.get("widgets", [])
|
||||
|
||||
if not token or token == "REPLACE_ME":
|
||||
print("Set ETHICA_TOKEN env var or 'token' in config "
|
||||
@@ -334,7 +367,7 @@ def main():
|
||||
label = w.get("name", w.get("type", "?"))
|
||||
try:
|
||||
name, kind, status, lat, ver, note = run_widget(w)
|
||||
code = push(home, token, name, kind, status, lat, ver, note)
|
||||
code = push(home, token, name, kind, status, lat, ver, note, agent_group)
|
||||
print(f" [{status:<9}] {name} → {code}")
|
||||
except NotImplementedError as e:
|
||||
print(f" [SKIP ] {label}: {e}", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user