bclermo's picture
Upload folder using huggingface_hub
00c1e8f verified
Raw
History Blame Contribute Delete
2.62 kB
"""Audit gateway — every inference is logged.
Audit events are stored in FHIR AuditEvent-compatible JSON format.
In production, these would be exported to a FHIR server. For MVP,
we persist to a local append-only log.
"""
from __future__ import annotations
import asyncio
import json
import logging
import time
import uuid
from pathlib import Path
from typing import Any
logger = logging.getLogger("openclinical.runtime.audit")
class AuditLogger:
"""Audit logger for clinical AI inferences.
MVP: append-only JSON file. Production: FHIR AuditEvent export to server.
"""
def __init__(self, audit_path: Path) -> None:
self.audit_path = audit_path
self.audit_path.mkdir(parents=True, exist_ok=True)
self.log_file = self.audit_path / "audit.jsonl"
self._lock = asyncio.Lock()
async def log(self, **fields: Any) -> str:
"""Log an audit event and return the event ID."""
event_id = str(uuid.uuid4())
timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
event = {
"event_id": event_id,
"timestamp": timestamp,
**fields,
}
async with self._lock:
# MVP: append to file
with open(self.log_file, "a") as f:
f.write(json.dumps(event) + "\n")
logger.info("audit event %s (%s)", event_id, fields.get("event_type", "unknown"))
return event_id
async def query(
self,
tenant_id: str | None = None,
patient_id: str | None = None,
model_id: str | None = None,
limit: int = 100,
) -> list[dict[str, Any]]:
"""Query audit events with optional filters.
tenant_id filter is mandatory for production — without it, callers
see events from all tenants which would be a multi-tenant breach.
"""
events = []
if not self.log_file.exists():
return events
with open(self.log_file) as f:
for line in f:
line = line.strip()
if not line:
continue
event = json.loads(line)
if tenant_id and event.get("tenant_id") != tenant_id:
continue
if patient_id and event.get("patient_id") != patient_id:
continue
if model_id and event.get("model_id") != model_id:
continue
events.append(event)
if len(events) >= limit:
break
# Newest first
return list(reversed(events))