feat(web-gateway): add Prometheus metrics server on port 9098
Adds a dedicated Prometheus metrics endpoint on port 9098 (configurable via METRICS_PORT env var) with counters for HTTP requests, histograms for request duration, gauges for uptime and active WebSocket connections, and counters for WebSocket messages. Metrics are registered using once_cell Lazy statics with abort-on-failure to satisfy the crate's deny(unwrap) lint policy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
84
crates/web-gateway/src/metrics.rs
Normal file
84
crates/web-gateway/src/metrics.rs
Normal file
@@ -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<Gauge> = 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<CounterVec> = 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<HistogramVec> = 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<Gauge> = 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<CounterVec> = 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());
|
||||
}
|
||||
Reference in New Issue
Block a user