lots of stuff
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "internet",
|
||||
"title": "Internet",
|
||||
"subtitle": "public",
|
||||
"group": "edge"
|
||||
},
|
||||
{
|
||||
"id": "ingress",
|
||||
"title": "Ingress",
|
||||
"subtitle": "animeistrash",
|
||||
"group": "edge",
|
||||
"up": "up{job=\"node\",host=\"ingress\"}",
|
||||
"stat": "100 - (avg(rate(node_cpu_seconds_total{job=\"node\",host=\"ingress\",mode=\"idle\"}[5m])) * 100)",
|
||||
"stat_unit": "pct"
|
||||
},
|
||||
{
|
||||
"id": "tunnel",
|
||||
"title": "WireGuard",
|
||||
"subtitle": "{{ wireguard_ingress_ip }} to {{ wireguard_beepi_ip }}",
|
||||
"group": "edge",
|
||||
"up": "max(wireguard_peer_up{peer=\"beepi\"})",
|
||||
"stat": "max(wireguard_peer_handshake_age_seconds{peer=\"beepi\"})",
|
||||
"stat_unit": "h"
|
||||
},
|
||||
{
|
||||
"id": "beepi",
|
||||
"title": "beepi",
|
||||
"subtitle": "raspberry pi",
|
||||
"group": "host",
|
||||
"up": "up{job=\"node\",host=\"beepi\"}",
|
||||
"stat": "100 - (avg(rate(node_cpu_seconds_total{job=\"node\",host=\"beepi\",mode=\"idle\"}[5m])) * 100)",
|
||||
"stat_unit": "pct"
|
||||
},
|
||||
{% for service in monitored_services %}
|
||||
{
|
||||
"id": "{{ service.name }}",
|
||||
"title": "{{ service.title }}",
|
||||
"subtitle": "{{ service.vhost | default(service.group) }}",
|
||||
"group": "{{ service.group }}",
|
||||
"enabled": "max(service_enabled{service=\"{{ service.name }}\"})",
|
||||
{% if service.probe is defined or service.tcp is defined %}
|
||||
"up": "min(probe_success{service=\"{{ service.name }}\",vantage=\"local\"})",
|
||||
"stat": "max(probe_duration_seconds{service=\"{{ service.name }}\",vantage=\"local\"})",
|
||||
"stat_unit": "s"
|
||||
{% elif service.vhost is defined %}
|
||||
"up": "min(probe_success{service=\"{{ service.name }}\",vantage=\"nginx\"})",
|
||||
"stat": "max(probe_duration_seconds{service=\"{{ service.name }}\",vantage=\"nginx\"})",
|
||||
"stat_unit": "s"
|
||||
{% else %}
|
||||
"up": "min(service_units_active{service=\"{{ service.name }}\"} / service_units_total{service=\"{{ service.name }}\"})"
|
||||
{% endif %}
|
||||
},
|
||||
{% endfor %}
|
||||
{
|
||||
"id": "raid",
|
||||
"title": "RAID",
|
||||
"subtitle": "{{ backup_raid_mount }}",
|
||||
"group": "storage",
|
||||
"up": "max(backup_raid_mounted)",
|
||||
"stat": "max(node_filesystem_avail_bytes{mountpoint=\"{{ backup_raid_mount }}\"})",
|
||||
"stat_unit": "bytes",
|
||||
"fill": "1 - (max(node_filesystem_avail_bytes{mountpoint=\"{{ backup_raid_mount }}\"}) / max(node_filesystem_size_bytes{mountpoint=\"{{ backup_raid_mount }}\"}))"
|
||||
},
|
||||
{% for job in backup_jobs %}
|
||||
{% if job.name != 'offsite' %}
|
||||
{
|
||||
"id": "backup-{{ job.name }}",
|
||||
"title": "{{ job.title }}",
|
||||
"subtitle": "backup job",
|
||||
"group": "backup",
|
||||
"up": "(max(backup_last_status{backup_job=\"{{ job.name }}\"}) == 1) and ((time() - max(backup_last_success_timestamp_seconds{backup_job=\"{{ job.name }}\"})) < {{ job.max_age_hours * 3600 }})",
|
||||
"stat": "time() - max(backup_last_success_timestamp_seconds{backup_job=\"{{ job.name }}\"})",
|
||||
"stat_unit": "h"
|
||||
},
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{
|
||||
"id": "offsite",
|
||||
"title": "Offsite sync",
|
||||
"subtitle": "daily 07:00",
|
||||
"group": "storage",
|
||||
"up": "(time() - max(backup_last_success_timestamp_seconds{backup_job=\"offsite\"})) < {{ 26 * 3600 }}",
|
||||
"stat": "time() - max(backup_last_success_timestamp_seconds{backup_job=\"offsite\"})",
|
||||
"stat_unit": "h"
|
||||
},
|
||||
{
|
||||
"id": "hetzner",
|
||||
"title": "Hetzner box",
|
||||
"subtitle": "{{ hetzner_host | default('storage box') }}",
|
||||
"group": "storage",
|
||||
"up": "(max(hetzner_reachable) == 1) and (max(hetzner_remote_files{path=\"beepi\"}) > 0)",
|
||||
"stat": "max(hetzner_remote_bytes{path=\"beepi\"})",
|
||||
"stat_unit": "bytes",
|
||||
"fill": "sum(hetzner_remote_bytes) / {{ hetzner_box_bytes }}"
|
||||
},
|
||||
{
|
||||
"id": "uptimerobot",
|
||||
"title": "External checks",
|
||||
"subtitle": "uptimerobot",
|
||||
"group": "edge",
|
||||
"dashboard": "/d/beepi-uptimerobot",
|
||||
"enabled": "max(uptimerobot_api_ok)",
|
||||
"up": "min(uptimerobot_monitor_up and on (monitor) (uptimerobot_monitor_enabled == 1))",
|
||||
"stat": "count(uptimerobot_monitor_enabled == 1)"
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{"source": "uptimerobot", "target": "internet", "label": "probes"},
|
||||
{"source": "internet", "target": "ingress", "label": "public"},
|
||||
{"source": "ingress", "target": "tunnel", "label": "dnat"},
|
||||
{"source": "tunnel", "target": "beepi", "label": "wg0"},
|
||||
{% for service in monitored_services %}
|
||||
{% if service.vhost is defined and service.name != 'nginx' %}
|
||||
{"source": "nginx", "target": "{{ service.name }}", "label": "proxy"},
|
||||
{% endif %}
|
||||
{% if service.vhost is not defined %}
|
||||
{"source": "beepi", "target": "{{ service.name }}", "label": "{{ service.group }}"},
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{"source": "beepi", "target": "nginx", "label": "tls"},
|
||||
{"source": "beepi", "target": "backup-daily", "label": "03:00"},
|
||||
{"source": "backup-daily", "target": "backup-weekly", "label": "sun 05:00"},
|
||||
{"source": "backup-daily", "target": "backup-restoretest", "label": "sun 06:00"},
|
||||
{"source": "backup-daily", "target": "raid", "label": "archive"},
|
||||
{"source": "raid", "target": "offsite", "label": "rsync"},
|
||||
{"source": "offsite", "target": "hetzner", "label": "ssh 23"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
PROMETHEUS_URL = os.environ.get("PROMETHEUS_URL", "http://prometheus:9090")
|
||||
LISTEN_PORT = int(os.environ.get("LISTEN_PORT", "8088"))
|
||||
SPEC_PATH = os.environ.get("SPEC_PATH", "/app/topology-spec.json")
|
||||
CACHE_SECONDS = int(os.environ.get("CACHE_SECONDS", "10"))
|
||||
|
||||
STATUS_OK = "ok"
|
||||
STATUS_FAIL = "fail"
|
||||
STATUS_DISABLED = "disabled"
|
||||
STATUS_UNKNOWN = "unknown"
|
||||
|
||||
ARCS = {
|
||||
STATUS_OK: "arc__ok",
|
||||
STATUS_FAIL: "arc__fail",
|
||||
STATUS_DISABLED: "arc__disabled",
|
||||
STATUS_UNKNOWN: "arc__unknown",
|
||||
}
|
||||
|
||||
ARC_FIELDS = list(ARCS.values()) + ["arc__used", "arc__free"]
|
||||
|
||||
COLORS = {
|
||||
STATUS_OK: "green",
|
||||
STATUS_FAIL: "red",
|
||||
STATUS_DISABLED: "#6e7079",
|
||||
STATUS_UNKNOWN: "orange",
|
||||
}
|
||||
|
||||
DASHBOARDS = {
|
||||
"apps": "/d/beepi-services",
|
||||
"platform": "/d/beepi-services",
|
||||
"backup": "/d/beepi-backups",
|
||||
"storage": "/d/beepi-backups",
|
||||
"edge": "/d/beepi-ingress",
|
||||
"host": "/d/beepi-ingress",
|
||||
}
|
||||
|
||||
_lock = threading.Lock()
|
||||
_cache = {"at": 0.0, "body": None}
|
||||
|
||||
|
||||
def query(expr):
|
||||
url = PROMETHEUS_URL + "/api/v1/query?" + urllib.parse.urlencode({"query": expr})
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=8) as response:
|
||||
payload = json.load(response)
|
||||
except (urllib.error.URLError, OSError, ValueError):
|
||||
return None
|
||||
|
||||
if payload.get("status") != "success":
|
||||
return None
|
||||
|
||||
result = payload.get("data", {}).get("result", [])
|
||||
|
||||
if not result:
|
||||
return None
|
||||
|
||||
try:
|
||||
return float(result[0]["value"][1])
|
||||
except (KeyError, IndexError, TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def format_stat(value, unit):
|
||||
if value is None:
|
||||
return ""
|
||||
|
||||
if unit == "s":
|
||||
return "%.0f ms" % (value * 1000) if value < 1 else "%.1f s" % value
|
||||
|
||||
if unit == "h":
|
||||
return "%.1f h" % (value / 3600.0)
|
||||
|
||||
if unit == "d":
|
||||
return "%.1f d" % (value / 86400.0)
|
||||
|
||||
if unit == "bytes":
|
||||
size = float(value)
|
||||
for suffix in ("B", "KiB", "MiB", "GiB", "TiB"):
|
||||
if size < 1024 or suffix == "TiB":
|
||||
return "%.1f %s" % (size, suffix)
|
||||
size /= 1024.0
|
||||
|
||||
if unit == "pct":
|
||||
return "%.0f%%" % value
|
||||
|
||||
return "%.0f" % value
|
||||
|
||||
|
||||
def evaluate(node):
|
||||
enabled = node.get("enabled")
|
||||
|
||||
if enabled:
|
||||
value = query(enabled)
|
||||
|
||||
if value is not None and value < 1:
|
||||
return STATUS_DISABLED
|
||||
|
||||
up = node.get("up")
|
||||
|
||||
if not up:
|
||||
return STATUS_OK
|
||||
|
||||
value = query(up)
|
||||
|
||||
if value is None:
|
||||
return STATUS_UNKNOWN
|
||||
|
||||
return STATUS_OK if value >= 1 else STATUS_FAIL
|
||||
|
||||
|
||||
def build():
|
||||
with open(SPEC_PATH) as handle:
|
||||
spec = json.load(handle)
|
||||
|
||||
nodes = []
|
||||
statuses = {}
|
||||
|
||||
for node in spec["nodes"]:
|
||||
status = evaluate(node)
|
||||
statuses[node["id"]] = status
|
||||
|
||||
entry = {
|
||||
"id": node["id"],
|
||||
"title": node["title"],
|
||||
"subtitle": node.get("subtitle", ""),
|
||||
"mainstat": format_stat(query(node["stat"]), node.get("stat_unit", "")) if node.get("stat") else "",
|
||||
"detail__group": node.get("group", ""),
|
||||
"detail__status": status,
|
||||
"detail__used": "",
|
||||
"dashboard": node.get("dashboard") or DASHBOARDS.get(node.get("group", ""), "/d/beepi-map"),
|
||||
}
|
||||
|
||||
for name in ARC_FIELDS:
|
||||
entry[name] = 0.0
|
||||
|
||||
used = query(node["fill"]) if node.get("fill") and status == STATUS_OK else None
|
||||
|
||||
if used is None:
|
||||
entry[ARCS[status]] = 1.0
|
||||
else:
|
||||
used = min(max(used, 0.0), 1.0)
|
||||
entry["arc__used"] = used
|
||||
entry["arc__free"] = 1.0 - used
|
||||
entry["detail__used"] = "%.0f%% used" % (used * 100)
|
||||
|
||||
nodes.append(entry)
|
||||
|
||||
edges = []
|
||||
|
||||
for index, edge in enumerate(spec["edges"]):
|
||||
source = statuses.get(edge["source"], STATUS_UNKNOWN)
|
||||
target = statuses.get(edge["target"], STATUS_UNKNOWN)
|
||||
degraded = STATUS_FAIL in (source, target)
|
||||
idle = STATUS_DISABLED in (source, target)
|
||||
|
||||
if degraded:
|
||||
color = COLORS[STATUS_FAIL]
|
||||
elif idle:
|
||||
color = COLORS[STATUS_DISABLED]
|
||||
else:
|
||||
color = COLORS[STATUS_OK]
|
||||
|
||||
edges.append({
|
||||
"id": str(index),
|
||||
"source": edge["source"],
|
||||
"target": edge["target"],
|
||||
"mainstat": edge.get("label", ""),
|
||||
"color": color,
|
||||
"thickness": 3 if degraded else 1,
|
||||
})
|
||||
|
||||
return {"nodes": nodes, "edges": edges}
|
||||
|
||||
|
||||
def cached():
|
||||
with _lock:
|
||||
now = time.time()
|
||||
|
||||
if _cache["body"] is None or now - _cache["at"] > CACHE_SECONDS:
|
||||
_cache["body"] = json.dumps(build()).encode()
|
||||
_cache["at"] = now
|
||||
|
||||
return _cache["body"]
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path.rstrip("/") not in ("", "/topology.json", "/topology"):
|
||||
self.send_error(404)
|
||||
return
|
||||
|
||||
try:
|
||||
body = cached()
|
||||
except Exception:
|
||||
self.send_error(500)
|
||||
return
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ThreadingHTTPServer(("0.0.0.0", LISTEN_PORT), Handler).serve_forever()
|
||||
Reference in New Issue
Block a user