feat(admin): Add API monitoring, debug console, and system health templates for enhanced admin interface docs: Add AI system cleanup summary and audit report detailing architecture, token management, and recommendations docs: Introduce credits and tokens system guide outlining configuration, data flow, and monitoring strategies
407 lines
13 KiB
Python
407 lines
13 KiB
Python
"""
|
|
Admin Monitoring Module - System Health, API Monitor, Debug Console
|
|
Provides read-only monitoring and debugging tools for Django Admin
|
|
"""
|
|
from django.shortcuts import render
|
|
from django.contrib.admin.views.decorators import staff_member_required
|
|
from django.utils import timezone
|
|
from django.db import connection
|
|
from django.conf import settings
|
|
import time
|
|
import os
|
|
|
|
|
|
@staff_member_required
|
|
def system_health_dashboard(request):
|
|
"""
|
|
System infrastructure health monitoring
|
|
Checks: Database, Redis, Celery, File System
|
|
"""
|
|
context = {
|
|
'page_title': 'System Health Monitor',
|
|
'checked_at': timezone.now(),
|
|
'checks': []
|
|
}
|
|
|
|
# Database Check
|
|
db_check = {
|
|
'name': 'PostgreSQL Database',
|
|
'status': 'unknown',
|
|
'message': '',
|
|
'details': {}
|
|
}
|
|
try:
|
|
start = time.time()
|
|
with connection.cursor() as cursor:
|
|
cursor.execute("SELECT version()")
|
|
version = cursor.fetchone()[0]
|
|
cursor.execute("SELECT COUNT(*) FROM django_session")
|
|
session_count = cursor.fetchone()[0]
|
|
|
|
elapsed = (time.time() - start) * 1000
|
|
db_check.update({
|
|
'status': 'healthy',
|
|
'message': f'Connected ({elapsed:.2f}ms)',
|
|
'details': {
|
|
'version': version.split('\n')[0],
|
|
'response_time': f'{elapsed:.2f}ms',
|
|
'active_sessions': session_count
|
|
}
|
|
})
|
|
except Exception as e:
|
|
db_check.update({
|
|
'status': 'error',
|
|
'message': f'Connection failed: {str(e)}'
|
|
})
|
|
context['checks'].append(db_check)
|
|
|
|
# Redis Check
|
|
redis_check = {
|
|
'name': 'Redis Cache',
|
|
'status': 'unknown',
|
|
'message': '',
|
|
'details': {}
|
|
}
|
|
try:
|
|
import redis
|
|
r = redis.Redis(
|
|
host=settings.CACHES['default']['LOCATION'].split(':')[0] if ':' in settings.CACHES['default'].get('LOCATION', '') else 'redis',
|
|
port=6379,
|
|
db=0,
|
|
socket_connect_timeout=2
|
|
)
|
|
start = time.time()
|
|
r.ping()
|
|
elapsed = (time.time() - start) * 1000
|
|
|
|
info = r.info()
|
|
redis_check.update({
|
|
'status': 'healthy',
|
|
'message': f'Connected ({elapsed:.2f}ms)',
|
|
'details': {
|
|
'version': info.get('redis_version', 'unknown'),
|
|
'uptime': f"{info.get('uptime_in_seconds', 0) // 3600}h",
|
|
'connected_clients': info.get('connected_clients', 0),
|
|
'used_memory': f"{info.get('used_memory_human', 'unknown')}",
|
|
'response_time': f'{elapsed:.2f}ms'
|
|
}
|
|
})
|
|
except Exception as e:
|
|
redis_check.update({
|
|
'status': 'error',
|
|
'message': f'Connection failed: {str(e)}'
|
|
})
|
|
context['checks'].append(redis_check)
|
|
|
|
# Celery Workers Check
|
|
celery_check = {
|
|
'name': 'Celery Workers',
|
|
'status': 'unknown',
|
|
'message': '',
|
|
'details': {}
|
|
}
|
|
try:
|
|
from igny8_core.celery import app
|
|
inspect = app.control.inspect(timeout=2)
|
|
stats = inspect.stats()
|
|
active = inspect.active()
|
|
|
|
if stats:
|
|
worker_count = len(stats)
|
|
total_tasks = sum(len(tasks) for tasks in active.values()) if active else 0
|
|
celery_check.update({
|
|
'status': 'healthy',
|
|
'message': f'{worker_count} worker(s) active',
|
|
'details': {
|
|
'workers': worker_count,
|
|
'active_tasks': total_tasks,
|
|
'worker_names': list(stats.keys())
|
|
}
|
|
})
|
|
else:
|
|
celery_check.update({
|
|
'status': 'warning',
|
|
'message': 'No workers responding'
|
|
})
|
|
except Exception as e:
|
|
celery_check.update({
|
|
'status': 'error',
|
|
'message': f'Check failed: {str(e)}'
|
|
})
|
|
context['checks'].append(celery_check)
|
|
|
|
# File System Check
|
|
fs_check = {
|
|
'name': 'File System',
|
|
'status': 'unknown',
|
|
'message': '',
|
|
'details': {}
|
|
}
|
|
try:
|
|
import shutil
|
|
media_root = settings.MEDIA_ROOT
|
|
static_root = settings.STATIC_ROOT
|
|
|
|
media_stat = shutil.disk_usage(media_root) if os.path.exists(media_root) else None
|
|
|
|
if media_stat:
|
|
free_gb = media_stat.free / (1024**3)
|
|
total_gb = media_stat.total / (1024**3)
|
|
used_percent = (media_stat.used / media_stat.total) * 100
|
|
|
|
fs_check.update({
|
|
'status': 'healthy' if used_percent < 90 else 'warning',
|
|
'message': f'{free_gb:.1f}GB free of {total_gb:.1f}GB',
|
|
'details': {
|
|
'media_root': media_root,
|
|
'free_space': f'{free_gb:.1f}GB',
|
|
'total_space': f'{total_gb:.1f}GB',
|
|
'used_percent': f'{used_percent:.1f}%'
|
|
}
|
|
})
|
|
else:
|
|
fs_check.update({
|
|
'status': 'warning',
|
|
'message': 'Media directory not found'
|
|
})
|
|
except Exception as e:
|
|
fs_check.update({
|
|
'status': 'error',
|
|
'message': f'Check failed: {str(e)}'
|
|
})
|
|
context['checks'].append(fs_check)
|
|
|
|
# Overall system status
|
|
statuses = [check['status'] for check in context['checks']]
|
|
if 'error' in statuses:
|
|
context['overall_status'] = 'error'
|
|
context['overall_message'] = 'System has errors'
|
|
elif 'warning' in statuses:
|
|
context['overall_status'] = 'warning'
|
|
context['overall_message'] = 'System has warnings'
|
|
else:
|
|
context['overall_status'] = 'healthy'
|
|
context['overall_message'] = 'All systems operational'
|
|
|
|
return render(request, 'admin/monitoring/system_health.html', context)
|
|
|
|
|
|
@staff_member_required
|
|
def api_monitor_dashboard(request):
|
|
"""
|
|
API endpoint health monitoring
|
|
Tests key endpoints and displays response times
|
|
"""
|
|
from django.test.client import Client
|
|
|
|
context = {
|
|
'page_title': 'API Monitor',
|
|
'checked_at': timezone.now(),
|
|
'endpoint_groups': []
|
|
}
|
|
|
|
# Define endpoint groups to check
|
|
endpoint_configs = [
|
|
{
|
|
'name': 'Authentication',
|
|
'endpoints': [
|
|
{'path': '/api/v1/auth/check/', 'method': 'GET', 'auth_required': False},
|
|
]
|
|
},
|
|
{
|
|
'name': 'System Settings',
|
|
'endpoints': [
|
|
{'path': '/api/v1/system/health/', 'method': 'GET', 'auth_required': False},
|
|
]
|
|
},
|
|
{
|
|
'name': 'Planner Module',
|
|
'endpoints': [
|
|
{'path': '/api/v1/planner/keywords/', 'method': 'GET', 'auth_required': True},
|
|
]
|
|
},
|
|
{
|
|
'name': 'Writer Module',
|
|
'endpoints': [
|
|
{'path': '/api/v1/writer/tasks/', 'method': 'GET', 'auth_required': True},
|
|
]
|
|
},
|
|
{
|
|
'name': 'Billing',
|
|
'endpoints': [
|
|
{'path': '/api/v1/billing/credits/balance/', 'method': 'GET', 'auth_required': True},
|
|
]
|
|
},
|
|
]
|
|
|
|
client = Client()
|
|
|
|
for group_config in endpoint_configs:
|
|
group_results = {
|
|
'name': group_config['name'],
|
|
'endpoints': []
|
|
}
|
|
|
|
for endpoint in group_config['endpoints']:
|
|
result = {
|
|
'path': endpoint['path'],
|
|
'method': endpoint['method'],
|
|
'status': 'unknown',
|
|
'status_code': None,
|
|
'response_time': None,
|
|
'message': ''
|
|
}
|
|
|
|
try:
|
|
start = time.time()
|
|
|
|
if endpoint['method'] == 'GET':
|
|
response = client.get(endpoint['path'])
|
|
else:
|
|
response = client.post(endpoint['path'])
|
|
|
|
elapsed = (time.time() - start) * 1000
|
|
|
|
result.update({
|
|
'status_code': response.status_code,
|
|
'response_time': f'{elapsed:.2f}ms',
|
|
})
|
|
|
|
# Determine status
|
|
if response.status_code < 300:
|
|
result['status'] = 'healthy'
|
|
result['message'] = 'OK'
|
|
elif response.status_code == 401 and endpoint.get('auth_required'):
|
|
result['status'] = 'healthy'
|
|
result['message'] = 'Auth required (expected)'
|
|
elif response.status_code < 500:
|
|
result['status'] = 'warning'
|
|
result['message'] = 'Client error'
|
|
else:
|
|
result['status'] = 'error'
|
|
result['message'] = 'Server error'
|
|
|
|
except Exception as e:
|
|
result.update({
|
|
'status': 'error',
|
|
'message': str(e)[:100]
|
|
})
|
|
|
|
group_results['endpoints'].append(result)
|
|
|
|
context['endpoint_groups'].append(group_results)
|
|
|
|
# Calculate overall stats
|
|
all_endpoints = [ep for group in context['endpoint_groups'] for ep in group['endpoints']]
|
|
total = len(all_endpoints)
|
|
healthy = len([ep for ep in all_endpoints if ep['status'] == 'healthy'])
|
|
warnings = len([ep for ep in all_endpoints if ep['status'] == 'warning'])
|
|
errors = len([ep for ep in all_endpoints if ep['status'] == 'error'])
|
|
|
|
context['stats'] = {
|
|
'total': total,
|
|
'healthy': healthy,
|
|
'warnings': warnings,
|
|
'errors': errors,
|
|
'health_percentage': (healthy / total * 100) if total > 0 else 0
|
|
}
|
|
|
|
return render(request, 'admin/monitoring/api_monitor.html', context)
|
|
|
|
|
|
@staff_member_required
|
|
def debug_console(request):
|
|
"""
|
|
System debug information (read-only)
|
|
Shows environment, database config, cache config, etc.
|
|
"""
|
|
context = {
|
|
'page_title': 'Debug Console',
|
|
'checked_at': timezone.now(),
|
|
'sections': []
|
|
}
|
|
|
|
# Environment Variables Section
|
|
env_section = {
|
|
'title': 'Environment',
|
|
'items': {
|
|
'DEBUG': settings.DEBUG,
|
|
'ENVIRONMENT': os.getenv('ENVIRONMENT', 'not set'),
|
|
'DJANGO_SETTINGS_MODULE': os.getenv('DJANGO_SETTINGS_MODULE', 'not set'),
|
|
'ALLOWED_HOSTS': settings.ALLOWED_HOSTS,
|
|
'TIME_ZONE': settings.TIME_ZONE,
|
|
'USE_TZ': settings.USE_TZ,
|
|
}
|
|
}
|
|
context['sections'].append(env_section)
|
|
|
|
# Database Configuration
|
|
db_config = settings.DATABASES.get('default', {})
|
|
db_section = {
|
|
'title': 'Database Configuration',
|
|
'items': {
|
|
'ENGINE': db_config.get('ENGINE', 'not set'),
|
|
'NAME': db_config.get('NAME', 'not set'),
|
|
'HOST': db_config.get('HOST', 'not set'),
|
|
'PORT': db_config.get('PORT', 'not set'),
|
|
'CONN_MAX_AGE': db_config.get('CONN_MAX_AGE', 'not set'),
|
|
}
|
|
}
|
|
context['sections'].append(db_section)
|
|
|
|
# Cache Configuration
|
|
cache_config = settings.CACHES.get('default', {})
|
|
cache_section = {
|
|
'title': 'Cache Configuration',
|
|
'items': {
|
|
'BACKEND': cache_config.get('BACKEND', 'not set'),
|
|
'LOCATION': cache_config.get('LOCATION', 'not set'),
|
|
'KEY_PREFIX': cache_config.get('KEY_PREFIX', 'not set'),
|
|
}
|
|
}
|
|
context['sections'].append(cache_section)
|
|
|
|
# Celery Configuration
|
|
celery_section = {
|
|
'title': 'Celery Configuration',
|
|
'items': {
|
|
'BROKER_URL': getattr(settings, 'CELERY_BROKER_URL', 'not set'),
|
|
'RESULT_BACKEND': getattr(settings, 'CELERY_RESULT_BACKEND', 'not set'),
|
|
'TASK_ALWAYS_EAGER': getattr(settings, 'CELERY_TASK_ALWAYS_EAGER', False),
|
|
}
|
|
}
|
|
context['sections'].append(celery_section)
|
|
|
|
# Media & Static Files
|
|
files_section = {
|
|
'title': 'Media & Static Files',
|
|
'items': {
|
|
'MEDIA_ROOT': settings.MEDIA_ROOT,
|
|
'MEDIA_URL': settings.MEDIA_URL,
|
|
'STATIC_ROOT': settings.STATIC_ROOT,
|
|
'STATIC_URL': settings.STATIC_URL,
|
|
}
|
|
}
|
|
context['sections'].append(files_section)
|
|
|
|
# Installed Apps (count)
|
|
apps_section = {
|
|
'title': 'Installed Applications',
|
|
'items': {
|
|
'Total Apps': len(settings.INSTALLED_APPS),
|
|
'Custom Apps': len([app for app in settings.INSTALLED_APPS if app.startswith('igny8_')]),
|
|
}
|
|
}
|
|
context['sections'].append(apps_section)
|
|
|
|
# Middleware (count)
|
|
middleware_section = {
|
|
'title': 'Middleware',
|
|
'items': {
|
|
'Total Middleware': len(settings.MIDDLEWARE),
|
|
}
|
|
}
|
|
context['sections'].append(middleware_section)
|
|
|
|
return render(request, 'admin/monitoring/debug_console.html', context)
|