pulseengine_mcp_server/
alerting_endpoint.rs1use axum::{
4 Router,
5 extract::{Path, State},
6 http::StatusCode,
7 response::{IntoResponse, Json},
8 routing::{get, post},
9};
10use pulseengine_mcp_logging::{AlertManager, AlertSeverity, AlertState};
11use serde::{Deserialize, Serialize};
12use std::sync::Arc;
13use uuid::Uuid;
14
15pub struct AlertingState {
17 pub alert_manager: Arc<AlertManager>,
18}
19
20#[derive(Debug, Serialize, Deserialize)]
22pub struct AlertSummary {
23 pub total_active: usize,
24 pub by_severity: std::collections::HashMap<AlertSeverity, usize>,
25 pub by_state: std::collections::HashMap<AlertState, usize>,
26}
27
28#[derive(Debug, Deserialize)]
30pub struct AcknowledgeRequest {
31 pub acknowledged_by: String,
32 pub comment: Option<String>,
33}
34
35#[derive(Debug, Deserialize)]
37pub struct ResolveRequest {
38 pub resolved_by: String,
39 pub comment: Option<String>,
40}
41
42pub async fn get_alert_summary(State(state): State<Arc<AlertingState>>) -> impl IntoResponse {
44 let active_alerts = state.alert_manager.get_active_alerts().await;
45
46 let mut by_severity = std::collections::HashMap::new();
47 let mut by_state = std::collections::HashMap::new();
48
49 for alert in &active_alerts {
50 *by_severity.entry(alert.severity.clone()).or_insert(0) += 1;
51 *by_state.entry(alert.state.clone()).or_insert(0) += 1;
52 }
53
54 let summary = AlertSummary {
55 total_active: active_alerts.len(),
56 by_severity,
57 by_state,
58 };
59
60 (StatusCode::OK, Json(summary))
61}
62
63pub async fn get_active_alerts(State(state): State<Arc<AlertingState>>) -> impl IntoResponse {
65 let alerts = state.alert_manager.get_active_alerts().await;
66 (StatusCode::OK, Json(alerts))
67}
68
69pub async fn get_alert_history(State(state): State<Arc<AlertingState>>) -> impl IntoResponse {
71 let history = state.alert_manager.get_alert_history().await;
72 (StatusCode::OK, Json(history))
73}
74
75pub async fn get_alert(
77 Path(alert_id): Path<Uuid>,
78 State(state): State<Arc<AlertingState>>,
79) -> impl IntoResponse {
80 let active_alerts = state.alert_manager.get_active_alerts().await;
81
82 if let Some(alert) = active_alerts.iter().find(|a| a.id == alert_id) {
83 (StatusCode::OK, Json(alert.clone())).into_response()
84 } else {
85 let history = state.alert_manager.get_alert_history().await;
87 if let Some(alert) = history.iter().find(|a| a.id == alert_id) {
88 (StatusCode::OK, Json(alert.clone())).into_response()
89 } else {
90 (
91 StatusCode::NOT_FOUND,
92 Json(serde_json::json!({
93 "error": "Alert not found",
94 "alert_id": alert_id
95 })),
96 )
97 .into_response()
98 }
99 }
100}
101
102pub async fn acknowledge_alert(
104 Path(alert_id): Path<Uuid>,
105 State(state): State<Arc<AlertingState>>,
106 Json(request): Json<AcknowledgeRequest>,
107) -> impl IntoResponse {
108 match state
109 .alert_manager
110 .acknowledge_alert(alert_id, request.acknowledged_by)
111 .await
112 {
113 Ok(()) => (
114 StatusCode::OK,
115 Json(serde_json::json!({
116 "success": true,
117 "message": "Alert acknowledged"
118 })),
119 )
120 .into_response(),
121 Err(e) => (
122 StatusCode::BAD_REQUEST,
123 Json(serde_json::json!({
124 "error": e.to_string(),
125 "alert_id": alert_id
126 })),
127 )
128 .into_response(),
129 }
130}
131
132pub async fn resolve_alert(
134 Path(alert_id): Path<Uuid>,
135 State(state): State<Arc<AlertingState>>,
136 Json(_request): Json<ResolveRequest>,
137) -> impl IntoResponse {
138 match state.alert_manager.resolve_alert(alert_id).await {
139 Ok(()) => (
140 StatusCode::OK,
141 Json(serde_json::json!({
142 "success": true,
143 "message": "Alert resolved"
144 })),
145 )
146 .into_response(),
147 Err(e) => (
148 StatusCode::BAD_REQUEST,
149 Json(serde_json::json!({
150 "error": e.to_string(),
151 "alert_id": alert_id
152 })),
153 )
154 .into_response(),
155 }
156}
157
158pub fn create_alerting_router(alert_manager: Arc<AlertManager>) -> Router {
160 let state = Arc::new(AlertingState { alert_manager });
161
162 Router::new()
163 .route("/alerts/summary", get(get_alert_summary))
164 .route("/alerts/active", get(get_active_alerts))
165 .route("/alerts/history", get(get_alert_history))
166 .route("/alerts/:id", get(get_alert))
167 .route("/alerts/:id/acknowledge", post(acknowledge_alert))
168 .route("/alerts/:id/resolve", post(resolve_alert))
169 .with_state(state)
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use axum::http::StatusCode;
176 use axum_test::TestServer;
177 use pulseengine_mcp_logging::{Alert, AlertConfig};
178
179 #[tokio::test]
180 async fn test_alert_summary_endpoint() {
181 let config = AlertConfig::default();
182 let manager = Arc::new(AlertManager::new(config));
183 let router = create_alerting_router(manager);
184
185 let server = TestServer::new(router).unwrap();
186 let response = server.get("/alerts/summary").await;
187
188 assert_eq!(response.status_code(), StatusCode::OK);
189
190 let summary: AlertSummary = response.json();
191 assert_eq!(summary.total_active, 0);
192 }
193
194 #[tokio::test]
195 async fn test_active_alerts_endpoint() {
196 let config = AlertConfig::default();
197 let manager = Arc::new(AlertManager::new(config));
198 let router = create_alerting_router(manager);
199
200 let server = TestServer::new(router).unwrap();
201 let response = server.get("/alerts/active").await;
202
203 assert_eq!(response.status_code(), StatusCode::OK);
204
205 let alerts: Vec<Alert> = response.json();
206 assert!(alerts.is_empty());
207 }
208}