From 4fb1aba5c6d522670fa693256f883c426a7c3707 Mon Sep 17 00:00:00 2001 From: steffen Date: Fri, 14 Aug 2026 01:09:33 +0200 Subject: [PATCH] agent v1.4: MySQL/MariaDB live metrics (_mysql_stats) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same fields as the postgres variant: sizes/tables/rows/top tables from information_schema.TABLES, connections/uptime from SHOW GLOBAL STATUS, InnoDB buffer-pool hit ratio as cache_hit_pct. Best-effort like the rest — any failure returns None and never disturbs the heartbeat. --- backhaul-agent.py | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/backhaul-agent.py b/backhaul-agent.py index 07f69e8..0f45a9b 100644 --- a/backhaul-agent.py +++ b/backhaul-agent.py @@ -22,7 +22,7 @@ import subprocess import sys import urllib.request -VERSION = "1.3" +VERSION = "1.4" RESULT_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".dump-result.json") @@ -81,9 +81,11 @@ def db_version(t): def db_stats(t): """Read-only live metrics for the dashboard's 'nerd stats' — size, tables, rows, index footprint, connections, cache-hit ratio, the biggest tables, server version + uptime. - Best-effort: any failure returns None and never disturbs the heartbeat. Postgres only.""" + Best-effort: any failure returns None and never disturbs the heartbeat.""" if t["engine"] == "sqlite": return _sqlite_stats(t) + if t["engine"] in ("mysql", "mariadb"): + return _mysql_stats(t) if t["engine"] != "postgres": return None env = dict(os.environ, PGPASSWORD=t["pw"]) @@ -120,6 +122,45 @@ def db_stats(t): return None +def _mysql_stats(t): + """Live metrics for MySQL/MariaDB, same fields as the postgres variant. Sizes and row + counts come from information_schema.TABLES (InnoDB estimates — same spirit as + n_live_tup); cache hit is the InnoDB buffer pool hit ratio.""" + def q(sql): + r = subprocess.run(("mysql", "-h", t["host"], "-P", t["port"], "-u", t["user"], + f"-p{t['pw']}", "-N", "-B", t["db"], "-e", sql), + capture_output=True, text=True, timeout=30) + return r.stdout.strip() + + def status(var): + out = q(f"SHOW GLOBAL STATUS LIKE '{var}'") + return int(out.split("\t")[1]) if "\t" in out else 0 + + try: + size, idx, tables, rows = (int(float(v or 0)) for v in q( + "SELECT COALESCE(SUM(data_length+index_length),0)," + " COALESCE(SUM(index_length),0), COUNT(*), COALESCE(SUM(table_rows),0)" + " FROM information_schema.TABLES" + " WHERE table_schema=DATABASE() AND table_type='BASE TABLE'").split("\t")) + top = [{"name": f[0], "bytes": int(f[1]), "rows": int(f[2])} + for f in (line.split("\t") for line in q( + "SELECT table_name, data_length+index_length, COALESCE(table_rows,0)" + " FROM information_schema.TABLES" + " WHERE table_schema=DATABASE() AND table_type='BASE TABLE'" + " ORDER BY data_length+index_length DESC LIMIT 10").splitlines()) + if len(f) == 3] + req = status("Innodb_buffer_pool_read_requests") + miss = status("Innodb_buffer_pool_reads") + return {"size_bytes": size, "tables": tables, "rows_est": rows, + "index_bytes": idx, "connections": status("Threads_connected"), + "cache_hit_pct": round(100.0 * (req - miss) / req, 2) if req else None, + "server_version": q("SELECT VERSION()"), + "uptime_s": status("Uptime") or None, + "top_tables": top} + except Exception: + return None + + def _sqlite_stats(t): """Live metrics for a SQLite file: file size, table count, row totals, largest tables.""" def q(sql):