Skip to main content

feagi_api/endpoints/
monitoring.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5 * FEAGI v1 Monitoring API
6 *
7 * Endpoints for system monitoring, metrics, and telemetry
8 * Maps to Python: feagi/api/v1/monitoring.py
9 */
10
11use crate::common::ApiState;
12use crate::common::{ApiError, ApiResult, Json, Query, State};
13use serde::Deserialize;
14use serde_json::{json, Value};
15use std::collections::HashMap;
16use utoipa::IntoParams;
17
18// ============================================================================
19// MONITORING & METRICS
20// ============================================================================
21
22/// Get monitoring system status including metrics collection and brain readiness.
23#[utoipa::path(
24    get,
25    path = "/v1/monitoring/status",
26    tag = "monitoring",
27    responses(
28        (status = 200, description = "Monitoring status", body = HashMap<String, serde_json::Value>),
29        (status = 500, description = "Internal server error")
30    )
31)]
32pub async fn get_status(State(state): State<ApiState>) -> ApiResult<Json<HashMap<String, Value>>> {
33    // Get monitoring status from analytics service
34    let analytics_service = state.analytics_service.as_ref();
35
36    // Get system health as a proxy for monitoring status
37    let health = analytics_service
38        .get_system_health()
39        .await
40        .map_err(|e| ApiError::internal(format!("Failed to get system health: {}", e)))?;
41
42    let mut response = HashMap::new();
43    response.insert("enabled".to_string(), json!(true));
44    response.insert("metrics_collected".to_string(), json!(5)); // Static count for now
45    response.insert("brain_readiness".to_string(), json!(health.brain_readiness));
46    response.insert(
47        "burst_engine_active".to_string(),
48        json!(health.burst_engine_active),
49    );
50
51    Ok(Json(response))
52}
53
54/// Get system metrics including burst frequency, neuron count, and brain readiness.
55#[utoipa::path(
56    get,
57    path = "/v1/monitoring/metrics",
58    tag = "monitoring",
59    responses(
60        (status = 200, description = "System metrics", body = HashMap<String, serde_json::Value>),
61        (status = 500, description = "Internal server error")
62    )
63)]
64pub async fn get_metrics(State(state): State<ApiState>) -> ApiResult<Json<HashMap<String, Value>>> {
65    // Get system metrics from analytics and runtime services
66    let runtime_service = state.runtime_service.as_ref();
67    let analytics_service = state.analytics_service.as_ref();
68
69    let runtime_status = runtime_service
70        .get_status()
71        .await
72        .map_err(|e| ApiError::internal(format!("Failed to get runtime status: {}", e)))?;
73
74    let health = analytics_service
75        .get_system_health()
76        .await
77        .map_err(|e| ApiError::internal(format!("Failed to get system health: {}", e)))?;
78
79    let mut response = HashMap::new();
80    response.insert(
81        "burst_frequency_hz".to_string(),
82        json!(runtime_status.frequency_hz),
83    );
84    response.insert("burst_count".to_string(), json!(runtime_status.burst_count));
85    response.insert("neuron_count".to_string(), json!(health.neuron_count));
86    response.insert(
87        "cortical_area_count".to_string(),
88        json!(health.cortical_area_count),
89    );
90    response.insert("brain_readiness".to_string(), json!(health.brain_readiness));
91    response.insert(
92        "burst_engine_active".to_string(),
93        json!(health.burst_engine_active),
94    );
95
96    Ok(Json(response))
97}
98
99/// Get detailed monitoring data with timestamps for analysis and debugging.
100#[utoipa::path(
101    get,
102    path = "/v1/monitoring/data",
103    tag = "monitoring",
104    responses(
105        (status = 200, description = "Monitoring data", body = HashMap<String, serde_json::Value>),
106        (status = 500, description = "Internal server error")
107    )
108)]
109pub async fn get_data(State(state): State<ApiState>) -> ApiResult<Json<HashMap<String, Value>>> {
110    // Get detailed monitoring data from all services
111    let analytics_service = state.analytics_service.as_ref();
112
113    let health = analytics_service
114        .get_system_health()
115        .await
116        .map_err(|e| ApiError::internal(format!("Failed to get system health: {}", e)))?;
117
118    // Return comprehensive monitoring data
119    let mut data = HashMap::new();
120    data.insert("neuron_count".to_string(), json!(health.neuron_count));
121    data.insert(
122        "cortical_area_count".to_string(),
123        json!(health.cortical_area_count),
124    );
125    data.insert("burst_count".to_string(), json!(health.burst_count));
126    data.insert("brain_readiness".to_string(), json!(health.brain_readiness));
127    data.insert(
128        "burst_engine_active".to_string(),
129        json!(health.burst_engine_active),
130    );
131
132    let mut response = HashMap::new();
133    response.insert("data".to_string(), json!(data));
134    response.insert(
135        "timestamp".to_string(),
136        json!(chrono::Utc::now().to_rfc3339()),
137    );
138
139    Ok(Json(response))
140}
141
142/// Get performance metrics including CPU and memory usage.
143#[utoipa::path(
144    get,
145    path = "/v1/monitoring/performance",
146    tag = "monitoring",
147    responses(
148        (status = 200, description = "Performance metrics", body = HashMap<String, serde_json::Value>),
149        (status = 500, description = "Internal server error")
150    )
151)]
152pub async fn get_performance(
153    State(_state): State<ApiState>,
154) -> ApiResult<Json<HashMap<String, Value>>> {
155    let mut response = HashMap::new();
156    response.insert("cpu_usage".to_string(), json!(0.0));
157    response.insert("memory_usage".to_string(), json!(0.0));
158
159    Ok(Json(response))
160}
161
162// ============================================================================
163// CORTICAL ACTIVITY MONITORING
164// ============================================================================
165
166#[derive(Debug, Deserialize, IntoParams)]
167#[into_params(parameter_in = Query)]
168pub struct CorticalActivityQuery {
169    /// Cortical area ID (Base64 encoded) to monitor
170    pub area: String,
171    /// Duration to monitor in seconds
172    #[serde(default = "default_duration")]
173    pub duration: f32,
174}
175
176fn default_duration() -> f32 {
177    1.0
178}
179
180/// Monitor real-time neuron firing activity for a specific cortical area over a time window.
181#[utoipa::path(
182    get,
183    path = "/v1/monitoring/cortical_activity",
184    tag = "monitoring",
185    params(CorticalActivityQuery),
186    responses(
187        (status = 200, description = "Cortical area firing activity", body = HashMap<String, serde_json::Value>),
188        (status = 404, description = "Cortical area not found"),
189        (status = 500, description = "Internal server error")
190    )
191)]
192pub async fn get_cortical_activity(
193    State(state): State<ApiState>,
194    Query(params): Query<CorticalActivityQuery>,
195) -> ApiResult<Json<HashMap<String, Value>>> {
196    use tracing::info;
197
198    let runtime_service = state.runtime_service.as_ref();
199    let connectome_service = state.connectome_service.as_ref();
200
201    info!(
202        target: "feagi-api",
203        "Monitoring cortical activity for area={}, duration={}s",
204        params.area,
205        params.duration
206    );
207
208    let area = connectome_service
209        .get_cortical_area(&params.area)
210        .await
211        .map_err(|_| ApiError::not_found("cortical area", &params.area))?;
212
213    let cortical_idx = area.cortical_idx;
214    let area_name = area.name.clone();
215
216    let start_burst = runtime_service
217        .get_burst_count()
218        .await
219        .map_err(|e| ApiError::internal(format!("Failed to get burst count: {}", e)))?;
220
221    let sample_interval_ms = 50;
222    let num_samples = ((params.duration * 1000.0) / sample_interval_ms as f32).ceil() as usize;
223
224    let mut spike_history = Vec::new();
225    let mut neuron_fire_counts: HashMap<u32, u32> = HashMap::new();
226    let mut total_membrane_potential = 0.0;
227    let mut potential_samples = 0;
228
229    for _ in 0..num_samples {
230        tokio::time::sleep(tokio::time::Duration::from_millis(sample_interval_ms)).await;
231
232        let fq_sample = runtime_service
233            .get_fire_queue_sample()
234            .await
235            .map_err(|e| ApiError::internal(format!("Failed to get fire queue: {}", e)))?;
236
237        if let Some((neuron_ids, _x_coords, _y_coords, _z_coords, potentials)) =
238            fq_sample.get(&cortical_idx)
239        {
240            let current_burst = runtime_service
241                .get_burst_count()
242                .await
243                .map_err(|e| ApiError::internal(format!("Failed to get burst count: {}", e)))?;
244
245            for (i, &neuron_id) in neuron_ids.iter().enumerate() {
246                *neuron_fire_counts.entry(neuron_id).or_insert(0) += 1;
247
248                let potential = potentials.get(i).copied().unwrap_or(0.0);
249                total_membrane_potential += potential;
250                potential_samples += 1;
251
252                spike_history.push(json!({
253                    "burst": current_burst,
254                    "neuron_id": neuron_id,
255                    "potential": potential
256                }));
257            }
258        }
259    }
260
261    let end_burst = runtime_service
262        .get_burst_count()
263        .await
264        .map_err(|e| ApiError::internal(format!("Failed to get burst count: {}", e)))?;
265
266    let total_spikes = spike_history.len();
267    let firing_rate_hz = if params.duration > 0.0 {
268        total_spikes as f32 / params.duration
269    } else {
270        0.0
271    };
272
273    let active_neurons: Vec<u32> = neuron_fire_counts.keys().copied().collect();
274    let peak_firing_neuron = neuron_fire_counts
275        .iter()
276        .max_by_key(|(_, &count)| count)
277        .map(|(&neuron_id, _)| neuron_id);
278
279    let avg_membrane_potential = if potential_samples > 0 {
280        total_membrane_potential / potential_samples as f32
281    } else {
282        0.0
283    };
284
285    info!(
286        target: "feagi-api",
287        "Activity monitoring complete: {} spikes, {} active neurons, {:.2} Hz",
288        total_spikes,
289        active_neurons.len(),
290        firing_rate_hz
291    );
292
293    Ok(Json(HashMap::from([
294        ("area_id".to_string(), json!(params.area)),
295        ("area_name".to_string(), json!(area_name)),
296        ("duration_ms".to_string(), json!(params.duration * 1000.0)),
297        (
298            "burst_count_sampled".to_string(),
299            json!(end_burst - start_burst),
300        ),
301        (
302            "firing_statistics".to_string(),
303            json!({
304                "total_spikes": total_spikes,
305                "firing_rate_hz": firing_rate_hz,
306                "active_neurons": active_neurons,
307                "active_neuron_count": active_neurons.len(),
308                "peak_firing_neuron": peak_firing_neuron,
309                "avg_membrane_potential": avg_membrane_potential,
310            }),
311        ),
312        ("spike_history".to_string(), json!(spike_history)),
313    ])))
314}