feat: ingest HTTP 201/409 for event_id idempotency

- Validate UUID format; log created/duplicate/rejected

- Tests for 201, 409, 422; update agent-integration and work-plan

- Docs: neutral IDE wording (no product-specific editor names)
This commit is contained in:
2026-05-28 09:13:12 +10:00
parent 5cf76f492c
commit 2b76ae8497
11 changed files with 210 additions and 26 deletions
+31 -11
View File
@@ -1,7 +1,9 @@
import logging
from datetime import datetime
from typing import Any
from fastapi import APIRouter, Body, Depends, HTTPException, Query
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from sqlalchemy import func, select
from sqlalchemy.orm import Session, joinedload
@@ -18,6 +20,7 @@ from app.services.schema_validate import validate_event_payload
from app.services.telegram_notify import notify_event, notify_problem
router = APIRouter(prefix="/events", tags=["events"])
logger = logging.getLogger("sac.ingest")
class IngestResponse(BaseModel):
@@ -28,14 +31,32 @@ class IngestResponse(BaseModel):
problem_id: int | None = None
@router.post("", status_code=202, response_model=IngestResponse)
def _ingest_response(event: Event, *, created: bool) -> IngestResponse:
settings = get_settings()
base = settings.sac_public_url.rstrip("/")
return IngestResponse(
status="created" if created else "duplicate",
event_id=event.event_id,
created=created,
sac_event_url=f"{base}/api/v1/events/{event.id}",
problem_id=None,
)
@router.post("", response_model=IngestResponse)
def post_event(
payload: dict[str, Any] = Body(...),
db: Session = Depends(get_db),
_api_key: str = Depends(get_api_key_auth),
) -> IngestResponse:
) -> JSONResponse:
errors = validate_event_payload(payload)
if errors:
event_id_hint = payload.get("event_id") if isinstance(payload.get("event_id"), str) else None
logger.warning(
"ingest rejected status=422 event_id=%s errors=%s",
event_id_hint,
errors[:5],
)
raise HTTPException(status_code=422, detail={"schema_errors": errors[:20]})
event, created = ingest_event(db, payload)
@@ -46,17 +67,16 @@ def post_event(
notify_event(event)
if problem is not None and problem_created:
notify_problem(problem, event)
logger.info("ingest created event_id=%s type=%s host_id=%s", event.event_id, event.type, event.host_id)
else:
logger.info("ingest duplicate event_id=%s", event.event_id)
db.commit()
settings = get_settings()
base = settings.sac_public_url.rstrip("/")
return IngestResponse(
status="accepted",
event_id=event.event_id,
created=created,
sac_event_url=f"{base}/api/v1/events/{event.id}",
problem_id=problem.id if problem else None,
)
body = _ingest_response(event, created=created)
if problem is not None:
body.problem_id = problem.id
status_code = 201 if created else 409
return JSONResponse(status_code=status_code, content=body.model_dump(mode="json"))
def _parse_optional_dt(value: str | None) -> datetime | None: