feat(metrics): add per-IP and HTTP-request metrics, propagate source IP through proxy paths, and expose new metrics to the TS adapter
This commit is contained in:
@@ -7,6 +7,14 @@ use tracing::debug;
|
||||
|
||||
use rustproxy_metrics::MetricsCollector;
|
||||
|
||||
/// Context for forwarding metrics, replacing the growing tuple pattern.
|
||||
#[derive(Clone)]
|
||||
pub struct ForwardMetricsCtx {
|
||||
pub collector: Arc<MetricsCollector>,
|
||||
pub route_id: Option<String>,
|
||||
pub source_ip: Option<String>,
|
||||
}
|
||||
|
||||
/// Perform bidirectional TCP forwarding between client and backend.
|
||||
///
|
||||
/// This is the core data path for passthrough connections.
|
||||
@@ -73,13 +81,13 @@ pub async fn forward_bidirectional_with_timeouts(
|
||||
inactivity_timeout: std::time::Duration,
|
||||
max_lifetime: std::time::Duration,
|
||||
cancel: CancellationToken,
|
||||
metrics: Option<(Arc<MetricsCollector>, Option<String>)>,
|
||||
metrics: Option<ForwardMetricsCtx>,
|
||||
) -> std::io::Result<(u64, u64)> {
|
||||
// Send initial data (peeked bytes) to backend
|
||||
if let Some(data) = initial_data {
|
||||
backend.write_all(data).await?;
|
||||
if let Some((ref m, ref rid)) = metrics {
|
||||
m.record_bytes(data.len() as u64, 0, rid.as_deref());
|
||||
if let Some(ref ctx) = metrics {
|
||||
ctx.collector.record_bytes(data.len() as u64, 0, ctx.route_id.as_deref(), ctx.source_ip.as_deref());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,8 +113,8 @@ pub async fn forward_bidirectional_with_timeouts(
|
||||
}
|
||||
total += n as u64;
|
||||
la1.store(start.elapsed().as_millis() as u64, Ordering::Relaxed);
|
||||
if let Some((ref m, ref rid)) = metrics_c2b {
|
||||
m.record_bytes(n as u64, 0, rid.as_deref());
|
||||
if let Some(ref ctx) = metrics_c2b {
|
||||
ctx.collector.record_bytes(n as u64, 0, ctx.route_id.as_deref(), ctx.source_ip.as_deref());
|
||||
}
|
||||
}
|
||||
let _ = backend_write.shutdown().await;
|
||||
@@ -128,8 +136,8 @@ pub async fn forward_bidirectional_with_timeouts(
|
||||
}
|
||||
total += n as u64;
|
||||
la2.store(start.elapsed().as_millis() as u64, Ordering::Relaxed);
|
||||
if let Some((ref m, ref rid)) = metrics_b2c {
|
||||
m.record_bytes(0, n as u64, rid.as_deref());
|
||||
if let Some(ref ctx) = metrics_b2c {
|
||||
ctx.collector.record_bytes(0, n as u64, ctx.route_id.as_deref(), ctx.source_ip.as_deref());
|
||||
}
|
||||
}
|
||||
let _ = client_write.shutdown().await;
|
||||
@@ -182,4 +190,3 @@ pub async fn forward_bidirectional_with_timeouts(
|
||||
watchdog.abort();
|
||||
Ok((bytes_in, bytes_out))
|
||||
}
|
||||
|
||||
|
||||
@@ -20,14 +20,16 @@ use crate::connection_tracker::ConnectionTracker;
|
||||
struct ConnectionGuard {
|
||||
metrics: Arc<MetricsCollector>,
|
||||
route_id: Option<String>,
|
||||
source_ip: Option<String>,
|
||||
disarmed: bool,
|
||||
}
|
||||
|
||||
impl ConnectionGuard {
|
||||
fn new(metrics: Arc<MetricsCollector>, route_id: Option<&str>) -> Self {
|
||||
fn new(metrics: Arc<MetricsCollector>, route_id: Option<&str>, source_ip: Option<&str>) -> Self {
|
||||
Self {
|
||||
metrics,
|
||||
route_id: route_id.map(|s| s.to_string()),
|
||||
source_ip: source_ip.map(|s| s.to_string()),
|
||||
disarmed: false,
|
||||
}
|
||||
}
|
||||
@@ -42,7 +44,7 @@ impl ConnectionGuard {
|
||||
impl Drop for ConnectionGuard {
|
||||
fn drop(&mut self) {
|
||||
if !self.disarmed {
|
||||
self.metrics.connection_closed(self.route_id.as_deref());
|
||||
self.metrics.connection_closed(self.route_id.as_deref(), self.source_ip.as_deref());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -395,6 +397,9 @@ impl TcpListenerManager {
|
||||
|
||||
stream.set_nodelay(true)?;
|
||||
|
||||
// Extract source IP once for all metric calls
|
||||
let ip_str = peer_addr.ip().to_string();
|
||||
|
||||
// === Fast path: try port-only matching before peeking at data ===
|
||||
// This handles "server-speaks-first" protocols where the client
|
||||
// doesn't send initial data (e.g., SMTP, greeting-based protocols).
|
||||
@@ -460,8 +465,8 @@ impl TcpListenerManager {
|
||||
}
|
||||
}
|
||||
|
||||
metrics.connection_opened(route_id);
|
||||
let _fast_guard = ConnectionGuard::new(Arc::clone(&metrics), route_id);
|
||||
metrics.connection_opened(route_id, Some(&ip_str));
|
||||
let _fast_guard = ConnectionGuard::new(Arc::clone(&metrics), route_id, Some(&ip_str));
|
||||
|
||||
let connect_timeout = std::time::Duration::from_millis(conn_config.connection_timeout_ms);
|
||||
let inactivity_timeout = std::time::Duration::from_millis(conn_config.socket_timeout_ms);
|
||||
@@ -499,13 +504,21 @@ impl TcpListenerManager {
|
||||
let (_bytes_in, _bytes_out) = forwarder::forward_bidirectional_with_timeouts(
|
||||
stream, backend_w, None,
|
||||
inactivity_timeout, max_lifetime, cancel,
|
||||
Some((Arc::clone(&metrics), route_id.map(|s| s.to_string()))),
|
||||
Some(forwarder::ForwardMetricsCtx {
|
||||
collector: Arc::clone(&metrics),
|
||||
route_id: route_id.map(|s| s.to_string()),
|
||||
source_ip: Some(ip_str.clone()),
|
||||
}),
|
||||
).await?;
|
||||
} else {
|
||||
let (_bytes_in, _bytes_out) = forwarder::forward_bidirectional_with_timeouts(
|
||||
stream, backend, None,
|
||||
inactivity_timeout, max_lifetime, cancel,
|
||||
Some((Arc::clone(&metrics), route_id.map(|s| s.to_string()))),
|
||||
Some(forwarder::ForwardMetricsCtx {
|
||||
collector: Arc::clone(&metrics),
|
||||
route_id: route_id.map(|s| s.to_string()),
|
||||
source_ip: Some(ip_str.clone()),
|
||||
}),
|
||||
).await?;
|
||||
}
|
||||
|
||||
@@ -646,8 +659,8 @@ impl TcpListenerManager {
|
||||
}
|
||||
|
||||
// Track connection in metrics — guard ensures connection_closed on all exit paths
|
||||
metrics.connection_opened(route_id);
|
||||
let _conn_guard = ConnectionGuard::new(Arc::clone(&metrics), route_id);
|
||||
metrics.connection_opened(route_id, Some(&ip_str));
|
||||
let _conn_guard = ConnectionGuard::new(Arc::clone(&metrics), route_id, Some(&ip_str));
|
||||
|
||||
// Check if this is a socket-handler route that should be relayed to TypeScript
|
||||
if route_match.route.action.action_type == RouteActionType::SocketHandler {
|
||||
@@ -755,7 +768,11 @@ impl TcpListenerManager {
|
||||
let (_bytes_in, _bytes_out) = forwarder::forward_bidirectional_with_timeouts(
|
||||
stream, backend, Some(&actual_buf),
|
||||
inactivity_timeout, max_lifetime, cancel,
|
||||
Some((Arc::clone(&metrics), route_id.map(|s| s.to_string()))),
|
||||
Some(forwarder::ForwardMetricsCtx {
|
||||
collector: Arc::clone(&metrics),
|
||||
route_id: route_id.map(|s| s.to_string()),
|
||||
source_ip: Some(ip_str.clone()),
|
||||
}),
|
||||
).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -816,7 +833,11 @@ impl TcpListenerManager {
|
||||
let (_bytes_in, _bytes_out) = Self::forward_bidirectional_split_with_timeouts(
|
||||
tls_read, tls_write, backend_read, backend_write,
|
||||
inactivity_timeout, max_lifetime,
|
||||
Some((Arc::clone(&metrics), route_id.map(|s| s.to_string()))),
|
||||
Some(forwarder::ForwardMetricsCtx {
|
||||
collector: Arc::clone(&metrics),
|
||||
route_id: route_id.map(|s| s.to_string()),
|
||||
source_ip: Some(ip_str.clone()),
|
||||
}),
|
||||
).await;
|
||||
}
|
||||
Ok(())
|
||||
@@ -865,7 +886,11 @@ impl TcpListenerManager {
|
||||
let (_bytes_in, _bytes_out) = forwarder::forward_bidirectional_with_timeouts(
|
||||
stream, backend, Some(&actual_buf),
|
||||
inactivity_timeout, max_lifetime, cancel,
|
||||
Some((Arc::clone(&metrics), route_id.map(|s| s.to_string()))),
|
||||
Some(forwarder::ForwardMetricsCtx {
|
||||
collector: Arc::clone(&metrics),
|
||||
route_id: route_id.map(|s| s.to_string()),
|
||||
source_ip: Some(ip_str.clone()),
|
||||
}),
|
||||
).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -941,12 +966,14 @@ impl TcpListenerManager {
|
||||
let total_in = c2s + initial_len;
|
||||
debug!("Socket handler relay complete for {}: {} bytes in, {} bytes out",
|
||||
route_key, total_in, s2c);
|
||||
metrics.record_bytes(total_in, s2c, route_id);
|
||||
let ip = peer_addr.ip().to_string();
|
||||
metrics.record_bytes(total_in, s2c, route_id, Some(&ip));
|
||||
}
|
||||
Err(e) => {
|
||||
// Still record the initial data even on error
|
||||
if initial_len > 0 {
|
||||
metrics.record_bytes(initial_len, 0, route_id);
|
||||
let ip = peer_addr.ip().to_string();
|
||||
metrics.record_bytes(initial_len, 0, route_id, Some(&ip));
|
||||
}
|
||||
debug!("Socket handler relay ended for {}: {}", route_key, e);
|
||||
}
|
||||
@@ -1032,7 +1059,11 @@ impl TcpListenerManager {
|
||||
let (_bytes_in, _bytes_out) = Self::forward_bidirectional_split_with_timeouts(
|
||||
client_read, client_write, backend_read, backend_write,
|
||||
inactivity_timeout, max_lifetime,
|
||||
Some((metrics, route_id.map(|s| s.to_string()))),
|
||||
Some(forwarder::ForwardMetricsCtx {
|
||||
collector: metrics,
|
||||
route_id: route_id.map(|s| s.to_string()),
|
||||
source_ip: Some(peer_addr.ip().to_string()),
|
||||
}),
|
||||
).await;
|
||||
|
||||
Ok(())
|
||||
@@ -1078,7 +1109,7 @@ impl TcpListenerManager {
|
||||
mut backend_write: W2,
|
||||
inactivity_timeout: std::time::Duration,
|
||||
max_lifetime: std::time::Duration,
|
||||
metrics: Option<(Arc<MetricsCollector>, Option<String>)>,
|
||||
metrics: Option<forwarder::ForwardMetricsCtx>,
|
||||
) -> (u64, u64)
|
||||
where
|
||||
R1: tokio::io::AsyncRead + Unpin + Send + 'static,
|
||||
@@ -1111,8 +1142,8 @@ impl TcpListenerManager {
|
||||
start.elapsed().as_millis() as u64,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
if let Some((ref m, ref rid)) = metrics_c2b {
|
||||
m.record_bytes(n as u64, 0, rid.as_deref());
|
||||
if let Some(ref ctx) = metrics_c2b {
|
||||
ctx.collector.record_bytes(n as u64, 0, ctx.route_id.as_deref(), ctx.source_ip.as_deref());
|
||||
}
|
||||
}
|
||||
let _ = backend_write.shutdown().await;
|
||||
@@ -1137,8 +1168,8 @@ impl TcpListenerManager {
|
||||
start.elapsed().as_millis() as u64,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
if let Some((ref m, ref rid)) = metrics_b2c {
|
||||
m.record_bytes(0, n as u64, rid.as_deref());
|
||||
if let Some(ref ctx) = metrics_b2c {
|
||||
ctx.collector.record_bytes(0, n as u64, ctx.route_id.as_deref(), ctx.source_ip.as_deref());
|
||||
}
|
||||
}
|
||||
let _ = client_write.shutdown().await;
|
||||
|
||||
Reference in New Issue
Block a user