134 lines
4.2 KiB
Python

from __future__ import annotations
import json
import time
from typing import Any
from urllib.error import URLError
from urllib.parse import urlencode
from urllib.request import urlopen
from flask import jsonify
from .. import settings
_LAB_STATUS_CACHE: dict[str, Any] = {"ts": 0.0, "value": None}
def _vm_query(expr: str) -> float | None:
"""Run one instant VictoriaMetrics query and return the largest value."""
url = f"{settings.VM_BASE_URL}/api/v1/query?{urlencode({'query': expr})}"
with urlopen(url, timeout=settings.VM_QUERY_TIMEOUT_SEC) as resp:
payload = json.loads(resp.read().decode("utf-8"))
if payload.get("status") != "success":
return None
result = (payload.get("data") or {}).get("result") or []
if not result:
return None
values: list[float] = []
for item in result:
try:
values.append(float(item["value"][1]))
except (KeyError, IndexError, TypeError, ValueError):
continue
if not values:
return None
return max(values)
def _http_ok(url: str, expect_substring: str | None = None) -> bool:
"""Return whether a URL responds successfully and optionally contains text."""
try:
with urlopen(url, timeout=settings.HTTP_CHECK_TIMEOUT_SEC) as resp:
if getattr(resp, "status", 200) != 200:
return False
if expect_substring:
chunk = resp.read(4096).decode("utf-8", errors="ignore")
return expect_substring in chunk
return True
except (URLError, TimeoutError, ValueError):
return False
def register(app) -> None:
"""Register the lightweight lab connectivity status endpoint."""
@app.route("/api/lab/status")
def lab_status() -> Any:
"""Return cached Atlas/Oceanus health hints for the home page."""
now = time.time()
cached = _LAB_STATUS_CACHE.get("value")
if cached and (now - float(_LAB_STATUS_CACHE.get("ts", 0.0)) < settings.LAB_STATUS_CACHE_SEC):
return jsonify(cached)
t_total = time.perf_counter()
timings_ms: dict[str, int] = {}
connected = False
atlas_up = False
atlas_known = False
atlas_source = "unknown"
oceanus_up = False
oceanus_known = False
oceanus_source = "unknown"
# Atlas
try:
t_probe = time.perf_counter()
atlas_grafana_ok = _http_ok(settings.GRAFANA_HEALTH_URL, expect_substring="ok")
timings_ms["grafana"] = int((time.perf_counter() - t_probe) * 1000)
if atlas_grafana_ok:
connected = True
atlas_up = True
atlas_known = True
atlas_source = "grafana"
except Exception:
pass
if not atlas_known:
try:
t_probe = time.perf_counter()
value = _vm_query("up")
timings_ms["victoria_metrics"] = int((time.perf_counter() - t_probe) * 1000)
if value is not None:
connected = True
atlas_known = True
atlas_up = value > 0
atlas_source = "victoria-metrics"
except Exception:
pass
# Oceanus (node-exporter direct probe)
try:
t_probe = time.perf_counter()
if _http_ok(settings.OCEANUS_NODE_EXPORTER_URL):
timings_ms["oceanus_node_exporter"] = int((time.perf_counter() - t_probe) * 1000)
connected = True
oceanus_known = True
oceanus_up = True
oceanus_source = "node-exporter"
except Exception:
pass
timings_ms["total"] = int((time.perf_counter() - t_total) * 1000)
payload = {
"connected": connected,
"atlas": {"up": atlas_up, "known": atlas_known, "source": atlas_source},
"oceanus": {"up": oceanus_up, "known": oceanus_known, "source": oceanus_source},
"checked_at": int(now),
"timings_ms": timings_ms,
}
_LAB_STATUS_CACHE["ts"] = now
_LAB_STATUS_CACHE["value"] = payload
return jsonify(payload)