diff --git a/crates/web-gateway/Cargo.toml b/crates/web-gateway/Cargo.toml index 80f45f5c2..603131137 100644 --- a/crates/web-gateway/Cargo.toml +++ b/crates/web-gateway/Cargo.toml @@ -43,6 +43,10 @@ tracing-subscriber.workspace = true # Shared types common.workspace = true +# Metrics +prometheus.workspace = true +once_cell.workspace = true + # Time and IDs chrono.workspace = true uuid.workspace = true diff --git a/crates/web-gateway/src/lib.rs b/crates/web-gateway/src/lib.rs index f0c857662..ac7f3da28 100644 --- a/crates/web-gateway/src/lib.rs +++ b/crates/web-gateway/src/lib.rs @@ -10,6 +10,7 @@ pub mod auth; pub mod config; pub mod error; pub mod grpc; +pub mod metrics; pub mod rate_limit; pub mod routes; pub mod state; diff --git a/crates/web-gateway/src/main.rs b/crates/web-gateway/src/main.rs index 84e05fd11..047af22e3 100644 --- a/crates/web-gateway/src/main.rs +++ b/crates/web-gateway/src/main.rs @@ -75,6 +75,52 @@ async fn main() -> Result<()> { .layer(TraceLayer::new_for_http()) .layer(middleware::from_fn(request_id_middleware)); + // Initialize Prometheus metrics + web_gateway::metrics::init_metrics(); + let service_start = std::time::Instant::now(); + + // Spawn uptime updater + tokio::spawn(async move { + let mut interval = tokio::time::interval(std::time::Duration::from_secs(1)); + loop { + interval.tick().await; + web_gateway::metrics::update_uptime(service_start); + } + }); + + // Start Prometheus metrics HTTP endpoint on a separate port + let metrics_port: u16 = std::env::var("METRICS_PORT") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(9098); + tokio::spawn(async move { + use axum::{routing::get, Router}; + use prometheus::{Encoder, TextEncoder}; + + async fn metrics_handler() -> String { + let encoder = TextEncoder::new(); + let metric_families = prometheus::gather(); + let mut buffer = vec![]; + let _ = encoder.encode(&metric_families, &mut buffer); + String::from_utf8(buffer).unwrap_or_else(|_| String::new()) + } + + let app = Router::new().route("/metrics", get(metrics_handler)); + let addr = format!("0.0.0.0:{}", metrics_port); + tracing::info!("Prometheus metrics endpoint listening on http://{}", addr); + + let metrics_listener = match tokio::net::TcpListener::bind(&addr).await { + Ok(l) => l, + Err(e) => { + tracing::error!("Failed to bind metrics endpoint {}: {}", addr, e); + return; + } + }; + if let Err(e) = axum::serve(metrics_listener, app).await { + tracing::error!("Metrics server failed: {}", e); + } + }); + let listener = tokio::net::TcpListener::bind(&listen_addr).await?; info!("Web gateway listening on {}", listen_addr); diff --git a/crates/web-gateway/src/metrics.rs b/crates/web-gateway/src/metrics.rs new file mode 100644 index 000000000..eaa4de619 --- /dev/null +++ b/crates/web-gateway/src/metrics.rs @@ -0,0 +1,84 @@ +//! Prometheus metrics for Web Gateway + +use once_cell::sync::Lazy; +use prometheus::{ + register_counter_vec, register_gauge, register_histogram_vec, CounterVec, Gauge, HistogramVec, +}; + +/// Service uptime in seconds +pub static SERVICE_UPTIME: Lazy = Lazy::new(|| { + register_gauge!( + "foxhunt_web_gateway_uptime_seconds", + "Service uptime in seconds" + ) + .unwrap_or_else(|e| { + eprintln!("FATAL: failed to register uptime gauge: {e}"); + std::process::abort() + }) +}); + +/// HTTP requests total +pub static HTTP_REQUESTS: Lazy = Lazy::new(|| { + register_counter_vec!( + "foxhunt_web_gateway_http_requests_total", + "Total HTTP requests", + &["method", "path", "status"] + ) + .unwrap_or_else(|e| { + eprintln!("FATAL: failed to register http_requests counter: {e}"); + std::process::abort() + }) +}); + +/// HTTP request duration +pub static HTTP_REQUEST_DURATION: Lazy = Lazy::new(|| { + register_histogram_vec!( + "foxhunt_web_gateway_http_request_duration_seconds", + "HTTP request duration in seconds", + &["method", "path"], + vec![0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 5.0] + ) + .unwrap_or_else(|e| { + eprintln!("FATAL: failed to register http_request_duration histogram: {e}"); + std::process::abort() + }) +}); + +/// Active WebSocket connections +pub static WS_CONNECTIONS: Lazy = Lazy::new(|| { + register_gauge!( + "foxhunt_web_gateway_ws_connections_active", + "Active WebSocket connections" + ) + .unwrap_or_else(|e| { + eprintln!("FATAL: failed to register ws_connections gauge: {e}"); + std::process::abort() + }) +}); + +/// WebSocket messages total +pub static WS_MESSAGES: Lazy = Lazy::new(|| { + register_counter_vec!( + "foxhunt_web_gateway_ws_messages_total", + "Total WebSocket messages", + &["direction", "topic"] + ) + .unwrap_or_else(|e| { + eprintln!("FATAL: failed to register ws_messages counter: {e}"); + std::process::abort() + }) +}); + +/// Initialize all metrics (force lazy registration) +pub fn init_metrics() { + let _ = &*SERVICE_UPTIME; + let _ = &*HTTP_REQUESTS; + let _ = &*HTTP_REQUEST_DURATION; + let _ = &*WS_CONNECTIONS; + let _ = &*WS_MESSAGES; +} + +/// Update service uptime metric +pub fn update_uptime(start_time: std::time::Instant) { + SERVICE_UPTIME.set(start_time.elapsed().as_secs_f64()); +}