import os import time import shutil import platform from datetime import datetime, timezone from zoneinfo import ZoneInfo from flask import render_template, jsonify APP_START_TS = time.time() def _read_meminfo(): data = {} try: with open("/proc/meminfo", "r", encoding="utf-8") as f: for line in f: if ":" not in line: continue key, val = line.split(":", 1) data[key.strip()] = val.strip() except Exception: pass return data def _kb_to_mb(kb_value): try: return round(int(kb_value) / 1024, 2) except Exception: return None def _server_uptime_seconds(): try: with open("/proc/uptime", "r", encoding="utf-8") as f: return int(float(f.read().split()[0])) except Exception: return None def _format_duration(seconds): if seconds is None: return None seconds = int(seconds) days, rem = divmod(seconds, 86400) hours, rem = divmod(rem, 3600) minutes, secs = divmod(rem, 60) return f"{days}d {hours}h {minutes}m {secs}s" def _health_payload(app): now_utc = datetime.now(timezone.utc) now_toronto = now_utc.astimezone(ZoneInfo("America/Toronto")) load1 = load5 = load15 = None try: load1, load5, load15 = os.getloadavg() except Exception: pass meminfo = _read_meminfo() mem_total_kb = None mem_available_kb = None mem_used_kb = None mem_used_percent = None try: mem_total_kb = int(meminfo.get("MemTotal", "0 kB").split()[0]) mem_available_kb = int(meminfo.get("MemAvailable", "0 kB").split()[0]) mem_used_kb = mem_total_kb - mem_available_kb if mem_total_kb > 0: mem_used_percent = round((mem_used_kb / mem_total_kb) * 100, 2) except Exception: pass disk = shutil.disk_usage("/") db_ok = False db_error = None try: connector = app.config.get("OTB_HEALTH_DB_CONNECTOR") if callable(connector): conn = connector() cur = conn.cursor() cur.execute("SELECT 1") cur.fetchone() cur.close() conn.close() db_ok = True else: db_error = "DB connector not registered" except Exception as e: db_error = str(e) app_uptime = int(time.time() - APP_START_TS) server_uptime = _server_uptime_seconds() payload = { "status": "ok" if db_ok else "degraded", "app_name": "otb_billing", "hostname": platform.node(), "server_time_utc": now_utc.isoformat(), "server_time_toronto": now_toronto.isoformat(), "app_uptime_seconds": app_uptime, "app_uptime_human": _format_duration(app_uptime), "server_uptime_seconds": server_uptime, "server_uptime_human": _format_duration(server_uptime), "load_average": {"1m": load1, "5m": load5, "15m": load15}, "memory": { "total_mb": _kb_to_mb(mem_total_kb) if mem_total_kb is not None else None, "available_mb": _kb_to_mb(mem_available_kb) if mem_available_kb is not None else None, "used_mb": _kb_to_mb(mem_used_kb) if mem_used_kb is not None else None, "used_percent": mem_used_percent, }, "disk_root": { "total_gb": round(disk.total / (1024**3), 2), "used_gb": round(disk.used / (1024**3), 2), "free_gb": round(disk.free / (1024**3), 2), "used_percent": round((disk.used / disk.total) * 100, 2) if disk.total else None, }, "database": {"ok": db_ok, "error": db_error}, } http_code = 200 if db_ok else 503 return payload, http_code def register_health_routes(app): @app.route("/health", methods=["GET"]) def health_page(): health, http_code = _health_payload(app) return render_template("health.html", health=health), http_code @app.route("/health.json", methods=["GET"]) def health_json(): health, http_code = _health_payload(app) return jsonify(health), http_code