Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a05e2e-805b-7042-a750-71f473bceea2
100 lines
3.3 KiB
Python
100 lines
3.3 KiB
Python
"""Asynchronous delivery of the transactional outbox to audit-core."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import threading
|
|
from pathlib import Path
|
|
from typing import Any, Callable
|
|
from urllib.error import HTTPError, URLError
|
|
from urllib.request import Request, urlopen
|
|
|
|
from .store import Engine
|
|
|
|
|
|
class AuditDeliveryError(RuntimeError):
|
|
"""Bounded delivery failure; details deliberately exclude response bodies."""
|
|
|
|
|
|
def audit_envelope(payload: dict[str, Any]) -> dict[str, Any]:
|
|
details = dict(payload.get("details") or {})
|
|
return {
|
|
"id": payload["event_id"],
|
|
"type": payload["action"],
|
|
"source": payload["source"],
|
|
"subject": payload["resource"],
|
|
"tenant": payload["tenant"],
|
|
"correlation_id": details.get("approval_id") or payload["event_id"],
|
|
"occurred_at": payload["observed_at"],
|
|
"data": {
|
|
"schema_version": payload["schema_version"],
|
|
"scope": payload["scope"],
|
|
"actor": payload.get("actor"),
|
|
"resource": payload["resource"],
|
|
"outcome": payload["outcome"],
|
|
"reason": payload.get("reason"),
|
|
"details": details,
|
|
},
|
|
}
|
|
|
|
|
|
class AuditCoreSink:
|
|
def __init__(
|
|
self,
|
|
base_url: str,
|
|
token_file: str | Path,
|
|
*,
|
|
timeout_seconds: float = 5,
|
|
opener: Callable[..., Any] = urlopen,
|
|
) -> None:
|
|
self.url = base_url.rstrip("/") + "/v1/events"
|
|
self.token_file = Path(token_file)
|
|
self.timeout_seconds = timeout_seconds
|
|
self.opener = opener
|
|
|
|
def __call__(self, payload: dict[str, Any]) -> None:
|
|
token = self.token_file.read_text(encoding="utf-8").strip()
|
|
if not token or any(ch.isspace() for ch in token):
|
|
raise AuditDeliveryError("audit credential is unavailable")
|
|
body = json.dumps(audit_envelope(payload), sort_keys=True).encode("utf-8")
|
|
request = Request(
|
|
self.url,
|
|
data=body,
|
|
method="POST",
|
|
headers={
|
|
"Authorization": f"Bearer {token}",
|
|
"Content-Type": "application/json",
|
|
"Idempotency-Key": payload["event_id"],
|
|
},
|
|
)
|
|
try:
|
|
response = self.opener(request, timeout=self.timeout_seconds)
|
|
status = int(response.getcode())
|
|
response.close()
|
|
except (HTTPError, URLError, OSError) as exc:
|
|
raise AuditDeliveryError(type(exc).__name__) from exc
|
|
if status not in {200, 202}:
|
|
raise AuditDeliveryError(f"audit ingest returned status {status}")
|
|
|
|
|
|
class OutboxWorker:
|
|
def __init__(
|
|
self,
|
|
engine: Engine,
|
|
sink: Callable[[dict[str, Any]], None],
|
|
*,
|
|
heartbeat_interval_seconds: int = 300,
|
|
) -> None:
|
|
self.engine = engine
|
|
self.sink = sink
|
|
self.heartbeat_interval_seconds = heartbeat_interval_seconds
|
|
|
|
def run_once(self) -> dict[str, int]:
|
|
if self.engine.heartbeat_due(self.heartbeat_interval_seconds):
|
|
self.engine.emit_heartbeat()
|
|
return self.engine.drain(self.sink)
|
|
|
|
def run_forever(self, stop: threading.Event, poll_seconds: float = 5) -> None:
|
|
while not stop.is_set():
|
|
self.run_once()
|
|
stop.wait(poll_seconds)
|