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:
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
@@ -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)
|
||||
@@ -29,6 +29,10 @@
|
||||
Save as PDF
|
||||
</button>
|
||||
<span id="regionBadge" class="badge badge-primary badge-lg">{{ region.name }}</span>
|
||||
<div id="source-status" class="flex items-center gap-2 no-print" aria-label="Data source status">
|
||||
<span id="source-status-prometheus" class="badge badge-ghost badge-sm" title="Prometheus">Prometheus: …</span>
|
||||
<span id="source-status-openstack" class="badge badge-ghost badge-sm" title="OpenStack">OpenStack: …</span>
|
||||
</div>
|
||||
<label class="swap swap-rotate theme-toggle no-print">
|
||||
<input type="checkbox" class="theme-controller" value="dark" />
|
||||
<svg class="swap-off fill-current w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
@@ -71,6 +75,46 @@
|
||||
});
|
||||
</script>
|
||||
<script src="{% static 'js/export-pdf.js' %}"></script>
|
||||
<script>
|
||||
(function() {
|
||||
function updateSourceStatus(el, label, data) {
|
||||
if (!el) return;
|
||||
var status = data && data.status;
|
||||
var msg = data && data.message;
|
||||
var title = msg ? (label + ': ' + msg) : label;
|
||||
el.setAttribute('title', title);
|
||||
el.setAttribute('aria-label', title);
|
||||
if (status === 'ok') {
|
||||
el.textContent = label + ': OK';
|
||||
el.classList.remove('badge-error', 'badge-warning');
|
||||
el.classList.add('badge-success');
|
||||
} else if (status === 'error') {
|
||||
el.textContent = label + ': Error';
|
||||
el.classList.remove('badge-success', 'badge-warning');
|
||||
el.classList.add('badge-error');
|
||||
} else if (status === 'mock') {
|
||||
el.textContent = label + ': Mock';
|
||||
el.classList.remove('badge-error', 'badge-success');
|
||||
el.classList.add('badge-warning');
|
||||
} else {
|
||||
el.textContent = label + ': …';
|
||||
el.classList.remove('badge-success', 'badge-error', 'badge-warning');
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var promEl = document.getElementById('source-status-prometheus');
|
||||
var osEl = document.getElementById('source-status-openstack');
|
||||
if (!promEl || !osEl) return;
|
||||
fetch('/api/source-status/').then(function(r) { return r.ok ? r.json() : {}; }).then(function(data) {
|
||||
updateSourceStatus(promEl, 'Prometheus', data.prometheus);
|
||||
updateSourceStatus(osEl, 'OpenStack', data.openstack);
|
||||
}).catch(function() {
|
||||
updateSourceStatus(promEl, 'Prometheus', { status: 'error', message: 'Failed to fetch status' });
|
||||
updateSourceStatus(osEl, 'OpenStack', { status: 'error', message: 'Failed to fetch status' });
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% block script %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
||||
@@ -155,3 +155,4 @@ CACHES = {
|
||||
}
|
||||
}
|
||||
DASHBOARD_CACHE_TTL = 120 # seconds
|
||||
SOURCE_STATUS_CACHE_TTL = 30 # seconds (lightweight source-status checks)
|
||||
|
||||
Reference in New Issue
Block a user