billing frontend for mariadb. setup as otb_billing for outsidethebox.top accounting. also involved with outsidethedb
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

369 lines
11 KiB

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 _wallet_balance_format(raw_value, decimals):
try:
from decimal import Decimal, getcontext
getcontext().prec = 60
value = Decimal(int(raw_value)) / (Decimal(10) ** int(decimals))
fixed = f"{value:.8f}".rstrip("0").rstrip(".")
return fixed if fixed else "0"
except Exception:
return "error"
def _split_rpc_urls(value, defaults):
urls = []
if value:
for item in str(value).replace(",", " ").split():
item = item.strip()
if item:
urls.append(item)
for item in defaults:
if item and item not in urls:
urls.append(item)
return urls
def _wallet_rpc_call_one(rpc_url, method, params, timeout=5):
import json
import urllib.request
payload = json.dumps({
"jsonrpc": "2.0",
"id": 1,
"method": method,
"params": params,
}).encode("utf-8")
req = urllib.request.Request(
rpc_url,
data=payload,
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": "OTB-Billing-Health/1.0",
},
method="POST",
)
with urllib.request.urlopen(req, timeout=timeout) as resp:
data = json.loads(resp.read().decode("utf-8"))
if data.get("error"):
raise RuntimeError(str(data["error"]))
return data.get("result")
def _wallet_rpc_call(rpc_urls, method, params, timeout=5):
last_error = None
if isinstance(rpc_urls, str):
rpc_urls = [rpc_urls]
for rpc_url in rpc_urls:
try:
return _wallet_rpc_call_one(rpc_url, method, params, timeout=timeout)
except Exception as exc:
last_error = f"{rpc_url}: {exc}"
raise RuntimeError(last_error or "No RPC URLs available")
def _wallet_native_balance(rpc_urls, wallet_address, decimals=18):
result = _wallet_rpc_call(rpc_urls, "eth_getBalance", [wallet_address, "latest"])
return _wallet_balance_format(int(result or "0x0", 16), decimals)
def _wallet_erc20_balance(rpc_urls, token_contract, wallet_address, decimals):
# ERC20 balanceOf(address) selector: 0x70a08231
clean_addr = wallet_address.lower().replace("0x", "").rjust(64, "0")
data = "0x70a08231" + clean_addr
result = _wallet_rpc_call(
rpc_urls,
"eth_call",
[{"to": token_contract, "data": data}, "latest"],
)
return _wallet_balance_format(int(result or "0x0", 16), decimals)
def _wallet_balances(wallet_address=None, label=None):
import os
wallet = wallet_address or os.environ.get(
"OTB_OPERATIONS_WALLET",
"0x44f6c44C42e6ae0392E7289F032384C0d37F56D5",
)
arbitrum_rpcs = _split_rpc_urls(
os.environ.get("RPC_ARBITRUM_URLS") or os.environ.get("RPC_ARBITRUM_URL"),
[
"https://arb1.arbitrum.io/rpc",
"https://arbitrum-one-rpc.publicnode.com",
"https://arbitrum.drpc.org",
],
)
ethereum_rpcs = _split_rpc_urls(
os.environ.get("RPC_ETHEREUM_URLS") or os.environ.get("RPC_ETHEREUM_URL"),
[
"https://ethereum-rpc.publicnode.com",
"https://cloudflare-eth.com",
"https://eth.llamarpc.com",
],
)
etho_rpcs = _split_rpc_urls(
os.environ.get("RPC_ETHO_URLS") or os.environ.get("RPC_ETHO_URL"),
[
"https://rpc.ethoprotocol.com",
"https://rpc4.ethoprotocol.com",
],
)
etica_rpcs = _split_rpc_urls(
os.environ.get("RPC_ETICA_URLS") or os.environ.get("RPC_ETICA_URL"),
[
"https://rpc.etica-stats.org",
"https://eticamainnet.eticaprotocol.org",
],
)
assets = [
{
"coin": "USDC",
"chain": "Arbitrum",
"kind": "erc20",
"decimals": 6,
"rpc": arbitrum_rpcs,
"token": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
},
{
"coin": "ETH",
"chain": "Ethereum",
"kind": "native",
"decimals": 18,
"rpc": ethereum_rpcs,
},
{
"coin": "ETHO",
"chain": "Etho",
"kind": "native",
"decimals": 18,
"rpc": etho_rpcs,
},
{
"coin": "EGAZ",
"chain": "Etica",
"kind": "native",
"decimals": 18,
"rpc": etica_rpcs,
},
{
"coin": "ETI",
"chain": "Etica",
"kind": "erc20",
"decimals": 18,
"rpc": etica_rpcs,
"token": "0x34c61EA91bAcdA647269d4e310A86b875c09946f",
},
]
rows = []
for asset in assets:
row = {
"coin": asset["coin"],
"chain": asset["chain"],
"balance": "error",
"ok": False,
"error": None,
}
try:
if asset["kind"] == "native":
row["balance"] = _wallet_native_balance(
asset["rpc"],
wallet,
asset["decimals"],
)
else:
row["balance"] = _wallet_erc20_balance(
asset["rpc"],
asset["token"],
wallet,
asset["decimals"],
)
row["ok"] = True
except Exception as exc:
row["error"] = str(exc)[:220]
rows.append(row)
return {
"label": label,
"wallet": wallet,
"assets": rows,
}
def register_health_routes(app):
@app.route("/health", methods=["GET"])
def health_page():
health, http_code = _health_payload(app)
health["operations_balances"] = _wallet_balances(
os.environ.get("OTB_OPERATIONS_WALLET", "0x44f6c44C42e6ae0392E7289F032384C0d37F56D5"),
"Operations Bal",
)
health["treasury_balances"] = _wallet_balances(
os.environ.get("OTB_TREASURY_WALLET", "0xbe1fdc8c69f712d62cfcd3bf23f636de1dbd213f"),
"Treasury Bal",
)
# Backward compatible JSON key for anything already reading health.json.
health["wallet_balances"] = health["operations_balances"]
return render_template("health.html", health=health), http_code
@app.route("/health.json", methods=["GET"])
def health_json():
health, http_code = _health_payload(app)
health["operations_balances"] = _wallet_balances(
os.environ.get("OTB_OPERATIONS_WALLET", "0x44f6c44C42e6ae0392E7289F032384C0d37F56D5"),
"Operations Bal",
)
health["treasury_balances"] = _wallet_balances(
os.environ.get("OTB_TREASURY_WALLET", "0xbe1fdc8c69f712d62cfcd3bf23f636de1dbd213f"),
"Treasury Bal",
)
# Backward compatible JSON key for anything already reading health.json.
health["wallet_balances"] = health["operations_balances"]
return jsonify(health), http_code