feat(sofiia-console): add audit query endpoint with cursor pagination

Made-with: Cursor
This commit is contained in:
Apple
2026-03-02 09:36:11 -08:00
parent 9e70fc83d2
commit 11e0ba7264
3 changed files with 145 additions and 2 deletions

View File

@@ -3587,7 +3587,7 @@ async def api_chat_send_v2(chat_id: str, body: ChatMessageSendBody, request: Req
)
await audit_log(
AuditEvent(
event="chat.send.result",
event="chat.send.error",
operator_id=operator_id,
operator_id_missing=operator_id_missing,
ip=(request.client.host if request.client else None),
@@ -3627,7 +3627,7 @@ async def api_chat_send_v2(chat_id: str, body: ChatMessageSendBody, request: Req
)
await audit_log(
AuditEvent(
event="chat.send.result",
event="chat.send.error",
operator_id=operator_id,
operator_id_missing=operator_id_missing,
ip=(request.client.host if request.client else None),
@@ -3719,6 +3719,63 @@ async def api_chat_send_v2(chat_id: str, body: ChatMessageSendBody, request: Req
return result
@app.get("/api/audit")
async def api_audit_list(
chat_id: Optional[str] = Query(None),
operator_id: Optional[str] = Query(None),
event: Optional[str] = Query(None),
status: Optional[str] = Query(None),
node_id: Optional[str] = Query(None),
limit: int = Query(50, ge=1, le=200),
cursor: Optional[str] = Query(None),
_auth: str = Depends(require_auth),
):
SOFIIA_CURSOR_REQUESTS_TOTAL.labels(resource="audit").inc()
cur = _cursor_decode(cursor)
before_ts = str(cur.get("ts") or "").strip() or None
before_id = str(cur.get("id") or "").strip() or None
rows = await _app_db.list_audit_events_page(
event=(event or None),
operator_id=(operator_id or None),
status=(status or None),
node_id=(node_id or None),
chat_id=(chat_id or None),
limit=limit + 1,
before_ts=before_ts,
before_id=before_id,
)
has_more = len(rows) > limit
page = rows[:limit]
next_cursor = None
if has_more and page:
tail = page[-1]
next_cursor = _cursor_encode({"ts": tail.get("ts"), "id": tail.get("id")})
items = [
{
"id": r.get("id"),
"ts": r.get("ts"),
"event": r.get("event"),
"operator_id": r.get("operator_id"),
"operator_id_missing": bool(r.get("operator_id_missing")),
"ip": r.get("ip"),
"chat_id": r.get("chat_id"),
"node_id": r.get("node_id"),
"agent_id": r.get("agent_id"),
"status": r.get("status"),
"error_code": r.get("error_code"),
"duration_ms": r.get("duration_ms"),
"data": r.get("data_json") or {},
}
for r in page
]
return {
"items": items,
"has_more": has_more,
"next_cursor": next_cursor,
}
@app.get("/metrics")
def metrics():
data, content_type = render_metrics()