From 6c5180573a646f83d9afad0d463bbd556bef6507 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 14 Apr 2026 09:17:55 +0000 Subject: [PATCH] fix(rustproxy metrics): use stable route metrics keys across HTTP and passthrough listeners --- changelog.md | 8 +++ .../rustproxy-config/src/route_types.rs | 65 +++++++++++++++++++ .../rustproxy-http/src/proxy_service.rs | 5 +- .../rustproxy-passthrough/src/quic_handler.rs | 4 +- .../rustproxy-passthrough/src/tcp_listener.rs | 18 ++--- .../rustproxy-passthrough/src/udp_listener.rs | 2 +- ts/00_commitinfo_data.ts | 2 +- 7 files changed, 89 insertions(+), 15 deletions(-) diff --git a/changelog.md b/changelog.md index fc464d8..c9a9d79 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-04-14 - 27.7.4 - fix(rustproxy metrics) +use stable route metrics keys across HTTP and passthrough listeners + +- adds a shared RouteConfig::metrics_key helper that prefers route name and falls back to route id +- updates HTTP, TCP, UDP, and QUIC metrics labeling to use the shared route metrics key consistently +- keeps route cancellation and rate limiter indexing bound to route config ids where required +- adds tests covering metrics key selection behavior + ## 2026-04-14 - 27.7.3 - fix(repo) no changes detected diff --git a/rust/crates/rustproxy-config/src/route_types.rs b/rust/crates/rustproxy-config/src/route_types.rs index e827229..5b87245 100644 --- a/rust/crates/rustproxy-config/src/route_types.rs +++ b/rust/crates/rustproxy-config/src/route_types.rs @@ -656,6 +656,11 @@ impl RouteConfig { self.route_match.ports.to_ports() } + /// Stable key used for frontend route-scoped metrics. + pub fn metrics_key(&self) -> Option<&str> { + self.name.as_deref().or(self.id.as_deref()) + } + /// Get the TLS mode for this route (from action-level or first target). pub fn tls_mode(&self) -> Option<&crate::tls_types::TlsMode> { // Check action-level TLS first @@ -673,3 +678,63 @@ impl RouteConfig { None } } + +#[cfg(test)] +mod tests { + use super::*; + + fn test_route(name: Option<&str>, id: Option<&str>) -> RouteConfig { + RouteConfig { + id: id.map(str::to_string), + route_match: RouteMatch { + ports: PortRange::Single(443), + transport: None, + domains: None, + path: None, + client_ip: None, + tls_version: None, + headers: None, + protocol: None, + }, + action: RouteAction { + action_type: RouteActionType::Forward, + targets: None, + tls: None, + websocket: None, + load_balancing: None, + advanced: None, + options: None, + send_proxy_protocol: None, + udp: None, + }, + headers: None, + security: None, + name: name.map(str::to_string), + description: None, + priority: None, + tags: None, + enabled: None, + } + } + + #[test] + fn metrics_key_prefers_name() { + let route = test_route(Some("named-route"), Some("route-id")); + + assert_eq!(route.metrics_key(), Some("named-route")); + } + + #[test] + fn metrics_key_falls_back_to_id() { + let route = test_route(None, Some("route-id")); + + assert_eq!(route.metrics_key(), Some("route-id")); + } + + #[test] + fn metrics_key_is_absent_without_name_or_id() { + let route = test_route(None, None); + + assert_eq!(route.metrics_key(), None); + } +} diff --git a/rust/crates/rustproxy-http/src/proxy_service.rs b/rust/crates/rustproxy-http/src/proxy_service.rs index 3bf79e1..668e29a 100644 --- a/rust/crates/rustproxy-http/src/proxy_service.rs +++ b/rust/crates/rustproxy-http/src/proxy_service.rs @@ -639,7 +639,8 @@ impl HttpProxyService { } }; - let route_id = route_match.route.id.as_deref(); + let route_config_id = route_match.route.id.as_deref(); + let route_id = route_match.route.metrics_key(); let ip_str = ip_string; // reuse from above (avoid redundant to_string()) self.metrics.record_http_request(); if let Some(ref h) = host { @@ -654,7 +655,7 @@ impl HttpProxyService { .as_ref() .filter(|rl| rl.enabled) .map(|rl| { - let route_key = route_id.unwrap_or("__default__").to_string(); + let route_key = route_config_id.unwrap_or("__default__").to_string(); self.route_rate_limiters .entry(route_key) .or_insert_with(|| Arc::new(RateLimiter::new(rl.max_requests, rl.window))) diff --git a/rust/crates/rustproxy-passthrough/src/quic_handler.rs b/rust/crates/rustproxy-passthrough/src/quic_handler.rs index b6b379c..67b1a1e 100644 --- a/rust/crates/rustproxy-passthrough/src/quic_handler.rs +++ b/rust/crates/rustproxy-passthrough/src/quic_handler.rs @@ -420,7 +420,7 @@ pub async fn quic_accept_loop( } conn_tracker.connection_opened(&ip); - let route_id = route.name.clone().or(route.id.clone()); + let route_id = route.metrics_key().map(str::to_string); metrics.connection_opened(route_id.as_deref(), Some(&ip_str)); // Resolve per-route cancel token (child of global cancel) @@ -541,7 +541,7 @@ async fn handle_quic_stream_forwarding( real_client_addr: Option, ) -> anyhow::Result<()> { let effective_addr = real_client_addr.unwrap_or_else(|| connection.remote_address()); - let route_id = route.name.as_deref().or(route.id.as_deref()); + let route_id = route.metrics_key(); let metrics_arc = metrics; // Resolve backend target diff --git a/rust/crates/rustproxy-passthrough/src/tcp_listener.rs b/rust/crates/rustproxy-passthrough/src/tcp_listener.rs index 0d94330..5529fa4 100644 --- a/rust/crates/rustproxy-passthrough/src/tcp_listener.rs +++ b/rust/crates/rustproxy-passthrough/src/tcp_listener.rs @@ -715,10 +715,11 @@ impl TcpListenerManager { } else if let Some(target) = quick_match.target { let target_host = target.host.first().to_string(); let target_port = target.port.resolve(port); - let route_id = quick_match.route.id.as_deref(); + let route_config_id = quick_match.route.id.as_deref(); + let route_id = quick_match.route.metrics_key(); // Resolve per-route cancel token (child of global cancel) - let route_cancel = match route_id { + let route_cancel = match route_config_id { Some(id) => route_cancels.entry(id.to_string()) .or_insert_with(|| cancel.child_token()) .clone(), @@ -733,7 +734,7 @@ impl TcpListenerManager { cancel: conn_cancel.clone(), source_ip: peer_addr.ip(), domain: None, // fast path has no domain - route_id: route_id.map(|s| s.to_string()), + route_id: route_config_id.map(|s| s.to_string()), }, ); @@ -905,12 +906,13 @@ impl TcpListenerManager { } }; - let route_id = route_match.route.id.as_deref(); + let route_config_id = route_match.route.id.as_deref(); + let route_id = route_match.route.metrics_key(); // Resolve per-route cancel token (child of global cancel). // When this route is removed via updateRoutes, the token is cancelled, // terminating all connections on this route. - let route_cancel = match route_id { + let route_cancel = match route_config_id { Some(id) => route_cancels.entry(id.to_string()) .or_insert_with(|| cancel.child_token()) .clone(), @@ -925,7 +927,7 @@ impl TcpListenerManager { cancel: cancel.clone(), source_ip: peer_addr.ip(), domain: domain.clone(), - route_id: route_id.map(|s| s.to_string()), + route_id: route_config_id.map(|s| s.to_string()), }, ); @@ -1314,9 +1316,7 @@ impl TcpListenerManager { }; // Build metadata JSON - let route_key = route_match.route.name.as_deref() - .or(route_match.route.id.as_deref()) - .unwrap_or("unknown"); + let route_key = route_match.route.metrics_key().unwrap_or("unknown"); let metadata = serde_json::json!({ "routeKey": route_key, diff --git a/rust/crates/rustproxy-passthrough/src/udp_listener.rs b/rust/crates/rustproxy-passthrough/src/udp_listener.rs index 31b290c..f00ac34 100644 --- a/rust/crates/rustproxy-passthrough/src/udp_listener.rs +++ b/rust/crates/rustproxy-passthrough/src/udp_listener.rs @@ -617,7 +617,7 @@ impl UdpListenerManager { }; let route = route_match.route; - let route_id = route.name.as_deref().or(route.id.as_deref()); + let route_id = route.metrics_key(); // Socket handler routes → relay datagram to TS via persistent Unix socket if route.action.action_type == RouteActionType::SocketHandler { diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 6204620..e2a80f9 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartproxy', - version: '27.7.3', + version: '27.7.4', description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.' }