feat(dashboard): add cached cpu/mem stats fields to containers-dashboard
This commit is contained in:
@@ -17,6 +17,85 @@ PODMAN_API_BASE = "http+unix://%2Frun%2Fuser%2F1000%2Fpodman%2Fpodman.sock/v5.4.
|
|||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
ALLOWLIST_FILE = os.getenv("ALLOWLIST_FILE", os.path.join(BASE_DIR, "allowed_units.txt"))
|
ALLOWLIST_FILE = os.getenv("ALLOWLIST_FILE", os.path.join(BASE_DIR, "allowed_units.txt"))
|
||||||
WORKLOADS_DIR = "/app/workloads"
|
WORKLOADS_DIR = "/app/workloads"
|
||||||
|
|
||||||
|
# --- STATS CACHE (contract-neutral; in-memory) ---
|
||||||
|
# Poll Podman stats centrally and expose as optional dashboard fields.
|
||||||
|
_STATS_CACHE_BY_NAME = {} # name -> {"cpu": float|None, "mem_usage": float|None, "mem_perc": float|None}
|
||||||
|
_STATS_CACHE_TS = None
|
||||||
|
_STATS_POLLER_TASK = None
|
||||||
|
|
||||||
|
def _norm_container_name(name) -> str:
|
||||||
|
try:
|
||||||
|
return str(name or "").lstrip("/")
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _parse_stats_interval_seconds() -> float:
|
||||||
|
raw = os.getenv("STATS_INTERVAL_SECONDS", "1.0")
|
||||||
|
try:
|
||||||
|
v = float(raw)
|
||||||
|
except Exception:
|
||||||
|
v = 1.0
|
||||||
|
if v <= 0:
|
||||||
|
v = 1.0
|
||||||
|
if v < 0.5:
|
||||||
|
v = 0.5
|
||||||
|
if v > 30:
|
||||||
|
v = 30
|
||||||
|
return v
|
||||||
|
|
||||||
|
async def _stats_poller_loop():
|
||||||
|
global _STATS_CACHE_BY_NAME, _STATS_CACHE_TS
|
||||||
|
|
||||||
|
interval = _parse_stats_interval_seconds()
|
||||||
|
stats_url = f"{PODMAN_API_BASE}/libpod/containers/stats?all=true&stream=false"
|
||||||
|
|
||||||
|
def _to_float(x):
|
||||||
|
try:
|
||||||
|
return float(x)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
data = SESSION.get(stats_url, timeout=5).json()
|
||||||
|
stats_list = data.get("Stats") if isinstance(data, dict) else None
|
||||||
|
if not isinstance(stats_list, list):
|
||||||
|
stats_list = []
|
||||||
|
|
||||||
|
new_cache = {}
|
||||||
|
for st in stats_list:
|
||||||
|
if not isinstance(st, dict):
|
||||||
|
continue
|
||||||
|
key = _norm_container_name(st.get("Name"))
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
cpu_val = st.get("CPUPerc")
|
||||||
|
if cpu_val is None:
|
||||||
|
cpu_val = st.get("CPU")
|
||||||
|
if cpu_val is None:
|
||||||
|
cpu_val = st.get("AvgCPU")
|
||||||
|
new_cache[key] = {
|
||||||
|
"cpu": _to_float(cpu_val),
|
||||||
|
"mem_usage": _to_float(st.get("MemUsage")),
|
||||||
|
"mem_perc": _to_float(st.get("MemPerc")),
|
||||||
|
}
|
||||||
|
|
||||||
|
_STATS_CACHE_BY_NAME = new_cache
|
||||||
|
_STATS_CACHE_TS = int(__import__("time").time())
|
||||||
|
except Exception:
|
||||||
|
# Keep last good cache; try again next tick.
|
||||||
|
pass
|
||||||
|
|
||||||
|
await asyncio.sleep(interval)
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def _startup_stats_poller():
|
||||||
|
global _STATS_POLLER_TASK
|
||||||
|
if _STATS_POLLER_TASK and not _STATS_POLLER_TASK.done():
|
||||||
|
return
|
||||||
|
_STATS_POLLER_TASK = asyncio.create_task(_stats_poller_loop())
|
||||||
|
|
||||||
# --- ROUTERS ---
|
# --- ROUTERS ---
|
||||||
# Images API lives in a dedicated module to keep this file from growing further.
|
# Images API lives in a dedicated module to keep this file from growing further.
|
||||||
app.include_router(init_images_router(SESSION, PODMAN_API_BASE))
|
app.include_router(init_images_router(SESSION, PODMAN_API_BASE))
|
||||||
@@ -384,6 +463,9 @@ def _make_defined_container_dashboard_row(name: str, relpath: str):
|
|||||||
"_dashboard_source": "systemd",
|
"_dashboard_source": "systemd",
|
||||||
"_dashboard_unit": f"{name}.service",
|
"_dashboard_unit": f"{name}.service",
|
||||||
"_dashboard_def_path": relpath,
|
"_dashboard_def_path": relpath,
|
||||||
|
"_dashboard_cpu": None,
|
||||||
|
"_dashboard_mem_usage": None,
|
||||||
|
"_dashboard_mem_perc": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -507,6 +589,8 @@ def containers_dashboard():
|
|||||||
# Cache zodat we niet voor elke container opnieuw systemctl doen
|
# Cache zodat we niet voor elke container opnieuw systemctl doen
|
||||||
unit_active_cache = {}
|
unit_active_cache = {}
|
||||||
|
|
||||||
|
stats_by_name = _STATS_CACHE_BY_NAME
|
||||||
|
|
||||||
def _unit_is_active(unit):
|
def _unit_is_active(unit):
|
||||||
if not unit:
|
if not unit:
|
||||||
return False
|
return False
|
||||||
@@ -528,6 +612,16 @@ def containers_dashboard():
|
|||||||
# Normaliseer naam: Podman kan "/name" geven
|
# Normaliseer naam: Podman kan "/name" geven
|
||||||
rname = ((c.get("Names") or ["?"])[0] or "").lstrip("/")
|
rname = ((c.get("Names") or ["?"])[0] or "").lstrip("/")
|
||||||
|
|
||||||
|
# Optional live stats (always present; null on miss)
|
||||||
|
c["_dashboard_cpu"] = None
|
||||||
|
c["_dashboard_mem_usage"] = None
|
||||||
|
c["_dashboard_mem_perc"] = None
|
||||||
|
st = stats_by_name.get(rname)
|
||||||
|
if isinstance(st, dict):
|
||||||
|
c["_dashboard_cpu"] = st.get("cpu")
|
||||||
|
c["_dashboard_mem_usage"] = st.get("mem_usage")
|
||||||
|
c["_dashboard_mem_perc"] = st.get("mem_perc")
|
||||||
|
|
||||||
# 1) Managed: systemd als er een .container definitie bestaat
|
# 1) Managed: systemd als er een .container definitie bestaat
|
||||||
if rname in defined:
|
if rname in defined:
|
||||||
c["_dashboard_source"] = "systemd"
|
c["_dashboard_source"] = "systemd"
|
||||||
|
|||||||
Reference in New Issue
Block a user