incidentops / server /scenario_engine.py
Bot
Initial commit of IncidentOps OpenEnv environment
471d260
Raw History Blame Contribute Delete
6.65 kB
import json
import random
from typing import Dict, List, Any
from pathlib import Path
from server.models import Alert, LogEntry, MetricSeries, DeployEvent
class ScenarioEngine:
def __init__(self):
self.templates_dir = Path(__file__).parent.parent / 'scenarios'
self.templates = self._load_templates()
def _load_templates(self) -> Dict[str, Dict]:
templates = {}
for f in self.templates_dir.glob('*.json'):
with open(f) as file:
templates[f.stem] = json.load(file)
return templates
def generate(self, template_id: str, seed: int) -> Dict[str, Any]:
rng = random.Random(seed)
if template_id not in self.templates:
raise ValueError(f"Scenario template {template_id} not found.")
template = self.templates[template_id]
# Generate base timeline (60 minutes of logs/metrics)
timeline = self._generate_timeline(rng)
# Inject root cause anomaly
if 'root_anomaly' in template:
root_anomaly = template['root_anomaly']
self._inject_root_anomaly(timeline, root_anomaly, rng)
# Cascade to dependents
for dep in template.get('dependents', []):
lag = rng.randint(2, 8)
self._cascade_to_dependent(timeline, dep, lag, rng)
# Generate logs from anomalies
logs = self._generate_logs(timeline, template, rng)
# Build final realistic alert list with noise
base_alerts = template.get('real_alerts', [])
noise_alerts = self._generate_noise_alerts(rng.randint(4, 7), rng)
all_alerts = base_alerts + noise_alerts
rng.shuffle(all_alerts)
# Deploy history
deploys = self._generate_deploy_history(template, rng)
return {
'id': template_id,
'seed': seed,
'alerts': all_alerts,
'logs': logs,
'metrics': timeline['metrics'],
'deploy_history': deploys,
'root_cause_keywords': template.get('root_cause_keywords', {}),
'valid_fixes': template.get('valid_fixes', []),
'relevant_services': template.get('relevant_services', []),
}
def _generate_timeline(self, rng: random.Random) -> Dict:
timestamps = [f"2023-10-01T00:{m:02d}:00Z" for m in range(60)]
metrics = {}
for service in ['payment-api', 'order-service', 'db-cluster']:
metrics[f"{service}.cpu_percent"] = MetricSeries(
name=f"{service}.cpu_percent", unit="%", timestamps=timestamps,
values=[rng.uniform(20, 40) for _ in timestamps]
)
metrics[f"{service}.memory_percent"] = MetricSeries(
name=f"{service}.memory_percent", unit="%", timestamps=timestamps,
values=[rng.uniform(40, 60) for _ in timestamps]
)
metrics[f"{service}.requests_5xx"] = MetricSeries(
name=f"{service}.requests_5xx", unit="ops", timestamps=timestamps,
values=[rng.uniform(0, 0.5) for _ in timestamps]
)
return {'timestamps': timestamps, 'metrics': metrics}
def _inject_root_anomaly(self, timeline: Dict, anomaly: Dict, rng: random.Random):
start_min = anomaly.get('start_min', 30)
end_min = anomaly.get('end_min', 60)
metric_name = anomaly.get('metric')
if metric_name in timeline['metrics']:
for i in range(start_min, end_min):
timeline['metrics'][metric_name].values[i] += rng.uniform(40, 60)
def _cascade_to_dependent(self, timeline: Dict, dep: Dict, lag: int, rng: random.Random):
metric_name = dep.get('metric')
# Simple cascade logic at start+lag
if metric_name in timeline['metrics']:
for i in range(30 + lag, 60):
timeline['metrics'][metric_name].values[i] += rng.uniform(5, 15) * dep.get('cascade_factor', 1)
def _generate_logs(self, timeline: Dict, template: Dict, rng: random.Random) -> List[Dict]:
logs = []
for i, ts in enumerate(timeline['timestamps']):
for srv in ['payment-api', 'order-service', 'db-cluster']:
# Background info logs
if rng.random() > 0.5:
logs.append({"ts": ts, "level": "INFO", "service": srv, "message": f"Processed request successfully for {srv}"})
# If anomaly is happening, inject errors
if i >= 30: # Hardcoded anomaly window for simplicity
if srv in template.get('relevant_services', []):
if rng.random() > 0.7:
logs.append({"ts": ts, "level": "ERROR", "service": srv, "message": f"Connection pool exhausted or memory high in {srv}"})
return logs
def _generate_noise_alerts(self, count: int, rng: random.Random) -> List[Dict]:
noise = []
for i in range(count):
noise.append({
"alert_id": f"noise_{i}",
"title": f"Intermittent latency in {rng.choice(['frontend', 'worker-queue'])}",
"service": rng.choice(['frontend', 'worker-queue']),
"timestamp": f"2023-10-01T00:{rng.randint(0, 59):02d}:00Z",
"raw_value": rng.uniform(500, 800),
"threshold": 500,
# Intentionally missing true_severity and true_team or set to false flags for grading
})
return noise
def _generate_deploy_history(self, template: Dict, rng: random.Random) -> List[Dict]:
deploys = []
services = ['payment-api', 'order-service', 'frontend']
for i in range(10):
srv = rng.choice(services)
deploys.append({
"deploy_id": f"dep_{i}",
"service": srv,
"sha": f"{rng.randint(100000, 999999)}",
"timestamp": f"2023-10-01T00:{rng.randint(0, 59):02d}:00Z",
"deployer": f"user_{rng.randint(1, 5)}",
"diff_summary": "Minor fixes and updates"
})
# Inject bad deploy if missing
if 'root_cause_keywords' in template and 'v2.4.1' in template['root_cause_keywords'].get('required', []):
deploys.append({
"deploy_id": "dep_bad",
"service": "payment-api",
"sha": "bad123",
"timestamp": "2023-10-01T00:25:00Z",
"deployer": "user_1",
"diff_summary": "Update payment-api to v2.4.1"
})
return sorted(deploys, key=lambda x: x['timestamp'])