95 lines
3.3 KiB
Python
95 lines
3.3 KiB
Python
"""Single entry point for writing :class:`AuditLog` rows.
|
|
|
|
Every frontend ends up needing the same three-line pattern — open a DB
|
|
session, append a row, swallow failures so the user-visible request
|
|
still succeeds. Phase 4.3 inlined that pattern in the admin frontend
|
|
under a private ``_audit()`` helper; Phase 4.4 lifts it here so the
|
|
Messages and MCP frontends can call the same function and so the
|
|
swallow-and-log policy lives in one place.
|
|
|
|
The contract:
|
|
|
|
* ``log(runtime, actor=..., kind=...)`` is fire-and-forget. It awaits
|
|
the DB write (so callers can ``await`` it before responding and get
|
|
ordering), but never raises — if the audit insert fails, the function
|
|
emits an ``exception`` log line and returns.
|
|
* ``actor`` is a free-form string. By convention: ``"token:<name>"``
|
|
for bearer-authenticated traffic, ``"admin:<user>"`` for admin-UI
|
|
actions, ``"anon"`` for failed-auth paths we still want to record.
|
|
* ``kind`` is a short tag — see :data:`KNOWN_KINDS` for the set the
|
|
current frontends emit; new tags don't need a code change here, the
|
|
column is free-form.
|
|
* ``**detail`` is JSON-serialised by :func:`append_audit`. Keep it
|
|
small: paths, methods, status codes — not request bodies. Anything
|
|
passed here lands in ``AuditLog.detail_json`` verbatim.
|
|
|
|
Why a thin wrapper rather than ``append_audit`` directly: callers want
|
|
"write if you can, otherwise carry on", and pulling the try/except into
|
|
every frontend was already starting to drift (admin had it, bearer
|
|
frontends would have copy-pasted). One module, one policy.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from beaver_gateway.storage import append_audit
|
|
|
|
if TYPE_CHECKING:
|
|
from beaver_gateway.frontends.base import GatewayRuntime
|
|
|
|
|
|
_log = logging.getLogger("beaver_gateway.audit")
|
|
|
|
|
|
# Tags currently emitted by the gateway. The set is informational —
|
|
# ``AuditLog.kind`` is free-form so new code can introduce new tags
|
|
# without touching this list — but listing them here gives the admin UI
|
|
# and any downstream log consumers one canonical reference.
|
|
KNOWN_KINDS: frozenset[str] = frozenset(
|
|
{
|
|
"messages", # POST /v1/messages accepted
|
|
"mcp_call", # /mcp/<ns>/... proxied
|
|
"tool_call", # a model's tool call seen by the PreToolUse hook
|
|
"login_ok",
|
|
"login_failed",
|
|
"logout",
|
|
"token_create",
|
|
"token_revoke",
|
|
}
|
|
)
|
|
|
|
|
|
async def log(
|
|
runtime: GatewayRuntime,
|
|
*,
|
|
actor: str,
|
|
kind: str,
|
|
agent_name: str | None = None,
|
|
**detail: Any,
|
|
) -> None:
|
|
"""Best-effort audit insert. Never raises.
|
|
|
|
Opens its own short-lived :class:`AsyncSession` so callers don't
|
|
have to thread one through. If the DB hiccups (table missing,
|
|
disk full, connection drop), we log and move on — the audit trail
|
|
is observability, not a hard precondition for serving the request.
|
|
"""
|
|
try:
|
|
async with runtime.db.session() as session:
|
|
await append_audit(
|
|
session,
|
|
actor=actor,
|
|
kind=kind,
|
|
agent_name=agent_name,
|
|
detail=detail or None,
|
|
)
|
|
except Exception: # noqa: BLE001
|
|
_log.exception(
|
|
"audit write failed: actor=%s kind=%s agent=%s", actor, kind, agent_name
|
|
)
|
|
|
|
|
|
__all__ = ["KNOWN_KINDS", "log"]
|