feat: add audit events and runtime state snapshots to database
- Introduced new tables for audit events and runtime state snapshots in the database schema. - Created data classes for AuditRecord and RuntimeStateRecord to represent the new entities. - Implemented AuditRepository and RuntimeStateRepository for inserting and retrieving records. - Enhanced the dashboard to include an audit trail section, displaying recent audit events. - Added tests for the new audit repository and runtime lifecycle functionalities. - Updated settings validation to ensure proper configuration for alerting features. - Integrated alert notifications across various components, including execution sequencer and loss limits.
This commit is contained in:
@@ -66,6 +66,27 @@ class PnLRecord:
|
||||
source: str
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AuditRecord:
|
||||
occurred_at: datetime
|
||||
actor: str
|
||||
event_type: str
|
||||
decision: str
|
||||
payload: dict[str, Any] | None = None
|
||||
correlation_id: str | None = None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RuntimeStateRecord:
|
||||
snapshot_at: datetime
|
||||
is_running: bool
|
||||
kill_switch_active: bool
|
||||
kill_switch_reason: str | None
|
||||
open_trade_count: int
|
||||
last_known_balances: dict[str, Any] | None = None
|
||||
note: str | None = None
|
||||
|
||||
|
||||
class MarketSnapshotRepository:
|
||||
def __init__(self, store: DuckDBStore) -> None:
|
||||
self._store = store
|
||||
@@ -217,3 +238,141 @@ class PnLRepository:
|
||||
record.source,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class AuditRepository:
|
||||
def __init__(self, store: DuckDBStore) -> None:
|
||||
self._store = store
|
||||
|
||||
def insert(self, record: AuditRecord) -> None:
|
||||
with self._store.connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO audit_events (
|
||||
occurred_at,
|
||||
actor,
|
||||
event_type,
|
||||
decision,
|
||||
payload,
|
||||
correlation_id
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
[
|
||||
record.occurred_at,
|
||||
record.actor,
|
||||
record.event_type,
|
||||
record.decision,
|
||||
(
|
||||
None
|
||||
if record.payload is None
|
||||
else orjson.dumps(record.payload).decode("utf-8")
|
||||
),
|
||||
record.correlation_id,
|
||||
],
|
||||
)
|
||||
|
||||
def list_recent(self, *, limit: int = 25) -> list[AuditRecord]:
|
||||
with self._store.connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT occurred_at, actor, event_type, decision, payload, correlation_id
|
||||
FROM audit_events
|
||||
ORDER BY occurred_at DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
[limit],
|
||||
).fetchall()
|
||||
|
||||
records: list[AuditRecord] = []
|
||||
for row in rows:
|
||||
payload: dict[str, Any] | None = None
|
||||
raw_payload = row[4]
|
||||
if isinstance(raw_payload, str) and raw_payload:
|
||||
decoded = orjson.loads(raw_payload)
|
||||
if isinstance(decoded, dict):
|
||||
payload = {str(k): decoded[k] for k in decoded}
|
||||
|
||||
records.append(
|
||||
AuditRecord(
|
||||
occurred_at=row[0],
|
||||
actor=str(row[1]),
|
||||
event_type=str(row[2]),
|
||||
decision=str(row[3]),
|
||||
payload=payload,
|
||||
correlation_id=str(row[5]) if row[5] is not None else None,
|
||||
)
|
||||
)
|
||||
|
||||
return records
|
||||
|
||||
|
||||
class RuntimeStateRepository:
|
||||
def __init__(self, store: DuckDBStore) -> None:
|
||||
self._store = store
|
||||
|
||||
def insert(self, record: RuntimeStateRecord) -> None:
|
||||
with self._store.connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO runtime_state_snapshots (
|
||||
snapshot_at,
|
||||
is_running,
|
||||
kill_switch_active,
|
||||
kill_switch_reason,
|
||||
open_trade_count,
|
||||
last_known_balances,
|
||||
note
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
[
|
||||
record.snapshot_at,
|
||||
record.is_running,
|
||||
record.kill_switch_active,
|
||||
record.kill_switch_reason,
|
||||
record.open_trade_count,
|
||||
(
|
||||
None
|
||||
if record.last_known_balances is None
|
||||
else orjson.dumps(record.last_known_balances).decode("utf-8")
|
||||
),
|
||||
record.note,
|
||||
],
|
||||
)
|
||||
|
||||
def latest(self) -> RuntimeStateRecord | None:
|
||||
with self._store.connect() as conn:
|
||||
row = conn.execute("""
|
||||
SELECT
|
||||
snapshot_at,
|
||||
is_running,
|
||||
kill_switch_active,
|
||||
kill_switch_reason,
|
||||
open_trade_count,
|
||||
last_known_balances,
|
||||
note
|
||||
FROM runtime_state_snapshots
|
||||
ORDER BY snapshot_at DESC
|
||||
LIMIT 1
|
||||
""").fetchone()
|
||||
|
||||
if row is None:
|
||||
return None
|
||||
|
||||
balances: dict[str, Any] | None = None
|
||||
raw_balances = row[5]
|
||||
if isinstance(raw_balances, str) and raw_balances:
|
||||
decoded = orjson.loads(raw_balances)
|
||||
if isinstance(decoded, dict):
|
||||
balances = {str(key): decoded[key] for key in decoded}
|
||||
|
||||
return RuntimeStateRecord(
|
||||
snapshot_at=row[0],
|
||||
is_running=bool(row[1]),
|
||||
kill_switch_active=bool(row[2]),
|
||||
kill_switch_reason=str(row[3]) if row[3] is not None else None,
|
||||
open_trade_count=int(row[4]),
|
||||
last_known_balances=balances,
|
||||
note=str(row[6]) if row[6] is not None else None,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user