""" Service simulation helpers — generates alerts, formats data, cascades dependency health. """ from typing import Any, Dict, List, Set, Tuple from models import ServiceStatus def generate_alerts( services: Dict[str, Any], scenario_alerts: List[str], fixed_services: Set[str], ) -> List[str]: """Regenerate alerts based on current service state. If all root-cause services are fixed, alerts clear.""" alerts: List[str] = [] for svc_name, svc in services.items(): status = svc["status"] if status == ServiceStatus.DOWN and svc_name not in fixed_services: alerts.append(f"[ALERT SEV-1] {svc_name}: service is DOWN, 0 healthy pods") elif status == ServiceStatus.DEGRADED and svc_name not in fixed_services: alerts.append(f"[ALERT SEV-2] {svc_name}: service is DEGRADED") if not alerts: return ["[INFO] All services HEALTHY — no active alerts."] return alerts def recompute_health( services: Dict[str, Any], dependencies: Dict[str, List[str]], fixed_services: Set[str], root_cause_map: Dict[str, str], ) -> Dict[str, Any]: """Walk the dependency graph and update service health. Rules: - A root-cause service that has been fixed becomes HEALTHY. - A non-root-cause service becomes HEALTHY if all its deps are HEALTHY. - A non-root-cause service becomes DEGRADED if any dep is DEGRADED. - A non-root-cause service becomes DOWN if any dep is DOWN. """ updated = {k: dict(v) for k, v in services.items()} # First, fix root-cause services that have been remediated for svc_name in fixed_services: if svc_name in updated: updated[svc_name]["status"] = ServiceStatus.HEALTHY # Iteratively propagate health (max 5 rounds to handle chains) for _ in range(5): changed = False for svc_name, deps in dependencies.items(): if svc_name in fixed_services: continue if svc_name in root_cause_map and svc_name not in fixed_services: continue # still broken if not deps: continue dep_statuses = [updated[d]["status"] for d in deps if d in updated] if not dep_statuses: continue if any(s == ServiceStatus.DOWN for s in dep_statuses): new_status = ServiceStatus.DEGRADED # downstream of DOWN = DEGRADED elif any(s == ServiceStatus.DEGRADED for s in dep_statuses): new_status = ServiceStatus.DEGRADED else: new_status = ServiceStatus.HEALTHY if updated[svc_name]["status"] != new_status: updated[svc_name]["status"] = new_status changed = True if not changed: break return updated def format_metrics(metrics_list: List[Dict[str, Any]]) -> str: """Format time-series metrics into a readable table.""" if not metrics_list: return "No metrics available for this service." # Get all keys from the first entry keys = list(metrics_list[0].keys()) header = " ".join(f"{k:<18}" for k in keys) lines = [header, "-" * len(header)] for row in metrics_list: vals = [] for k in keys: v = row.get(k, "") vals.append(f"{str(v):<18}") lines.append(" ".join(vals)) return "\n".join(lines) def format_logs(log_lines: List[str]) -> str: """Join log lines with newlines.""" if not log_lines: return "No logs available for this service." return "\n".join(log_lines) def format_traces(trace_lines: List[str]) -> str: """Format trace data.""" if not trace_lines: return "No traces available for this service." return "\n".join(trace_lines) def format_deploy_history(deploy_lines: List[str]) -> str: """Format deploy history.""" if not deploy_lines: return "No deploy history available for this service." return "\n".join(deploy_lines) def format_dependencies(deps: List[str]) -> str: """Format dependency list.""" if not deps: return "This service has no upstream dependencies." return "Dependencies: " + ", ".join(deps) def format_runbook(runbook: str) -> str: """Return runbook text.""" if not runbook: return "No runbook available for this service." return runbook def format_config_diff(config_data: Dict[str, str]) -> str: """Format config diff.""" if not config_data: return "No config data available for this service." result = [] if "diff" in config_data: result.append(f"Config diff: {config_data['diff']}") if "current" in config_data: result.append(f"\nCurrent config:\n{config_data['current']}") return "\n".join(result) def ping_service(status: ServiceStatus, service_name: str) -> str: """Simulate a ping to a service.""" if status == ServiceStatus.HEALTHY: return f"PING {service_name}: responding on :8080/healthz — 200 OK (latency: 5ms)" elif status == ServiceStatus.DEGRADED: return f"PING {service_name}: responding on :8080/healthz — 200 OK (latency: 1200ms, SLOW)" else: return f"PING {service_name}: connection refused on :8080/healthz — service unreachable"