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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user