"""Public flight recorder interfaces.""" from typing import Any, Protocol from shared.contracts import FdrEvent from shared.errors import ErrorEnvelope from .types import ( FdrAppendResult, FdrExportRequest, FdrExportResult, FdrHealth, FdrPayload, FdrSegmentSummary, ) class FlightRecorder(Protocol): """Append-only event recorder for runtime evidence.""" def append_event(self, event: Any) -> None: """Persist one FDR event.""" def export(self) -> Any: """Export recorded evidence for post-flight analysis.""" class InMemoryFlightRecorder: """Bounded append-only recorder for runtime evidence metadata.""" def __init__(self, segment_limit_bytes: int, storage_limit_bytes: int) -> None: if segment_limit_bytes <= 0: raise ValueError("segment_limit_bytes must be positive") if storage_limit_bytes < segment_limit_bytes: raise ValueError("storage_limit_bytes must be at least one segment") self._segment_limit_bytes = segment_limit_bytes self._storage_limit_bytes = storage_limit_bytes self._segments: list[list[FdrEvent]] = [[]] self._segment_bytes: list[int] = [0] self._used_bytes = 0 @property def health(self) -> FdrHealth: if self._used_bytes >= self._storage_limit_bytes: return FdrHealth( status="critical", used_bytes=self._used_bytes, max_bytes=self._storage_limit_bytes, message="fdr storage limit reached", ) if self._used_bytes >= int(self._storage_limit_bytes * 0.9): return FdrHealth( status="degraded", used_bytes=self._used_bytes, max_bytes=self._storage_limit_bytes, message="fdr storage nearing limit", ) return FdrHealth( status="ready", used_bytes=self._used_bytes, max_bytes=self._storage_limit_bytes, message="fdr storage ready", ) def append_event(self, event: FdrEvent, payload: FdrPayload) -> FdrAppendResult: if self._used_bytes + payload.size_bytes > self._storage_limit_bytes: return FdrAppendResult( appended=False, error=ErrorEnvelope( component="fdr_observability", category="resource", message="fdr storage limit reached", severity="critical", retryable=False, ), ) rollover = False if self._segment_bytes[-1] + payload.size_bytes > self._segment_limit_bytes: self._segments.append([]) self._segment_bytes.append(0) rollover = True segment_index = len(self._segments) - 1 stored_event = event.model_copy(update={"payload_ref": payload.ref}) self._segments[segment_index].append(stored_event) self._segment_bytes[segment_index] += payload.size_bytes self._used_bytes += payload.size_bytes return FdrAppendResult( appended=True, event=stored_event, segment_id=self._segment_id(segment_index), rollover=rollover, ) def export(self, request: FdrExportRequest) -> FdrExportResult: segments = tuple( FdrSegmentSummary( segment_id=self._segment_id(index), event_count=len(events), bytes_used=self._segment_bytes[index], ) for index, events in enumerate(self._segments) if events ) evidence_ref = f"fdr://exports/{request.mission_id}/{request.run_id}/evidence.json" analytics_ref = ( f"fdr://exports/{request.mission_id}/{request.run_id}/analytics.parquet" if request.include_analytics else None ) return FdrExportResult( produced=True, evidence_ref=evidence_ref, segments=segments, analytics_ref=analytics_ref, ) def _segment_id(self, index: int) -> str: return f"segment-{index + 1:04d}"