fix(rustproxy metrics): use stable route metrics keys across HTTP and passthrough listeners
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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<SocketAddr>,
|
||||
) -> 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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user