104 lines
3.4 KiB
Python
104 lines
3.4 KiB
Python
import base64
|
|
import binascii
|
|
from datetime import datetime
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from sqlalchemy import and_, or_, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from ..db import get_session
|
|
from ..models import Event
|
|
from ..schemas import (
|
|
ID_PATTERN,
|
|
EventBatchAcceptedResponse,
|
|
EventBatchRequest,
|
|
EventsPage,
|
|
StoredCheckResultEvent,
|
|
)
|
|
from ..services.ingestion import ingest_event
|
|
from ..timeutil import to_utc
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _encode_cursor(received_at: datetime, event_id) -> str:
|
|
raw = f"{to_utc(received_at).isoformat()}|{event_id}".encode()
|
|
return base64.urlsafe_b64encode(raw).decode().rstrip("=")
|
|
|
|
|
|
def _decode_cursor(cursor: str) -> tuple[datetime, str]:
|
|
pad = "=" * (-len(cursor) % 4)
|
|
try:
|
|
raw = base64.urlsafe_b64decode(cursor + pad).decode()
|
|
ts_str, eid = raw.split("|", 1)
|
|
return to_utc(datetime.fromisoformat(ts_str)), eid
|
|
except (binascii.Error, ValueError) as exc:
|
|
raise HTTPException(status_code=400, detail="invalid cursor") from exc
|
|
|
|
|
|
@router.post(
|
|
"/events",
|
|
response_model=EventBatchAcceptedResponse,
|
|
status_code=status.HTTP_202_ACCEPTED,
|
|
)
|
|
async def events(
|
|
body: EventBatchRequest, session: AsyncSession = Depends(get_session)
|
|
) -> EventBatchAcceptedResponse:
|
|
accepted = 0
|
|
deduplicated = 0
|
|
for ev in body.events:
|
|
ok = await ingest_event(session, body.agent_id, ev)
|
|
if ok:
|
|
accepted += 1
|
|
else:
|
|
deduplicated += 1
|
|
await session.commit()
|
|
return EventBatchAcceptedResponse(accepted=accepted, deduplicated=deduplicated)
|
|
|
|
|
|
@router.get("/events/query", response_model=EventsPage)
|
|
async def query_events(
|
|
agent_id: str | None = Query(default=None, max_length=128, pattern=ID_PATTERN),
|
|
check_id: str | None = Query(default=None, max_length=128, pattern=ID_PATTERN),
|
|
cursor: str | None = Query(default=None, max_length=512),
|
|
limit: int = Query(default=100, ge=1, le=500),
|
|
session: AsyncSession = Depends(get_session),
|
|
) -> EventsPage:
|
|
stmt = select(Event).order_by(Event.received_at.desc(), Event.event_id.desc())
|
|
if agent_id is not None:
|
|
stmt = stmt.where(Event.agent_id == agent_id)
|
|
if check_id is not None:
|
|
stmt = stmt.where(Event.check_id == check_id)
|
|
if cursor is not None:
|
|
ts, eid = _decode_cursor(cursor)
|
|
stmt = stmt.where(
|
|
or_(
|
|
Event.received_at < ts,
|
|
and_(Event.received_at == ts, Event.event_id < eid),
|
|
)
|
|
)
|
|
stmt = stmt.limit(limit + 1)
|
|
rows = (await session.execute(stmt)).scalars().all()
|
|
next_cursor = None
|
|
if len(rows) > limit:
|
|
rows = rows[:limit]
|
|
next_cursor = _encode_cursor(rows[-1].received_at, rows[-1].event_id)
|
|
items = [
|
|
StoredCheckResultEvent(
|
|
event_id=str(r.event_id),
|
|
agent_id=r.agent_id,
|
|
check_id=r.check_id,
|
|
observed_at=to_utc(r.observed_at),
|
|
received_at=to_utc(r.received_at),
|
|
status=r.status,
|
|
exit_code=r.exit_code,
|
|
duration_ms=r.duration_ms,
|
|
output=r.output,
|
|
output_truncated=r.output_truncated,
|
|
notifications_enabled=r.notifications_enabled,
|
|
incident_key=r.incident_key,
|
|
)
|
|
for r in rows
|
|
]
|
|
return EventsPage(items=items, next_cursor=next_cursor)
|