Updates
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
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'
|
||||
Reference in New Issue
Block a user