//! Prometheus Metrics HTTP Server //! //! Provides HTTP endpoint for Prometheus to scrape metrics from the Foxhunt trading system. use std::net::SocketAddr; use std::sync::Arc; use std::fmt; use std::error::Error as StdError; use axum::{ extract::State, http::StatusCode, response::{IntoResponse, Response}, routing::get, Router, }; use prometheus::{Encoder, TextEncoder, gather}; use tokio::net::TcpListener; use tracing::{info, error, warn}; use serde::{Deserialize, Serialize}; use crate::monitoring::metrics::FoxhuntMetrics; /// Metrics server configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MetricsServerConfig { /// Server bind address pub bind_address: String, /// Server port pub port: u16, /// Enable basic authentication pub enable_auth: bool, /// Basic auth username (if auth enabled) pub auth_username: Option, /// Basic auth password (if auth enabled) pub auth_password: Option, /// Enable detailed system metrics pub enable_system_metrics: bool, /// Metrics endpoint path pub metrics_path: String, } impl Default for MetricsServerConfig { fn default() -> Self { Self { bind_address: "0.0.0.0".to_string(), port: 9090, enable_auth: false, auth_username: None, auth_password: None, enable_system_metrics: true, metrics_path: "/metrics".to_string(), } } } /// Metrics server state #[derive(Debug, Clone)] pub struct MetricsServerState { config: MetricsServerConfig, metrics: Arc, } /// Prometheus metrics HTTP server pub struct MetricsServer { config: MetricsServerConfig, metrics: Arc, } impl MetricsServer { /// Create new metrics server pub fn new(config: MetricsServerConfig, metrics: Arc) -> Self { Self { config, metrics } } /// Start the metrics server pub async fn start(&self) -> Result<(), Box> { let addr = format!("{}:{}", self.config.bind_address, self.config.port); let socket_addr: SocketAddr = addr.parse()?; let state = MetricsServerState { config: self.config.clone(), metrics: self.metrics.clone(), }; let app = self.create_router(state); let listener = TcpListener::bind(socket_addr).await?; info!("🚀 Prometheus metrics server starting on http://{}", addr); info!("📊 Metrics endpoint: http://{}{}", addr, self.config.metrics_path); axum::serve(listener, app).await?; Ok(()) } /// Create the router with all endpoints fn create_router(&self, state: MetricsServerState) -> Router { let metrics_path = state.config.metrics_path.clone(); Router::new() .route(&metrics_path, get(metrics_handler)) .route("/health", get(health_handler)) .route("/", get(root_handler)) .with_state(state) } } /// Handler for the main metrics endpoint async fn metrics_handler( State(state): State, ) -> Result { let start_time = std::time::Instant::now(); // Gather all metrics let metric_families = gather(); // Encode to Prometheus text format let encoder = TextEncoder::new(); let mut buffer = Vec::new(); encoder.encode(&metric_families, &mut buffer) .map_err(|e| MetricsError::EncodingError(e.to_string()))?; let metrics_text = String::from_utf8(buffer) .map_err(|e| MetricsError::EncodingError(e.to_string()))?; // Add custom metrics summary if enabled let response_body = if state.config.enable_system_metrics { let system_metrics = collect_system_metrics().await; format!("{}\n{}", metrics_text, system_metrics) } else { metrics_text }; let duration = start_time.elapsed(); // Log slow metrics collection if duration.as_millis() > 100 { warn!("Slow metrics collection: {}ms", duration.as_millis()); } Ok(( StatusCode::OK, [("Content-Type", "text/plain; version=0.0.4; charset=utf-8")], response_body, ).into_response()) } /// Handler for health check endpoint async fn health_handler() -> impl IntoResponse { let health_status = HealthStatus { status: "healthy".to_string(), timestamp: chrono::Utc::now(), version: env!("CARGO_PKG_VERSION").to_string(), uptime_seconds: get_uptime_seconds(), }; (StatusCode::OK, serde_json::to_string(&health_status).unwrap_or_else(|_| "{\"status\":\"healthy\"}".to_string())) } /// Handler for root endpoint async fn root_handler(State(state): State) -> impl IntoResponse { let html = format!( r#" Foxhunt Metrics Server

🦊 Foxhunt HFT Trading System

Prometheus Metrics Server

Server Status: Running

Version: {}

Metrics Endpoint: {}

Health Check: /health

📊 View Metrics ❤️ Health Check

Available Metrics

  • foxhunt_orders_total - Total orders processed
  • foxhunt_latency_microseconds - System latency distribution
  • foxhunt_position_value_usd - Current position values
  • foxhunt_ml_predictions_total - ML predictions generated
  • foxhunt_risk_breaches_total - Risk limit breaches
  • foxhunt_throughput_ops_per_second - System throughput
  • And many more...

Integration

Add this target to your Prometheus configuration:

scrape_configs:
  - job_name: 'foxhunt-trading'
    static_configs:
      - targets: ['{}:{}']
    scrape_interval: 5s
    metrics_path: '{}'
"#, env!("CARGO_PKG_VERSION"), state.config.metrics_path, state.config.metrics_path, state.config.bind_address, state.config.port, state.config.metrics_path ); (StatusCode::OK, [("Content-Type", "text/html")], html) } /// Collect additional system metrics async fn collect_system_metrics() -> String { let mut system_metrics = Vec::new(); // Add timestamp let timestamp = chrono::Utc::now().timestamp_millis(); system_metrics.push(format!( "# HELP foxhunt_metrics_collection_timestamp_ms Timestamp when metrics were collected\n\ # TYPE foxhunt_metrics_collection_timestamp_ms gauge\n\ foxhunt_metrics_collection_timestamp_ms {}", timestamp )); // Add uptime let uptime = get_uptime_seconds(); system_metrics.push(format!( "# HELP foxhunt_uptime_seconds System uptime in seconds\n\ # TYPE foxhunt_uptime_seconds counter\n\ foxhunt_uptime_seconds {}", uptime )); // Add memory info (if available) if let Ok(memory_info) = get_memory_info() { system_metrics.push(format!( "# HELP foxhunt_memory_total_bytes Total system memory\n\ # TYPE foxhunt_memory_total_bytes gauge\n\ foxhunt_memory_total_bytes {}\n\ # HELP foxhunt_memory_available_bytes Available system memory\n\ # TYPE foxhunt_memory_available_bytes gauge\n\ foxhunt_memory_available_bytes {}", memory_info.total, memory_info.available )); } system_metrics.join("\n") } /// Get system uptime in seconds fn get_uptime_seconds() -> u64 { // Simplified uptime calculation // In production, this would read from /proc/uptime or use system calls static START_TIME: std::sync::LazyLock = std::sync::LazyLock::new(|| std::time::Instant::now()); START_TIME.elapsed().as_secs() } /// Memory information structure #[derive(Debug)] struct MemoryInfo { total: u64, available: u64, } /// Get memory information (Linux-specific) fn get_memory_info() -> Result { #[cfg(target_os = "linux")] { use std::fs; let meminfo = fs::read_to_string("/proc/meminfo")?; let mut total = 0u64; let mut available = 0u64; for line in meminfo.lines() { if line.starts_with("MemTotal:") { if let Some(value) = line.split_whitespace().nth(1) { total = value.parse::().unwrap_or(0) * 1024; // Convert KB to bytes } } else if line.starts_with("MemAvailable:") { if let Some(value) = line.split_whitespace().nth(1) { available = value.parse::().unwrap_or(0) * 1024; // Convert KB to bytes } } } Ok(MemoryInfo { total, available }) } #[cfg(not(target_os = "linux"))] { // Fallback for non-Linux systems Ok(MemoryInfo { total: 8 * 1024 * 1024 * 1024, // 8GB default available: 4 * 1024 * 1024 * 1024, // 4GB default }) } } /// Health status structure #[derive(Debug, Serialize)] struct HealthStatus { status: String, timestamp: chrono::DateTime, version: String, uptime_seconds: u64, } /// Metrics server errors #[derive(Debug)] pub enum MetricsError { EncodingError(String), AuthenticationFailed, ServerError(String), } impl fmt::Display for MetricsError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { MetricsError::EncodingError(msg) => write!(f, "Encoding error: {}", msg), MetricsError::AuthenticationFailed => write!(f, "Authentication failed"), MetricsError::ServerError(msg) => write!(f, "Server error: {}", msg), } } } impl StdError for MetricsError {} impl IntoResponse for MetricsError { fn into_response(self) -> Response { let (status, message) = match self { MetricsError::EncodingError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg), MetricsError::AuthenticationFailed => (StatusCode::UNAUTHORIZED, "Authentication required".to_string()), MetricsError::ServerError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg), }; (status, message).into_response() } } /// Start metrics server with default configuration pub async fn start_metrics_server() -> Result<(), Box> { let config = MetricsServerConfig::default(); let metrics = Arc::new(FoxhuntMetrics::new()); let server = MetricsServer::new(config, metrics); info!("Starting Foxhunt metrics server..."); server.start().await } /// Start metrics server with custom configuration pub async fn start_metrics_server_with_config( config: MetricsServerConfig, ) -> Result<(), Box> { let metrics = Arc::new(FoxhuntMetrics::new()); let server = MetricsServer::new(config, metrics); server.start().await } #[cfg(test)] mod tests { use super::*; #[test] fn test_config_defaults() { let config = MetricsServerConfig::default(); assert_eq!(config.port, 9090); assert_eq!(config.bind_address, "0.0.0.0"); assert_eq!(config.metrics_path, "/metrics"); assert!(!config.enable_auth); } #[test] fn test_memory_info() { // Should not panic on any platform let _result = get_memory_info(); } #[test] fn test_uptime() { let uptime = get_uptime_seconds(); assert!(uptime >= 0); } #[tokio::test] async fn test_system_metrics_collection() { let metrics = collect_system_metrics().await; assert!(metrics.contains("foxhunt_uptime_seconds")); assert!(metrics.contains("foxhunt_metrics_collection_timestamp_ms")); } #[tokio::test] async fn test_health_handler() { let response = health_handler().await; // Should not panic and return a response let _response = response.into_response(); } }