From 66bb1d686e69a845b768d6811119d77a6d1d6426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C3=B1igo=20Etxaniz?= Date: Sat, 7 Nov 2026 08:13:05 +0000 Subject: [PATCH] initial version --- README.md | 8 +++- main.py | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++ schedule.yaml | 12 ++++++ signals.yml | 4 ++ 4 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 main.py create mode 100644 schedule.yaml create mode 100644 signals.yml diff --git a/README.md b/README.md index 35d670e..84844ae 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ -# host_session +# host-session -host session table relateed scripts \ No newline at end of file +Stores one row per finished session of the host in the custom table `host_session` (schema 0.0.1): start and end time, duration, mean and max of CPU, RAM and disk percentage, and coverage (share of the session's minutes with a CPU sample). The hub adds the gateway as `spoke_id`. + +Runs every minute. The current boot comes from `/proc/uptime`. The previous session is found in the history of the uptime signal (63) before that boot: its last sample is the end, end minus that sample's uptime is the start. A session already stored (start within two minutes) is skipped, so repeated runs write nothing twice. The first row appears after the first reboot that follows the uptime signal's creation (2026-11-06 18:38). + +Signal ids are deb13-kde's in the home core. Needs only `requests` in the image. diff --git a/main.py b/main.py new file mode 100644 index 0000000..d243ddc --- /dev/null +++ b/main.py @@ -0,0 +1,103 @@ +import os +import time + +import requests + +SIDECAR_URL = os.getenv("SIDECAR_URL", "http://sidecar:80") +DB_URL = os.getenv("DB_CONTROLLER_URL", "http://sidecar:8081") +TABLE = "host_session" +UPTIME, CPU, RAM, DISK = 63, 21, 22, 23 +MINUTE_MS = 60000 +HOUR_MS = 60 * MINUTE_MS +LOOKBACK_MS = 60 * 24 * HOUR_MS +MATCH_MS = 2 * MINUTE_MS + + +def read_history(signals, start_ms, end_ms, resolution): + names = ",".join(str(signal) for signal in signals) + path = f"/api/history/?signals={names}&start={int(start_ms)}&end={int(end_ms)}&resolution={resolution}" + response = requests.get(SIDECAR_URL + path, timeout=(5, 80)) + response.raise_for_status() + return {item["signalId"]: item for item in response.json()["data"]["series"]} + + +def samples(series): + start, step = series["startMs"], series["stepMs"] + return [(start + i * step, value) for i, value in enumerate(series["values"]) if value is not None] + + +def last_samples(signal, start_ms, end_ms, resolution): + if end_ms <= start_ms: + return [] + found = samples(read_history([signal], start_ms, end_ms, resolution)[signal]) + return [(t, v) for t, v in found if t < end_ms] + + +def current_boot_ms(): + with open("/proc/uptime") as file: + uptime_s = float(file.read().split()[0]) + return int(time.time() * 1000 - uptime_s * 1000) + + +def previous_session(boot_ms): + hours = last_samples(UPTIME, boot_ms - LOOKBACK_MS, boot_ms, "1hr") + if not hours: + return None + minutes = last_samples(UPTIME, hours[-1][0], min(hours[-1][0] + HOUR_MS, boot_ms), "1min") + if not minutes: + return None + seconds = last_samples(UPTIME, minutes[-1][0], min(minutes[-1][0] + MINUTE_MS, boot_ms), "1s") + end_ms, uptime_s = (seconds or minutes)[-1] + return int(end_ms - uptime_s * 1000), int(end_ms) + + +def db_call(method, path, payload): + response = requests.request(method, DB_URL + path, json=payload, timeout=(5, 30)) + response.raise_for_status() + body = response.json() + if body.get("statusCode", 200) != 200: + raise RuntimeError(f"db-controller {body.get('statusCode')}: {body.get('errorMessage')}") + return body.get("data") + + +def already_stored(start_ms): + rows = db_call("POST", f"/api/db-query/{TABLE}/", {"filters": [], "orderBy": ["start_time"], "limit": 1000}) or [] + return any(abs(row["start_time"] - start_ms) < MATCH_MS for row in rows) + + +def mean_max(series, start_ms, end_ms): + values = [v for t, v in samples(series) if start_ms <= t < end_ms] + if not values: + return None, None + return sum(values) / len(values), max(values) + + +def coverage(series, start_ms, end_ms): + total = max(1, (end_ms - start_ms) // MINUTE_MS) + present = sum(1 for t, _ in samples(series) if start_ms <= t < end_ms) + return min(1.0, present / total) + + +def build_row(start_ms, end_ms): + history = read_history([CPU, RAM, DISK], start_ms, end_ms, "1min") + row = {"start_time": start_ms, "end_time": end_ms, "duration_s": (end_ms - start_ms) // 1000} + for prefix, signal in (("cpu", CPU), ("ram", RAM), ("disk", DISK)): + row[f"{prefix}_mean"], row[f"{prefix}_max"] = mean_max(history[signal], start_ms, end_ms) + row["coverage"] = coverage(history[CPU], start_ms, end_ms) + return row + + +def main(): + session = previous_session(current_boot_ms()) + if session is None: + print("no previous session in the uptime history", flush=True) + return + if already_stored(session[0]): + print("previous session already stored", session[0], flush=True) + return + row = build_row(*session) + print("stored", db_call("POST", f"/api/db/{TABLE}/", row), row, flush=True) + + +if __name__ == "__main__": + main() diff --git a/schedule.yaml b/schedule.yaml new file mode 100644 index 0000000..bbd6781 --- /dev/null +++ b/schedule.yaml @@ -0,0 +1,12 @@ +name: host-session +active: true +schedule: "* * * * *" +action: run +runOnStartup: true +timeoutSeconds: 120 +imageName: docker.izazpi.cloud/infra/infra-python:0.1.12 +command: ["python", "-u", "/app/main.py"] +maxCpuCores: 0.25 +maxMemoryMb: 128 +autoremove: false +logOption: 1 diff --git a/signals.yml b/signals.yml new file mode 100644 index 0000000..34d287c --- /dev/null +++ b/signals.yml @@ -0,0 +1,4 @@ +- { id: 21, name: 'deb13kde-cpu', read: 1, write: 0 } +- { id: 22, name: 'deb13kde-ram', read: 1, write: 0 } +- { id: 23, name: 'deb13kde-disk', read: 1, write: 0 } +- { id: 63, name: 'deb13kde-uptime', read: 1, write: 0 }