Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display worker status #151

Merged
merged 14 commits into from
Aug 15, 2023
172 changes: 131 additions & 41 deletions chirps/base_app/templates/header.html
Original file line number Diff line number Diff line change
@@ -1,45 +1,135 @@
{% load static %}
<style>
.worker-status-indicator {
display: inline-block;
width: 20px;
height: 20px;
border-radius: 50%;
}
</style>
<div>
<nav class="navbar navbar-light navbar-expand-lg bg-light w-100">
<div class="container-fluid">
<a class="navbar-brand" href="/"><img src="{% static 'account/chirps_logo.png' %}" width="32"></a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>

<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">

<li class="nav-item">
<a class="nav-link" aria-current="page" href="{% url 'asset_dashboard' %}"><i class="fa-solid fa-bullseye"></i> Assets</i></a>
</li>

<li class="nav-item">
<a class="nav-link" aria-current="page"href="{% url 'policy_dashboard' %}"><i class="fa-solid fa-map"></i> Policies</a>
</li>

<li class="nav-item">
<a class="nav-link" aria-current="page" href="{% url 'scan_dashboard' %}"><i class="fa-solid fa-magnifying-glass"></i> Scans</a>
</li>

{% if user.is_authenticated %}

<li class="nav-item">
<a class="nav-link" aria-current="page" href="{% url 'profile' %}"><i class="fa-regular fa-circle-user"></i> Account</a>
</li>

<li class="nav-item">
<a class="nav-link" aria-current="page" href="{% url 'logout' %}"><i class="fa-solid fa-arrow-right-from-bracket"></i> Logout</a>
</li>
{% endif %}

{% if user.is_superuser %}
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{% url 'admin:index' %}"><i class="fa-solid fa-user-ninja"></i> Admin</a>
</li>
{% endif %}
</ul>
</div>
<nav class="navbar navbar-light navbar-expand-lg bg-light w-100">
<div class="container-fluid d-flex justify-content-between">
<div>
<a class="navbar-brand" href="/"><img src="{% static 'account/chirps_logo.png' %}" width="32"></a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>

<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{% url 'asset_dashboard' %}"><i
class="fa-solid fa-bullseye"></i> Assets</i></a>
</li>

<li class="nav-item">
<a class="nav-link" aria-current="page" href="{% url 'policy_dashboard' %}"><i class="fa-solid fa-map"></i>
Policies</a>
</li>

<li class="nav-item">
<a class="nav-link" aria-current="page" href="{% url 'scan_dashboard' %}"><i
class="fa-solid fa-magnifying-glass"></i> Scans</a>
</li>

{% if user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{% url 'profile' %}"><i class="fa-regular fa-circle-user"></i>
Account</a>
</li>

<li class="nav-item">
<a class="nav-link" aria-current="page" href="{% url 'logout' %}"><i
class="fa-solid fa-arrow-right-from-bracket"></i> Logout</a>
</li>
{% endif %}

{% if user.is_superuser %}
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{% url 'admin:index' %}"><i
class="fa-solid fa-user-ninja"></i> Admin</a>
</li>
{% endif %}
</ul>
</div>
<!-- Status indicator -->
<div class="ms-auto">
<div id="worker-status" hx-get="/worker/status/" hx-trigger="load, every 5s" hx-swap="none">
<div class="worker-status-indicator" title="Worker status" hx-trigger="click"
hx-swap="none"></div>
</div>
</nav>
</div>
</div>
</nav>
</div>

<!-- Status Details Modal -->
<div class="modal fade" id="statusDetailsModal" tabindex="-1" aria-labelledby="statusDetailsModalLabel"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="statusDetailsModalLabel">Worker Status Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="statusDetailsModalBody">
<!-- Status details will be displayed here -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>

<script>
document.body.addEventListener('htmx:configRequest', function (event) {
if (event.target.id === 'worker-status') {
event.detail.headers['HX-Response-Content-Type'] = 'application/json';
}
});

document.body.addEventListener('htmx:afterSwap', function (event) {
if (event.target.id === 'worker-status') {
let xhr = event.detail.xhr;
if (xhr) {
let response = JSON.parse(xhr.responseText);
let overallStatus = response.overall_status;
let serviceStatuses = response.service_statuses;
let indicator = document.querySelector("#worker-status .worker-status-indicator");
indicator.style.backgroundColor = overallStatus;
indicator.setAttribute('title', 'Click to see service statuses');

// Store the service statuses in the indicator element for later use
indicator.dataset.serviceStatuses = JSON.stringify(serviceStatuses);
}
}
});

document.addEventListener('DOMContentLoaded', function () {
let indicator = document.querySelector("#worker-status .worker-status-indicator");
});

document.addEventListener('click', function (event) {
let indicator = document.querySelector("#worker-status .worker-status-indicator");
if (event.target === indicator) {
let serviceStatuses = JSON.parse(indicator.dataset.serviceStatuses);

let statusesHTML = '';
for (const [service, status] of Object.entries(serviceStatuses)) {
statusesHTML += `<p><strong>${service} status:</strong> ${status ? 'Online' : 'Offline'}</p>`;
}

// Display the service statuses in the modal body
let modalBody = document.getElementById('statusDetailsModalBody');
modalBody.innerHTML = statusesHTML;

// Show the modal
let statusDetailsModal = new bootstrap.Modal(document.getElementById('statusDetailsModal'));
statusDetailsModal.show();
}
});
</script>
1 change: 1 addition & 0 deletions chirps/chirps/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
'account',
'policy',
'embedding',
'worker',
]

MIDDLEWARE = [
Expand Down
1 change: 1 addition & 0 deletions chirps/chirps/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@
path('policy/', include('policy.urls')),
path('scan/', include('scan.urls')),
path('asset/', include('asset.urls')),
path('worker/', include('worker.urls')),
]
Empty file added chirps/worker/__init__.py
Empty file.
9 changes: 9 additions & 0 deletions chirps/worker/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Worker application apps"""
from django.apps import AppConfig


class WorkerConfig(AppConfig):
"""Worker application config"""

default_auto_field = 'django.db.models.BigAutoField'
name = 'worker'
Empty file.
48 changes: 48 additions & 0 deletions chirps/worker/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""worker application tests"""
from unittest.mock import patch

from django.test import Client, TestCase


class WorkerStatusViewTests(TestCase):
"""Tests of the worker status view"""

def setUp(self):
"""Test setup"""
self.client = Client()

@patch('worker.views.app')
@patch('worker.views.is_redis_running')
@patch('worker.views.os')
def test_worker_status_green(self, mock_os, mock_is_redis_running, mock_app):
"""Test response when services are running"""
# Mock the Celery worker, RabbitMQ, and Redis statuses to be running
mock_app.control.inspect.return_value.ping.return_value = {'worker1': {'ok': 'pong'}}
mock_os.system.return_value = 0
mock_is_redis_running.return_value = True

response = self.client.get('/worker/status/')

self.assertEqual(response.status_code, 200)
self.assertEqual(
response.json(),
{'overall_status': 'green', 'service_statuses': {'celery': True, 'rabbitmq': True, 'redis': True}},
)

@patch('worker.views.app')
@patch('worker.views.is_redis_running')
@patch('worker.views.os')
def test_worker_status_red(self, mock_os, mock_is_redis_running, mock_app):
"""Test response when services are not running"""
# Mock the Celery worker, RabbitMQ, and Redis statuses to be not running
mock_app.control.inspect.return_value.ping.return_value = None
mock_os.system.return_value = 1
mock_is_redis_running.return_value = False

response = self.client.get('/worker/status/')

self.assertEqual(response.status_code, 200)
self.assertEqual(
response.json(),
{'overall_status': 'red', 'service_statuses': {'celery': False, 'rabbitmq': False, 'redis': False}},
)
8 changes: 8 additions & 0 deletions chirps/worker/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Worker application URLs"""
from django.urls import path

from . import views

urlpatterns = [
path('status/', views.worker_status, name='worker_status'),
]
43 changes: 43 additions & 0 deletions chirps/worker/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Worker application views"""
import os
import subprocess

from django.http import JsonResponse
from requests import Request

from chirps.celery import app


def is_redis_running() -> bool:
"""Check redis status"""
cmd = 'docker-compose -f /workspace/.devcontainer/docker-compose.yml ps | grep redis'
try:
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, text=True, check=True)
except subprocess.CalledProcessError:
return False

if result.returncode == 0 and 'redis' in result.stdout and 'Up' in result.stdout:
return True

return False


def worker_status(request: Request) -> JsonResponse:
"""Get the status of the Celery worker"""
celery_inspection = app.control.inspect()
celery_statuses = celery_inspection.ping()

is_celery_running = False
if celery_statuses:
is_celery_running = all(v['ok'] == 'pong' for v in celery_statuses.values())

is_rabbit_running = os.system('rabbitmqctl ping') == 0

service_statuses = {'celery': is_celery_running, 'rabbitmq': is_rabbit_running, 'redis': is_redis_running()}

if all(result is True for result in service_statuses.values()):
status = 'green'
else:
status = 'red'

return JsonResponse({'overall_status': status, 'service_statuses': service_statuses})
3 changes: 3 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ ignore_missing_imports = True

[mypy-mantium_spec.*]
ignore_missing_imports = True

[mypy-chirps.*]
ignore_missing_imports = True