symfonic.services.shadow.recorder¶
recorder ¶
Recording capture — admission first, bytes second.
The recorder is deliberately shaped so that not recording is the easy path.
begin returns a session or None; a caller that ignores the return value
records nothing, which is the safe failure. Nothing here can raise into the
invocation being served: a refusal is a CaptureOutcome with a reason.
CaptureOutcome
dataclass
¶
Admitted (with a session) or refused (with a reason). Never both.
Recorder ¶
Recorder(*, policy: CaptureAdmissionPolicy, store: RecordingStore, actor: str, mode: RecordingMode = RecordingMode.SYNTHETIC, clock: Callable[[], datetime] | None = None)
Binds the admission policy to the governed store.
Source code in src/symfonic/services/shadow/recorder.py
begin ¶
Admit or refuse. A refusal means the invocation proceeds unrecorded.
Source code in src/symfonic/services/shadow/recorder.py
commit ¶
Seal and store. Any capture failure leaves the invocation unrecorded.
The refusal is deliberately broad. Serialization is the obvious way a
commit fails for a reason the store never sees — an allowlisted leaf
holding a datetime makes json.dumps raise TypeError, not
RecordingStoreError — and a cipher or backend can fail its own way
too. All of them are capture failures, and the contract is that
capture failure never reaches the invocation being served. The double
commit above still raises: that is a caller bug, not a capture failure.
Source code in src/symfonic/services/shadow/recorder.py
RecordingSession
dataclass
¶
RecordingSession(recording_id: str, tenant_id: str, mode: RecordingMode, decision: CaptureDecision, captured_at: datetime, events: list[RecordedEvent] = list(), extensions: list[str] = list(), committed: bool = False, project_response: Callable[[Any], ResponseProjection] | None = None, response_dropped: list[str] = list(), response_redacted: list[str] = list())
Accumulates events for one invocation, then commits once.
A session is normally built by :class:Recorder, which binds the
admission policy's response projection to it. A session built without one
still governs its answers — the fallback scrubs credential-shaped keys and
minimizes — because "nobody wired a projector" must not mean "write the
provider's answer verbatim".
observe ¶
observe(port_id: str, operation: str, request: Any, response: Any, *, family: str = '') -> RecordedEvent
Record one non-idempotent port crossing and its governed answer.
The request never enters the recording — only its digest — and the answer goes through the same governance as the payload before any of it is serialized.