Files
ETB/ETB-API/analytics_predictive_insights/management/commands/calculate_kpis.py
Iliyan Angelov 6b247e5b9f Updates
2025-09-19 11:58:53 +03:00

217 lines
7.9 KiB
Python

"""
Management command to calculate KPI measurements
"""
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from datetime import timedelta
from analytics_predictive_insights.models import KPIMetric, KPIMeasurement
from incident_intelligence.models import Incident
class Command(BaseCommand):
"""Calculate KPI measurements for all active metrics"""
help = 'Calculate KPI measurements for all active metrics'
def add_arguments(self, parser):
parser.add_argument(
'--metric-id',
type=str,
help='Calculate KPI for a specific metric ID only'
)
parser.add_argument(
'--time-window',
type=int,
default=24,
help='Time window in hours for KPI calculation (default: 24)'
)
parser.add_argument(
'--force',
action='store_true',
help='Force recalculation even if recent measurement exists'
)
def handle(self, *args, **options):
"""Handle the command execution"""
metric_id = options.get('metric_id')
time_window = options.get('time_window', 24)
force = options.get('force', False)
try:
if metric_id:
metrics = KPIMetric.objects.filter(id=metric_id, is_active=True)
if not metrics.exists():
raise CommandError(f'No active metric found with ID: {metric_id}')
else:
metrics = KPIMetric.objects.filter(is_active=True)
self.stdout.write(f'Calculating KPIs for {metrics.count()} metrics...')
total_calculated = 0
for metric in metrics:
try:
calculated = self._calculate_metric(metric, time_window, force)
if calculated:
total_calculated += 1
self.stdout.write(
self.style.SUCCESS(f'✓ Calculated KPI for {metric.name}')
)
else:
self.stdout.write(
self.style.WARNING(f'⚠ Skipped KPI for {metric.name} (recent measurement exists)')
)
except Exception as e:
self.stdout.write(
self.style.ERROR(f'✗ Error calculating KPI for {metric.name}: {str(e)}')
)
self.stdout.write(
self.style.SUCCESS(f'Successfully calculated {total_calculated} KPIs')
)
except Exception as e:
raise CommandError(f'Error executing command: {str(e)}')
def _calculate_metric(self, metric, time_window_hours, force=False):
"""Calculate KPI measurement for a specific metric"""
end_time = timezone.now()
start_time = end_time - timedelta(hours=time_window_hours)
# Check if recent measurement exists
if not force:
recent_measurement = KPIMeasurement.objects.filter(
metric=metric,
calculated_at__gte=end_time - timedelta(hours=1)
).first()
if recent_measurement:
return False
# Get incidents in the time window
incidents = Incident.objects.filter(
created_at__gte=start_time,
created_at__lte=end_time
)
# Apply metric filters
if metric.incident_categories:
incidents = incidents.filter(category__in=metric.incident_categories)
if metric.incident_severities:
incidents = incidents.filter(severity__in=metric.incident_severities)
if metric.incident_priorities:
incidents = incidents.filter(priority__in=metric.incident_priorities)
# Calculate metric value based on type
if metric.metric_type == 'MTTA':
value, unit = self._calculate_mtta(incidents)
elif metric.metric_type == 'MTTR':
value, unit = self._calculate_mttr(incidents)
elif metric.metric_type == 'INCIDENT_COUNT':
value, unit = incidents.count(), 'count'
elif metric.metric_type == 'RESOLUTION_RATE':
value, unit = self._calculate_resolution_rate(incidents)
elif metric.metric_type == 'AVAILABILITY':
value, unit = self._calculate_availability(incidents)
else:
value, unit = incidents.count(), 'count'
# Create or update measurement
measurement, created = KPIMeasurement.objects.get_or_create(
metric=metric,
measurement_period_start=start_time,
measurement_period_end=end_time,
defaults={
'value': value,
'unit': unit,
'incident_count': incidents.count(),
'sample_size': incidents.count()
}
)
if not created:
measurement.value = value
measurement.unit = unit
measurement.incident_count = incidents.count()
measurement.sample_size = incidents.count()
measurement.save()
return True
def _calculate_mtta(self, incidents):
"""Calculate Mean Time to Acknowledge"""
acknowledged_incidents = incidents.filter(
status__in=['IN_PROGRESS', 'RESOLVED', 'CLOSED']
).exclude(assigned_to__isnull=True)
if not acknowledged_incidents.exists():
return 0, 'minutes'
total_time = timedelta()
count = 0
for incident in acknowledged_incidents:
# Simplified calculation - in practice, you'd track acknowledgment time
if incident.updated_at and incident.created_at:
time_diff = incident.updated_at - incident.created_at
total_time += time_diff
count += 1
if count > 0:
avg_time = total_time / count
return avg_time.total_seconds() / 60, 'minutes' # Convert to minutes
return 0, 'minutes'
def _calculate_mttr(self, incidents):
"""Calculate Mean Time to Resolve"""
resolved_incidents = incidents.filter(
status__in=['RESOLVED', 'CLOSED'],
resolved_at__isnull=False
)
if not resolved_incidents.exists():
return 0, 'hours'
total_time = timedelta()
count = 0
for incident in resolved_incidents:
if incident.resolved_at and incident.created_at:
time_diff = incident.resolved_at - incident.created_at
total_time += time_diff
count += 1
if count > 0:
avg_time = total_time / count
return avg_time.total_seconds() / 3600, 'hours' # Convert to hours
return 0, 'hours'
def _calculate_resolution_rate(self, incidents):
"""Calculate resolution rate"""
total_incidents = incidents.count()
if total_incidents == 0:
return 0, 'percentage'
resolved_incidents = incidents.filter(
status__in=['RESOLVED', 'CLOSED']
).count()
rate = (resolved_incidents / total_incidents) * 100
return rate, 'percentage'
def _calculate_availability(self, incidents):
"""Calculate service availability"""
# Simplified availability calculation
# In practice, you'd need more sophisticated uptime tracking
total_incidents = incidents.count()
if total_incidents == 0:
return 100, 'percentage'
# Assume availability decreases with incident count
# This is a simplified calculation
availability = max(0, 100 - (total_incidents * 0.1))
return availability, 'percentage'