Add source status API and enhance dashboard with data source checks

- Introduced a new API endpoint `/api/source-status/` to return the status of Prometheus and OpenStack data sources.
- Implemented lightweight health check functions for both Prometheus and OpenStack.
- Updated the dashboard template to display the status of data sources dynamically.
- Added tests for the new API endpoint to ensure correct functionality and response handling.
- Configured a cache timeout for source status checks to improve performance.
This commit is contained in:
2026-02-07 17:12:25 +03:00
parent 917a7758bc
commit fd03c22042
7 changed files with 207 additions and 3 deletions

View File

@@ -3,6 +3,21 @@ from openstack.connection import Connection
from watcher_visio.settings import OPENSTACK_CLOUD, OPENSTACK_REGION_NAME
def check_openstack() -> dict:
"""
Lightweight check that OpenStack is reachable (connection only).
Returns {"status": "ok"} or {"status": "error", "message": "..."}.
"""
try:
conn = openstack.connect(cloud=OPENSTACK_CLOUD, region_name=OPENSTACK_REGION_NAME)
if conn is None:
return {"status": "error", "message": "No connection"}
return {"status": "ok"}
except Exception as e:
return {"status": "error", "message": str(e) or "Connection failed"}
def get_connection() -> Connection:
connection = openstack.connect(cloud=OPENSTACK_CLOUD, region_name=OPENSTACK_REGION_NAME)
return connection

View File

@@ -2,6 +2,29 @@ import requests
from watcher_visio.settings import PROMETHEUS_URL
# Timeout for lightweight health check (seconds)
CHECK_TIMEOUT = 5
def check_prometheus() -> dict:
"""
Lightweight check that Prometheus is reachable.
Returns {"status": "ok"} or {"status": "error", "message": "..."}.
"""
url = f"{PROMETHEUS_URL.rstrip('/')}/api/v1/query"
try:
response = requests.get(url, params={"query": "1"}, timeout=CHECK_TIMEOUT)
response.raise_for_status()
data = response.json()
if "data" in data and "result" in data["data"]:
return {"status": "ok"}
return {"status": "error", "message": "Invalid response"}
except requests.RequestException as e:
return {"status": "error", "message": str(e) or "Connection failed"}
except (ValueError, KeyError) as e:
return {"status": "error", "message": str(e) or "Invalid response"}
def query_prometheus(query: str) -> str | list[str]:
url = f"{PROMETHEUS_URL}/api/v1/query"
params = {

View File

@@ -12,6 +12,7 @@ from dashboard.views import (
collect_audits,
api_stats,
api_audits,
api_source_status,
)
@@ -259,3 +260,102 @@ class ApiAuditsTest(TestCase):
data = json.loads(response.content)
self.assertEqual(data["audits"][0]["name"], "Cached Audit")
self.assertEqual(data["current_cluster"], cached_cluster)
class ApiSourceStatusTest(TestCase):
"""Tests for api_source_status view."""
def setUp(self):
self.factory = RequestFactory()
@patch("dashboard.views.settings")
def test_api_source_status_mock_returns_mock_status(self, mock_settings):
mock_settings.USE_MOCK_DATA = True
mock_settings.SOURCE_STATUS_CACHE_TTL = 30
request = self.factory.get("/api/source-status/")
response = api_source_status(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-Type"], "application/json")
data = json.loads(response.content)
self.assertEqual(data["prometheus"]["status"], "mock")
self.assertEqual(data["openstack"]["status"], "mock")
@patch("dashboard.views.check_openstack")
@patch("dashboard.views.check_prometheus")
@patch("dashboard.views.settings")
def test_api_source_status_both_ok_returns_ok(
self, mock_settings, mock_check_prometheus, mock_check_openstack
):
mock_settings.USE_MOCK_DATA = False
mock_settings.SOURCE_STATUS_CACHE_TTL = 30
mock_check_prometheus.return_value = {"status": "ok"}
mock_check_openstack.return_value = {"status": "ok"}
cache.clear()
request = self.factory.get("/api/source-status/")
response = api_source_status(request)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertEqual(data["prometheus"]["status"], "ok")
self.assertEqual(data["openstack"]["status"], "ok")
mock_check_prometheus.assert_called_once()
mock_check_openstack.assert_called_once()
@patch("dashboard.views.check_openstack")
@patch("dashboard.views.check_prometheus")
@patch("dashboard.views.settings")
def test_api_source_status_prometheus_error_returns_error_message(
self, mock_settings, mock_check_prometheus, mock_check_openstack
):
mock_settings.USE_MOCK_DATA = False
mock_settings.SOURCE_STATUS_CACHE_TTL = 30
mock_check_prometheus.return_value = {"status": "error", "message": "Connection refused"}
mock_check_openstack.return_value = {"status": "ok"}
cache.clear()
request = self.factory.get("/api/source-status/")
response = api_source_status(request)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertEqual(data["prometheus"]["status"], "error")
self.assertEqual(data["prometheus"]["message"], "Connection refused")
self.assertEqual(data["openstack"]["status"], "ok")
@patch("dashboard.views.check_openstack")
@patch("dashboard.views.check_prometheus")
@patch("dashboard.views.settings")
def test_api_source_status_openstack_error_returns_error_message(
self, mock_settings, mock_check_prometheus, mock_check_openstack
):
mock_settings.USE_MOCK_DATA = False
mock_settings.SOURCE_STATUS_CACHE_TTL = 30
mock_check_prometheus.return_value = {"status": "ok"}
mock_check_openstack.return_value = {"status": "error", "message": "Auth failed"}
cache.clear()
request = self.factory.get("/api/source-status/")
response = api_source_status(request)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertEqual(data["prometheus"]["status"], "ok")
self.assertEqual(data["openstack"]["status"], "error")
self.assertEqual(data["openstack"]["message"], "Auth failed")
@patch("dashboard.views.check_openstack")
@patch("dashboard.views.check_prometheus")
@patch("dashboard.views.settings")
def test_api_source_status_uses_cache(
self, mock_settings, mock_check_prometheus, mock_check_openstack
):
mock_settings.USE_MOCK_DATA = False
mock_settings.SOURCE_STATUS_CACHE_TTL = 30
cache.clear()
cached = {
"prometheus": {"status": "ok"},
"openstack": {"status": "ok"},
}
cache.set("dashboard_source_status", cached, timeout=30)
request = self.factory.get("/api/source-status/")
response = api_source_status(request)
mock_check_prometheus.assert_not_called()
mock_check_openstack.assert_not_called()
data = json.loads(response.content)
self.assertEqual(data["prometheus"]["status"], "ok")
self.assertEqual(data["openstack"]["status"], "ok")

View File

@@ -5,4 +5,5 @@ urlpatterns = [
path('', views.index, name='index'),
path('api/stats/', views.api_stats),
path('api/audits/', views.api_audits),
path('api/source-status/', views.api_source_status),
]

View File

@@ -5,9 +5,9 @@ from django.conf import settings
from django.core.cache import cache
from django.http import JsonResponse
from django.shortcuts import render
from dashboard.openstack_utils.connect import get_connection
from dashboard.openstack_utils.connect import check_openstack, get_connection
from dashboard.openstack_utils.flavor import get_flavor_list
from dashboard.prometheus_utils.query import query_prometheus
from dashboard.prometheus_utils.query import check_prometheus, query_prometheus
from dashboard.openstack_utils.audits import get_audits, get_current_cluster_cpu
from dashboard.mock_data import get_mock_context
@@ -264,4 +264,24 @@ def api_audits(request):
connection = get_connection()
current_cluster = get_current_cluster_cpu(connection)
cache.set(cache_key_cluster, current_cluster, timeout=cache_ttl)
return JsonResponse({"audits": audits, "current_cluster": current_cluster})
return JsonResponse({"audits": audits, "current_cluster": current_cluster})
def api_source_status(request):
"""Return status of Prometheus and OpenStack data sources (ok / error / mock)."""
if getattr(settings, "USE_MOCK_DATA", False):
return JsonResponse({
"prometheus": {"status": "mock"},
"openstack": {"status": "mock"},
})
cache_key = "dashboard_source_status"
cache_ttl = getattr(settings, "SOURCE_STATUS_CACHE_TTL", 30)
data = cache.get(cache_key)
if data is None:
data = {
"prometheus": check_prometheus(),
"openstack": check_openstack(),
}
cache.set(cache_key, data, timeout=cache_ttl)
return JsonResponse(data)