fester/backend/api/autopsy.py

77 lines
2.6 KiB
Python

"""
Autopsy API — post-hoc root cause analysis for failed actions.
"""
from fastapi import APIRouter
from backend.analysis.failure_autopsy import FailureAutopsy
from backend.analysis.timeline_store import STORE as TIMELINE_STORE
from backend.api.replay import SESSION_DB
router = APIRouter(prefix="/autopsy", tags=["autopsy"])
# -----------------------------
# RUN AUTOPSY
# -----------------------------
@router.get("/{session_id}/{action}")
def run_autopsy(session_id: str, action: str):
"""Run a failure autopsy for `action` against the replay session `session_id`."""
# Try to fetch events for this session from SessionDB
events = SESSION_DB.get_replay_stream(session_id)
if not events:
# Fallback: use the live timeline store
journal = TIMELINE_STORE
else:
# Wrap the session's event list in a tiny shim that quacks like
# the TimelineStore API the FailureAutopsy expects.
journal = _JournalShim(events)
autopsy = FailureAutopsy(journal)
return autopsy.report(action)
# -----------------------------
# Legacy wrapper for api.py facade
# -----------------------------
def autopsy_endpoint(session_id, action=None):
"""Legacy entrypoint — caller passed only session_id; we look up
failures across all known sessions."""
events = SESSION_DB.get_replay_stream(session_id)
if not events:
return {"error": "session not found"}
journal = _JournalShim(events)
autopsy = FailureAutopsy(journal)
# If no action given, report on the first failure we find
if action is None:
failures = autopsy.find_failures()
if not failures:
return {"status": "no_failures_in_session", "session_id": session_id}
action = failures[0].get("data", failures[0]).get("action")
if not action:
return {"status": "no_actionable_failure", "session_id": session_id}
return autopsy.report(action)
class _JournalShim:
"""Minimal adapter that lets FailureAutopsy work against a plain list
of events (from a replay session) instead of a full TimelineStore."""
def __init__(self, events):
self.events = events
def find_failures(self):
return [
e for e in self.events
if e.get("type") in ("failure", "task_update", "pipeline_update")
and (e.get("state") == "failed" or e.get("data", {}).get("state") == "failed")
]
def trace_action(self, action_name):
return [
e for e in self.events
if (e.get("action") == action_name) or (e.get("data", {}).get("action") == action_name)
]