Netdata.cloud bot for Zulip
1"""FastAPI webhook server for receiving Netdata notifications.""" 2 3from typing import Dict, Any 4 5import structlog 6import uvicorn 7from fastapi import FastAPI, HTTPException, Request, status 8from fastapi.responses import JSONResponse 9 10from .formatter import ZulipMessageFormatter 11from .models import WebhookPayload, ZulipConfig, ServerConfig 12from .zulip_client import ZulipNotifier 13 14logger = structlog.get_logger() 15 16 17class NetdataWebhookServer: 18 """FastAPI server for handling Netdata Cloud webhooks.""" 19 20 def __init__(self, zulip_config: ZulipConfig, server_config: ServerConfig): 21 """Initialize the webhook server.""" 22 self.app = FastAPI( 23 title="Netdata Zulip Bot", 24 description="Webhook service for Netdata Cloud notifications", 25 version="0.1.0" 26 ) 27 self.zulip_config = zulip_config 28 self.server_config = server_config 29 self.formatter = ZulipMessageFormatter() 30 31 # Initialize Zulip client 32 try: 33 self.zulip_notifier = ZulipNotifier(zulip_config) 34 except Exception as e: 35 logger.error("Failed to initialize Zulip client", error=str(e)) 36 raise 37 38 self._setup_routes() 39 self._setup_middleware() 40 41 def _setup_routes(self): 42 """Setup FastAPI routes.""" 43 44 @self.app.get("/health") 45 async def health_check(): 46 """Health check endpoint.""" 47 return {"status": "healthy", "service": "netdata-zulip-bot"} 48 49 @self.app.post("/webhook/netdata") 50 async def netdata_webhook(request: Request): 51 """Handle Netdata Cloud webhook notifications.""" 52 try: 53 # Get raw JSON data 54 body = await request.json() 55 logger.info("Received webhook", payload_keys=list(body.keys())) 56 57 # Parse and validate the payload 58 notification = WebhookPayload.parse(body) 59 logger.info("Parsed notification", type=type(notification).__name__) 60 61 # Format message for Zulip 62 topic, content = self.formatter.format_notification(notification) 63 logger.info("Formatted message", topic=topic) 64 65 # Send to Zulip 66 success = self.zulip_notifier.send_message(topic, content) 67 68 if success: 69 return {"status": "success", "message": "Notification sent to Zulip"} 70 else: 71 raise HTTPException( 72 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 73 detail="Failed to send notification to Zulip" 74 ) 75 76 except ValueError as e: 77 logger.error("Invalid webhook payload", error=str(e)) 78 raise HTTPException( 79 status_code=status.HTTP_400_BAD_REQUEST, 80 detail=f"Invalid payload format: {str(e)}" 81 ) 82 except Exception as e: 83 logger.error("Webhook processing failed", error=str(e)) 84 raise HTTPException( 85 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 86 detail="Internal server error" 87 ) 88 89 def _setup_middleware(self): 90 """Setup middleware for logging and error handling.""" 91 92 @self.app.middleware("http") 93 async def log_requests(request: Request, call_next): 94 """Log all requests.""" 95 client_host = request.client.host if request.client else "unknown" 96 logger.info( 97 "Request received", 98 method=request.method, 99 url=str(request.url), 100 client=client_host 101 ) 102 103 try: 104 response = await call_next(request) 105 logger.info( 106 "Request completed", 107 method=request.method, 108 url=str(request.url), 109 status_code=response.status_code 110 ) 111 return response 112 except Exception as e: 113 logger.error( 114 "Request failed", 115 method=request.method, 116 url=str(request.url), 117 error=str(e) 118 ) 119 raise 120 121 122 def run(self): 123 """Run the webhook server (HTTP only, TLS handled by reverse proxy).""" 124 try: 125 logger.info( 126 "Starting Netdata Zulip webhook server (HTTP)", 127 host=self.server_config.host, 128 port=self.server_config.port 129 ) 130 131 uvicorn.run( 132 self.app, 133 host=self.server_config.host, 134 port=self.server_config.port, 135 access_log=False, # We handle logging in middleware 136 ) 137 138 except Exception as e: 139 logger.error("Failed to start server", error=str(e)) 140 raise