from __future__ import annotations import logging import uuid from fastapi import FastAPI, Header, HTTPException, Request, status from app.config import settings from app.forwarder import forward_to_processor from app.models import NormalizedEvent, ProcessorForwardEnvelope, ReceiverAck from app.normalizer import normalize_zabbix_payload logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s", ) logger = logging.getLogger(__name__) app = FastAPI( title="alert-receiver", version="0.1.0", description="Receives Zabbix webhooks and normalizes them for alert-processor.", ) @app.on_event("startup") async def startup_check() -> None: if settings.require_webhook_token and not settings.webhook_token: raise RuntimeError( "WEBHOOK_TOKEN is required, but not set. " "Set it in environment variables or in .env file." ) logger.info( "Startup configuration loaded: require_webhook_token=%s forward_to_processor=%s", settings.require_webhook_token, settings.forward_to_processor, ) def _extract_token( x_webhook_token: str | None, authorization: str | None, ) -> str | None: if x_webhook_token: return x_webhook_token.strip() if authorization: auth = authorization.strip() if auth.lower().startswith("bearer "): return auth[7:].strip() return auth return None def _extract_token( x_webhook_token: str | None, authorization: str | None, ) -> str | None: if x_webhook_token: return x_webhook_token.strip() if authorization: auth = authorization.strip() if auth.lower().startswith("bearer "): return auth[7:].strip() return auth return None def _validate_token( x_webhook_token: str | None, authorization: str | None, ) -> None: if not settings.require_webhook_token: return provided = _extract_token(x_webhook_token, authorization) if not provided or provided != settings.webhook_token: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or missing webhook token", ) @app.get("/health") async def health() -> dict[str, str]: return { "status": "ok", "service": settings.app_name, } @app.post( "/webhook/zabbix", response_model=ReceiverAck, status_code=status.HTTP_202_ACCEPTED, ) async def receive_zabbix_webhook( request: Request, x_webhook_token: str | None = Header(default=None), authorization: str | None = Header(default=None), x_correlation_id: str | None = Header(default=None), ) -> ReceiverAck: _validate_token(x_webhook_token, authorization) try: payload = await request.json() except Exception as exc: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid JSON payload: {exc}", ) from exc if not isinstance(payload, dict): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Payload must be a JSON object", ) correlation_id = x_correlation_id or str(uuid.uuid4()) remote_addr = request.client.host if request.client else None normalized_dict = normalize_zabbix_payload( payload=payload, correlation_id=correlation_id, remote_addr=remote_addr, ) event = NormalizedEvent.model_validate(normalized_dict) envelope = ProcessorForwardEnvelope(event=event) logger.info( "Accepted Zabbix event: correlation_id=%s event_id=%s severity=%s host=%s trigger=%s", event.correlation_id, event.event_id, event.severity, event.host, event.trigger_name, ) forwarded = await forward_to_processor(envelope) return ReceiverAck( accepted=True, correlation_id=event.correlation_id, forwarded_to_processor=forwarded, message="Webhook accepted", )