fix(rustproxy metrics): use stable route metrics keys across HTTP and passthrough listeners

This commit is contained in:
2026-04-14 09:17:55 +00:00
parent 30e5ab308f
commit 6c5180573a
7 changed files with 89 additions and 15 deletions
+8
View File
@@ -1,5 +1,13 @@
# Changelog # 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) ## 2026-04-14 - 27.7.3 - fix(repo)
no changes detected no changes detected
@@ -656,6 +656,11 @@ impl RouteConfig {
self.route_match.ports.to_ports() 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). /// Get the TLS mode for this route (from action-level or first target).
pub fn tls_mode(&self) -> Option<&crate::tls_types::TlsMode> { pub fn tls_mode(&self) -> Option<&crate::tls_types::TlsMode> {
// Check action-level TLS first // Check action-level TLS first
@@ -673,3 +678,63 @@ impl RouteConfig {
None 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);
}
}
@@ -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()) let ip_str = ip_string; // reuse from above (avoid redundant to_string())
self.metrics.record_http_request(); self.metrics.record_http_request();
if let Some(ref h) = host { if let Some(ref h) = host {
@@ -654,7 +655,7 @@ impl HttpProxyService {
.as_ref() .as_ref()
.filter(|rl| rl.enabled) .filter(|rl| rl.enabled)
.map(|rl| { .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 self.route_rate_limiters
.entry(route_key) .entry(route_key)
.or_insert_with(|| Arc::new(RateLimiter::new(rl.max_requests, rl.window))) .or_insert_with(|| Arc::new(RateLimiter::new(rl.max_requests, rl.window)))
@@ -420,7 +420,7 @@ pub async fn quic_accept_loop(
} }
conn_tracker.connection_opened(&ip); 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)); metrics.connection_opened(route_id.as_deref(), Some(&ip_str));
// Resolve per-route cancel token (child of global cancel) // Resolve per-route cancel token (child of global cancel)
@@ -541,7 +541,7 @@ async fn handle_quic_stream_forwarding(
real_client_addr: Option<SocketAddr>, real_client_addr: Option<SocketAddr>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let effective_addr = real_client_addr.unwrap_or_else(|| connection.remote_address()); 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; let metrics_arc = metrics;
// Resolve backend target // Resolve backend target
@@ -715,10 +715,11 @@ impl TcpListenerManager {
} else if let Some(target) = quick_match.target { } else if let Some(target) = quick_match.target {
let target_host = target.host.first().to_string(); let target_host = target.host.first().to_string();
let target_port = target.port.resolve(port); 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) // 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()) Some(id) => route_cancels.entry(id.to_string())
.or_insert_with(|| cancel.child_token()) .or_insert_with(|| cancel.child_token())
.clone(), .clone(),
@@ -733,7 +734,7 @@ impl TcpListenerManager {
cancel: conn_cancel.clone(), cancel: conn_cancel.clone(),
source_ip: peer_addr.ip(), source_ip: peer_addr.ip(),
domain: None, // fast path has no domain 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). // Resolve per-route cancel token (child of global cancel).
// When this route is removed via updateRoutes, the token is cancelled, // When this route is removed via updateRoutes, the token is cancelled,
// terminating all connections on this route. // 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()) Some(id) => route_cancels.entry(id.to_string())
.or_insert_with(|| cancel.child_token()) .or_insert_with(|| cancel.child_token())
.clone(), .clone(),
@@ -925,7 +927,7 @@ impl TcpListenerManager {
cancel: cancel.clone(), cancel: cancel.clone(),
source_ip: peer_addr.ip(), source_ip: peer_addr.ip(),
domain: domain.clone(), 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 // Build metadata JSON
let route_key = route_match.route.name.as_deref() let route_key = route_match.route.metrics_key().unwrap_or("unknown");
.or(route_match.route.id.as_deref())
.unwrap_or("unknown");
let metadata = serde_json::json!({ let metadata = serde_json::json!({
"routeKey": route_key, "routeKey": route_key,
@@ -617,7 +617,7 @@ impl UdpListenerManager {
}; };
let route = route_match.route; 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 // Socket handler routes → relay datagram to TS via persistent Unix socket
if route.action.action_type == RouteActionType::SocketHandler { if route.action.action_type == RouteActionType::SocketHandler {
+1 -1
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartproxy', 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.' 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.'
} }