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