116 lines
3.6 KiB
Python
116 lines
3.6 KiB
Python
import os
|
|
import json
|
|
import subprocess
|
|
from datetime import datetime
|
|
from flask import Flask, jsonify, render_template
|
|
|
|
app = Flask(__name__)
|
|
|
|
POLL_SECONDS = int(os.getenv("POLL_SECONDS", "10"))
|
|
SHOW_CONTAINERS = os.getenv("SHOW_CONTAINERS", "0") == "1"
|
|
|
|
def sh(cmd: list[str]) -> str:
|
|
return subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True).strip()
|
|
|
|
def dokku_apps() -> list[str]:
|
|
out = sh(["dokku", "apps:list"])
|
|
lines = [l.strip() for l in out.splitlines()]
|
|
# Format:
|
|
# =====> My Apps
|
|
# app1
|
|
# app2
|
|
return [l for l in lines if l and not l.startswith("====")]
|
|
|
|
def dokku_urls(app_name: str) -> list[str]:
|
|
try:
|
|
out = sh(["dokku", "urls:report", app_name, "--urls"])
|
|
return [u.strip() for u in out.split() if u.strip()]
|
|
except Exception:
|
|
return []
|
|
|
|
def dokku_ps_report(app_name: str) -> dict:
|
|
try:
|
|
out = sh(["dokku", "ps:report", app_name])
|
|
# crude parse: "Key: value"
|
|
data = {}
|
|
for line in out.splitlines():
|
|
if ":" in line:
|
|
k, v = line.split(":", 1)
|
|
data[k.strip()] = v.strip()
|
|
return data
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
def docker_stats() -> dict:
|
|
"""
|
|
Returns a mapping of container_name -> {cpu, mem_used, mem_limit, mem_pct}
|
|
"""
|
|
fmt = "{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"
|
|
out = sh(["docker", "stats", "--no-stream", "--format", fmt])
|
|
stats = {}
|
|
for line in out.splitlines():
|
|
name, cpu, mem_usage, mem_pct = line.split("\t")
|
|
# mem_usage like: "58.84MiB / 384MiB"
|
|
mem_used, mem_limit = [s.strip() for s in mem_usage.split("/", 1)]
|
|
stats[name] = {
|
|
"cpu": cpu,
|
|
"mem_used": mem_used,
|
|
"mem_limit": mem_limit,
|
|
"mem_pct": mem_pct,
|
|
}
|
|
return stats
|
|
|
|
def docker_ps() -> list[dict]:
|
|
fmt = "{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"
|
|
out = sh(["docker", "ps", "--format", fmt])
|
|
rows = []
|
|
for line in out.splitlines():
|
|
name, image, status, ports = line.split("\t")
|
|
rows.append({"name": name, "image": image, "status": status, "ports": ports})
|
|
return rows
|
|
|
|
def collect():
|
|
apps = dokku_apps()
|
|
stats = docker_stats()
|
|
|
|
rows = []
|
|
for a in apps:
|
|
urls = dokku_urls(a)
|
|
ps = dokku_ps_report(a)
|
|
# Your containers are typically named like "<app>.web.1"
|
|
container_name = f"{a}.web.1"
|
|
cstats = stats.get(container_name, None)
|
|
|
|
rows.append({
|
|
"app": a,
|
|
"urls": urls,
|
|
"running": ps.get("Running", ""),
|
|
"processes": ps.get("Processes", ""),
|
|
"status_web_1": ps.get("Status web 1", ""),
|
|
"cpu": cstats["cpu"] if cstats else "",
|
|
"mem_used": cstats["mem_used"] if cstats else "",
|
|
"mem_limit": cstats["mem_limit"] if cstats else "",
|
|
"mem_pct": cstats["mem_pct"] if cstats else "",
|
|
})
|
|
|
|
extra_containers = docker_ps() if SHOW_CONTAINERS else []
|
|
|
|
return {
|
|
"generated_at": datetime.utcnow().isoformat() + "Z",
|
|
"poll_seconds": POLL_SECONDS,
|
|
"apps": sorted(rows, key=lambda r: r["app"]),
|
|
"containers": extra_containers,
|
|
}
|
|
|
|
@app.get("/api/status")
|
|
def api_status():
|
|
return jsonify(collect())
|
|
|
|
@app.get("/")
|
|
def index():
|
|
return render_template("index.html", poll_seconds=POLL_SECONDS)
|
|
|
|
@app.get("/partial/apps")
|
|
def partial_apps():
|
|
data = collect()
|
|
return render_template("apps_table.html", data=data) |