217 lines
7.9 KiB
Python
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'
|