agent v1.4: MySQL/MariaDB live metrics (_mysql_stats)

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.
This commit is contained in:
2026-08-14 01:09:33 +02:00
parent 7a39ac494f
commit 4fb1aba5c6
+43 -2
View File
@@ -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):