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
|
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:
|
def get_connection() -> Connection:
|
||||||
connection = openstack.connect(cloud=OPENSTACK_CLOUD, region_name=OPENSTACK_REGION_NAME)
|
connection = openstack.connect(cloud=OPENSTACK_CLOUD, region_name=OPENSTACK_REGION_NAME)
|
||||||
return connection
|
return connection
|
||||||
|
|||||||
@@ -2,6 +2,29 @@ import requests
|
|||||||
|
|
||||||
from watcher_visio.settings import PROMETHEUS_URL
|
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]:
|
def query_prometheus(query: str) -> str | list[str]:
|
||||||
url = f"{PROMETHEUS_URL}/api/v1/query"
|
url = f"{PROMETHEUS_URL}/api/v1/query"
|
||||||
params = {
|
params = {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from dashboard.views import (
|
|||||||
collect_audits,
|
collect_audits,
|
||||||
api_stats,
|
api_stats,
|
||||||
api_audits,
|
api_audits,
|
||||||
|
api_source_status,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -259,3 +260,102 @@ class ApiAuditsTest(TestCase):
|
|||||||
data = json.loads(response.content)
|
data = json.loads(response.content)
|
||||||
self.assertEqual(data["audits"][0]["name"], "Cached Audit")
|
self.assertEqual(data["audits"][0]["name"], "Cached Audit")
|
||||||
self.assertEqual(data["current_cluster"], cached_cluster)
|
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('', views.index, name='index'),
|
||||||
path('api/stats/', views.api_stats),
|
path('api/stats/', views.api_stats),
|
||||||
path('api/audits/', views.api_audits),
|
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.core.cache import cache
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import render
|
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.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.openstack_utils.audits import get_audits, get_current_cluster_cpu
|
||||||
from dashboard.mock_data import get_mock_context
|
from dashboard.mock_data import get_mock_context
|
||||||
|
|
||||||
@@ -265,3 +265,23 @@ def api_audits(request):
|
|||||||
current_cluster = get_current_cluster_cpu(connection)
|
current_cluster = get_current_cluster_cpu(connection)
|
||||||
cache.set(cache_key_cluster, current_cluster, timeout=cache_ttl)
|
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
|
Save as PDF
|
||||||
</button>
|
</button>
|
||||||
<span id="regionBadge" class="badge badge-primary badge-lg">{{ region.name }}</span>
|
<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">
|
<label class="swap swap-rotate theme-toggle no-print">
|
||||||
<input type="checkbox" class="theme-controller" value="dark" />
|
<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">
|
<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>
|
||||||
<script src="{% static 'js/export-pdf.js' %}"></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 %}
|
{% block script %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -155,3 +155,4 @@ CACHES = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
DASHBOARD_CACHE_TTL = 120 # seconds
|
DASHBOARD_CACHE_TTL = 120 # seconds
|
||||||
|
SOURCE_STATUS_CACHE_TTL = 30 # seconds (lightweight source-status checks)
|
||||||
|
|||||||
Reference in New Issue
Block a user