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