""" 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'