use axum::{extract::State, Json};
use serde::Serialize;
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime};
use tracing::{debug, info};
use uuid::Uuid;
use crate::logging::generate_request_id;
use crate::state::AppState;
static SERVER_ID: once_cell::sync::Lazy<String> =
once_cell::sync::Lazy::new(|| Uuid::new_v4().to_string());
static START_TIME: once_cell::sync::Lazy<SystemTime> = once_cell::sync::Lazy::new(SystemTime::now);
#[derive(Serialize)]
pub struct HeartbeatResponse {
pub server_id: String,
pub timestamp: String,
pub uptime_seconds: u64,
pub memory_usage_bytes: Option<u64>,
pub available_memory_bytes: Option<u64>,
pub dataset: DatasetInfo,
pub status: String,
}
#[derive(Serialize)]
pub struct DatasetInfo {
pub file_path: String,
pub variable_count: usize,
pub variables: Vec<String>,
pub dimension_count: usize,
pub dimensions: Vec<(String, usize)>,
pub data_memory_bytes: usize,
}
pub async fn heartbeat_handler(State(state): State<Arc<AppState>>) -> Json<HeartbeatResponse> {
let request_id = generate_request_id();
let start_time = Instant::now();
debug!(
endpoint = "/heartbeat",
request_id = %request_id,
"Processing heartbeat request"
);
let now = SystemTime::now();
let timestamp = chrono::DateTime::<chrono::Utc>::from(now)
.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
let uptime = now
.duration_since(*START_TIME)
.unwrap_or(Duration::from_secs(0));
debug!(
uptime_seconds = uptime.as_secs(),
"Server uptime calculated"
);
let memory_usage = get_memory_usage();
let available_memory = get_available_memory();
let data_memory = calculate_data_memory_usage(&state);
debug!(
memory_usage_mb = memory_usage.map(|b| b / (1024 * 1024)),
available_memory_mb = available_memory.map(|b| b / (1024 * 1024)),
data_memory_mb = data_memory / (1024 * 1024),
"Memory statistics collected"
);
let dataset_info = DatasetInfo {
file_path: state.config.data.file_path.clone().map_or_else(
|| "<unknown>".to_string(),
|p| p.to_string_lossy().to_string(),
),
variable_count: state.metadata.variables.len(),
variables: state.metadata.variables.keys().cloned().collect(),
dimension_count: state.metadata.dimensions.len(),
dimensions: state
.metadata
.dimensions
.iter()
.map(|(name, dim)| (name.clone(), dim.size))
.collect(),
data_memory_bytes: data_memory,
};
let response = HeartbeatResponse {
server_id: SERVER_ID.clone(),
timestamp,
uptime_seconds: uptime.as_secs(),
memory_usage_bytes: memory_usage,
available_memory_bytes: available_memory,
dataset: dataset_info,
status: "healthy".to_string(),
};
let duration = start_time.elapsed();
info!(
endpoint = "/heartbeat",
request_id = %request_id,
duration_us = duration.as_micros() as u64,
uptime_seconds = uptime.as_secs(),
memory_usage_mb = memory_usage.map(|b| b / (1024 * 1024)),
data_memory_mb = data_memory / (1024 * 1024),
variable_count = state.metadata.variables.len(),
"Heartbeat request successful"
);
Json(response)
}
fn calculate_data_memory_usage(state: &AppState) -> usize {
let mut total_bytes = 0;
for array in state.data.values() {
total_bytes += array.len() * 4;
}
total_bytes
}
fn get_memory_usage() -> Option<u64> {
#[cfg(target_os = "linux")]
{
use std::fs::File;
use std::io::Read;
let mut statm = String::new();
if let Ok(mut file) = File::open("/proc/self/statm") {
if file.read_to_string(&mut statm).is_ok() {
let parts: Vec<&str> = statm.split_whitespace().collect();
if parts.len() >= 2 {
if let Ok(pages) = parts[1].parse::<u64>() {
return Some(pages * 4096);
}
}
}
}
None
}
#[cfg(target_os = "macos")]
{
use std::process::Command;
let output = Command::new("ps")
.args(["-o", "rss=", "-p", &std::process::id().to_string()])
.output();
if let Ok(output) = output {
let rss = String::from_utf8_lossy(&output.stdout)
.trim()
.parse::<u64>();
if let Ok(rss_kb) = rss {
return Some(rss_kb * 1024);
}
}
None
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
None
}
}
fn get_available_memory() -> Option<u64> {
#[cfg(target_os = "linux")]
{
use std::fs::File;
use std::io::{BufRead, BufReader};
if let Ok(file) = File::open("/proc/meminfo") {
let reader = BufReader::new(file);
for line in reader.lines().map_while(Result::ok) {
if line.starts_with("MemAvailable:") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
if let Ok(kb) = parts[1].parse::<u64>() {
return Some(kb * 1024);
}
}
}
}
}
None
}
#[cfg(target_os = "macos")]
{
use std::process::Command;
let output = Command::new("vm_stat").output();
if let Ok(output) = output {
let vm_stat = String::from_utf8_lossy(&output.stdout);
let page_size = if let Some(line) = vm_stat.lines().find(|l| l.contains("page size of"))
{
if let Some(size_str) = line.split("page size of ").nth(1) {
size_str.trim().parse::<u64>().unwrap_or(4096)
} else {
4096 }
} else {
4096
};
if let Some(line) = vm_stat.lines().find(|l| l.starts_with("Pages free:")) {
if let Some(count_str) = line.split(':').nth(1) {
if let Ok(count) = count_str.trim().replace(".", "").parse::<u64>() {
return Some(count * page_size);
}
}
}
}
None
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::state::Metadata;
use std::collections::HashMap;
#[test]
fn test_heartbeat_response_structure() {
let config = Config::default();
let mut dimensions = HashMap::new();
dimensions.insert(
"lat".to_string(),
crate::state::Dimension {
name: "lat".to_string(),
size: 180,
is_unlimited: false,
},
);
dimensions.insert(
"lon".to_string(),
crate::state::Dimension {
name: "lon".to_string(),
size: 360,
is_unlimited: false,
},
);
dimensions.insert(
"time".to_string(),
crate::state::Dimension {
name: "time".to_string(),
size: 24,
is_unlimited: true,
},
);
let metadata = Metadata {
dimensions,
variables: HashMap::new(),
global_attributes: HashMap::new(),
coordinates: HashMap::new(),
};
let data = HashMap::new();
let state = AppState::new(config, metadata, data);
let memory = calculate_data_memory_usage(&state);
assert_eq!(memory, 0);
}
}