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:
jgrusewski
2026-03-01 02:14:08 +01:00
parent dc0750dc9a
commit f0c653ab63
4 changed files with 135 additions and 0 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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);

View 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());
}