fix(metrics): keep export failures from delaying rate restoration
Build / build (pull_request) Canceled after 0s
DevSkim / DevSkim (pull_request) Canceled after 0s
rust-clippy analyze / Run rust-clippy analyzing (pull_request) Canceled after 0s

This commit is contained in:
Nils Andreas Svee committed 2026-09-24 17:01:50 +07:00
1 parent 8fb051d48f
commit 6773b6ed4f
2 files changed
+199 -47

No files matched your search

+8
View File
@@ -450,7 +450,15 @@ fn run(settings: &Settings) -> anyhow::Result<()> {
drop(metrics_tx); drop(metrics_tx);
if let Some(handle) = metrics_thread_handle { if let Some(handle) = metrics_thread_handle {
let deadline = Instant::now() + Duration::from_secs(1);
while !handle.is_finished() && Instant::now() < deadline {
sleep(Duration::from_millis(20));
}
if handle.is_finished() {
let _ = handle.join(); let _ = handle.join();
} else {
warn!("Metrics exporter did not stop within one second");
}
} }
restore_shaper( restore_shaper(
+191 -47
View File
@@ -2,6 +2,7 @@
// //
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
use crate::SHUTDOWN;
use crate::settings::Settings; use crate::settings::Settings;
use crate::settings::{MeasurementType, ObservabilityProtocol}; use crate::settings::{MeasurementType, ObservabilityProtocol};
use crate::time::Time; use crate::time::Time;
@@ -12,85 +13,152 @@ use std::fmt::Write;
use std::net::{IpAddr, TcpStream, UdpSocket}; use std::net::{IpAddr, TcpStream, UdpSocket};
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration; use std::time::{Duration, Instant};
const MAX_RECONNECT_BACKOFF: u64 = 60; const MAX_RECONNECT_BACKOFF: u64 = 60;
enum Transport { struct Endpoint {
Udp(UdpSocket),
Tcp {
stream: Option<TcpStream>,
host: String, host: String,
port: u16, port: u16,
reconnect_backoff: u64, backoff: u64,
next_attempt: Option<Instant>,
}
impl Endpoint {
fn new(host: &str, port: u16) -> Self {
Self {
host: host.into(),
port,
backoff: 1,
next_attempt: None,
}
}
fn failed(&mut self) -> u64 {
let delay = self.backoff;
self.next_attempt = Some(Instant::now() + Duration::from_secs(delay));
self.backoff = (self.backoff * 2).min(MAX_RECONNECT_BACKOFF);
delay
}
fn connected(&mut self) {
self.backoff = 1;
self.next_attempt = None;
}
}
enum Transport {
Udp {
socket: Option<UdpSocket>,
endpoint: Endpoint,
},
Tcp {
stream: Option<TcpStream>,
endpoint: Endpoint,
}, },
} }
impl Transport { impl Transport {
fn new_udp(host: &str, port: u16) -> anyhow::Result<Self> { fn new_udp(host: &str, port: u16) -> Self {
let socket = UdpSocket::bind("0.0.0.0:0")?; Transport::Udp {
socket.set_write_timeout(Some(Duration::from_millis(100)))?; socket: None,
socket.connect((host, port))?; endpoint: Endpoint::new(host, port),
Ok(Transport::Udp(socket)) }
} }
fn new_tcp(host: &str, port: u16) -> Self { fn new_tcp(host: &str, port: u16) -> Self {
Transport::Tcp { Transport::Tcp {
stream: None, stream: None,
host: host.to_string(), endpoint: Endpoint::new(host, port),
port,
reconnect_backoff: 1,
} }
} }
fn send(&mut self, data: &str) { fn send(&mut self, data: &str) {
match self { match self {
Transport::Udp(socket) => { Transport::Udp { socket, endpoint } => {
if let Err(e) = socket.send(data.as_bytes()) { if socket.is_none() {
warn!("UDP send failed: {}", e); if endpoint.next_attempt.is_some_and(|at| Instant::now() < at) {
return;
}
let opened = UdpSocket::bind("0.0.0.0:0").and_then(|socket| {
socket.set_write_timeout(Some(Duration::from_millis(100)))?;
socket.connect((endpoint.host.as_str(), endpoint.port))?;
Ok(socket)
});
match opened {
Ok(opened) => {
*socket = Some(opened);
endpoint.connected();
}
Err(error) => {
let delay = endpoint.failed();
warn!(
"Metrics UDP connect to {}:{} failed: {error}; retrying in {delay}s",
endpoint.host, endpoint.port
);
return;
} }
} }
Transport::Tcp { }
stream, if let Some(error) = socket
host, .as_ref()
port, .and_then(|socket| socket.send(data.as_bytes()).err())
reconnect_backoff, {
} => { *socket = None;
let delay = endpoint.failed();
warn!("Metrics UDP send failed: {error}; retrying in {delay}s");
}
}
Transport::Tcp { stream, endpoint } => {
if stream.is_none() { if stream.is_none() {
match TcpStream::connect((host.as_str(), *port)) { if endpoint.next_attempt.is_some_and(|at| Instant::now() < at) {
return;
}
match TcpStream::connect((endpoint.host.as_str(), endpoint.port)) {
Ok(s) => { Ok(s) => {
s.set_write_timeout(Some(Duration::from_millis(500))).ok(); s.set_write_timeout(Some(Duration::from_millis(500))).ok();
info!("Connected to metrics collector at {}:{}", host, port); info!(
"Connected to metrics collector at {}:{}",
endpoint.host, endpoint.port
);
*stream = Some(s); *stream = Some(s);
*reconnect_backoff = 1; endpoint.connected();
} }
Err(e) => { Err(e) => {
let delay = endpoint.failed();
warn!( warn!(
"Failed to connect to {}:{} - {}, backoff {}s", "Metrics TCP connect to {}:{} failed: {e}; retrying in {delay}s",
host, port, e, reconnect_backoff endpoint.host, endpoint.port
); );
*reconnect_backoff =
(*reconnect_backoff * 2).min(MAX_RECONNECT_BACKOFF);
return; return;
} }
} }
} }
let full_data = format!("{}\n", data);
// take the stream out, write, put it back or set None on failure
let write_error = stream let write_error = stream
.as_mut() .as_mut()
.and_then(|s| std::io::Write::write_all(s, full_data.as_bytes()).err()); .and_then(|s| std::io::Write::write_all(s, data.as_bytes()).err());
if let Some(e) = write_error { if let Some(e) = write_error {
warn!("TCP send failed: {}", e); warn!("TCP send failed: {}", e);
*stream = None; *stream = None;
*reconnect_backoff = (*reconnect_backoff * 2).min(MAX_RECONNECT_BACKOFF); endpoint.failed();
} }
} }
} }
} }
} }
fn escape_tag_value(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
for character in value.chars() {
if matches!(character, ',' | '=' | ' ') {
escaped.push('\\');
}
escaped.push(character);
}
escaped
}
pub enum Metric { pub enum Metric {
Ping { Ping {
reflector: IpAddr, reflector: IpAddr,
@@ -180,7 +248,7 @@ impl Metrics {
let mut transport = match self.settings.observability.protocol { let mut transport = match self.settings.observability.protocol {
ObservabilityProtocol::Udp => { ObservabilityProtocol::Udp => {
info!("Metrics exporter configured for UDP to {}:{}", host, port); info!("Metrics exporter configured for UDP to {}:{}", host, port);
Transport::new_udp(&host, port)? Transport::new_udp(&host, port)
} }
ObservabilityProtocol::Tcp => { ObservabilityProtocol::Tcp => {
info!("Metrics exporter configured for TCP to {}:{}", host, port); info!("Metrics exporter configured for TCP to {}:{}", host, port);
@@ -189,12 +257,30 @@ impl Metrics {
}; };
let mut batch = Vec::with_capacity(batch_size); let mut batch = Vec::with_capacity(batch_size);
let mut shutdown_deadline = None;
let mut last_flush = Instant::now();
loop { loop {
// block until first metric or timeout if SHUTDOWN.load(Ordering::Relaxed) {
match self.metrics_rx.recv_timeout(timeout) { let deadline = shutdown_deadline
.get_or_insert_with(|| Instant::now() + Duration::from_millis(400));
if Instant::now() >= *deadline {
if !batch.is_empty() {
self.flush(&mut transport, &batch, host_tag);
}
break;
}
}
match self
.metrics_rx
.recv_timeout(timeout.min(Duration::from_millis(100)))
{
Ok(metric) => batch.push(metric), Ok(metric) => batch.push(metric),
Err(RecvTimeoutError::Timeout) => { Err(RecvTimeoutError::Timeout) => {
if last_flush.elapsed() < timeout && shutdown_deadline.is_none() {
continue;
}
let dropped = self.metrics_dropped.swap(0, Ordering::Relaxed); let dropped = self.metrics_dropped.swap(0, Ordering::Relaxed);
if dropped > 0 { if dropped > 0 {
let ts = Time::new(ClockId::Realtime).as_nanos(); let ts = Time::new(ClockId::Realtime).as_nanos();
@@ -204,9 +290,15 @@ impl Metrics {
self.flush(&mut transport, &batch, host_tag); self.flush(&mut transport, &batch, host_tag);
batch.clear(); batch.clear();
} }
last_flush = Instant::now();
continue; continue;
} }
Err(RecvTimeoutError::Disconnected) => break, Err(RecvTimeoutError::Disconnected) => {
if !batch.is_empty() {
self.flush(&mut transport, &batch, host_tag);
}
break;
}
} }
// greedily drain without blocking // greedily drain without blocking
@@ -220,6 +312,7 @@ impl Metrics {
if batch.len() >= batch_size { if batch.len() >= batch_size {
self.flush(&mut transport, &batch, host_tag); self.flush(&mut transport, &batch, host_tag);
batch.clear(); batch.clear();
last_flush = Instant::now();
} }
} }
@@ -227,30 +320,31 @@ impl Metrics {
} }
fn flush(&self, transport: &mut Transport, batch: &[(Metric, u64)], host_tag: &str) { fn flush(&self, transport: &mut Transport, batch: &[(Metric, u64)], host_tag: &str) {
let host_tag = escape_tag_value(host_tag);
match transport { match transport {
/* /*
* the UDP receive buffer might be too small on the receiver to accept batched data, * the UDP receive buffer might be too small on the receiver to accept batched data,
* and it'll get cutoff in the middle of a record. so send metrics directly on UDP * and it'll get cutoff in the middle of a record. so send metrics directly on UDP
*/ */
Transport::Udp(_) => { Transport::Udp { .. } => {
let mut data = String::with_capacity(300); let mut data = String::with_capacity(300);
for (metric, timestamp_ns) in batch { for (metric, timestamp_ns) in batch {
data.clear(); data.clear();
self.write_lines(metric, *timestamp_ns, host_tag, &mut data); Self::write_lines(metric, *timestamp_ns, &host_tag, &mut data);
transport.send(&data); transport.send(&data);
} }
} }
Transport::Tcp { .. } => { Transport::Tcp { .. } => {
let mut data = String::with_capacity(batch.len() * 300); let mut data = String::with_capacity(batch.len() * 300);
for (metric, timestamp_ns) in batch { for (metric, timestamp_ns) in batch {
self.write_lines(metric, *timestamp_ns, host_tag, &mut data); Self::write_lines(metric, *timestamp_ns, &host_tag, &mut data);
} }
transport.send(&data); transport.send(&data);
} }
} }
} }
fn write_lines(&self, metric: &Metric, timestamp_ns: u64, host_tag: &str, out: &mut String) { fn write_lines(metric: &Metric, timestamp_ns: u64, host_tag: &str, out: &mut String) {
match metric { match metric {
Metric::Ping { Metric::Ping {
reflector, reflector,
@@ -262,7 +356,7 @@ impl Metrics {
writeln!( writeln!(
out, out,
"sqm_ping,host={host_tag},reflector={reflector},type={measurement_type} \ "sqm_ping,host={host_tag},reflector={reflector},type={measurement_type} \
rtt={rtt:.3},up_time={up_time:.3},down_time={down_time:.3} {timestamp_ns}i" rtt={rtt:.3},up_time={up_time:.3},down_time={down_time:.3} {timestamp_ns}"
) )
.unwrap_or(()); .unwrap_or(());
} }
@@ -276,11 +370,11 @@ impl Metrics {
} => { } => {
writeln!(out, writeln!(out,
"sqm_rate,host={host_tag},direction=download \ "sqm_rate,host={host_tag},direction=download \
rate_kbps={dl_rate:.0},load={rx_load:.4},delta_delay={delta_delay_down:.3} {timestamp_ns}i" rate_kbps={dl_rate:.0},load={rx_load:.4},delta_delay={delta_delay_down:.3} {timestamp_ns}"
).unwrap_or(()); ).unwrap_or(());
writeln!(out, writeln!(out,
"sqm_rate,host={host_tag},direction=upload \ "sqm_rate,host={host_tag},direction=upload \
rate_kbps={ul_rate:.0},load={tx_load:.4},delta_delay={delta_delay_up:.3} {timestamp_ns}i" rate_kbps={ul_rate:.0},load={tx_load:.4},delta_delay={delta_delay_up:.3} {timestamp_ns}"
).unwrap_or(()); ).unwrap_or(());
} }
Metric::Baseline { Metric::Baseline {
@@ -292,11 +386,11 @@ impl Metrics {
} => { } => {
writeln!(out, writeln!(out,
"sqm_baseline,host={host_tag},reflector={reflector},direction=up \ "sqm_baseline,host={host_tag},reflector={reflector},direction=up \
baseline_ewma={baseline_up_ewma:.3},recent_ewma={recent_up_ewma:.3} {timestamp_ns}i" baseline_ewma={baseline_up_ewma:.3},recent_ewma={recent_up_ewma:.3} {timestamp_ns}"
).unwrap_or(()); ).unwrap_or(());
writeln!(out, writeln!(out,
"sqm_baseline,host={host_tag},reflector={reflector},direction=down \ "sqm_baseline,host={host_tag},reflector={reflector},direction=down \
baseline_ewma={baseline_down_ewma:.3},recent_ewma={recent_down_ewma:.3} {timestamp_ns}i" baseline_ewma={baseline_down_ewma:.3},recent_ewma={recent_down_ewma:.3} {timestamp_ns}"
).unwrap_or(()); ).unwrap_or(());
} }
Metric::Event { Metric::Event {
@@ -331,3 +425,53 @@ impl Metrics {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn line_protocol_uses_bare_timestamps_and_escaped_tags() {
let reflector = "192.0.2.1".parse().unwrap();
let metrics = [
Metric::Ping {
reflector,
measurement_type: MeasurementType::Icmp,
rtt: 2.0,
up_time: 1.0,
down_time: 1.0,
},
Metric::Rate {
dl_rate: 100.0,
ul_rate: 50.0,
rx_load: 0.2,
tx_load: 0.3,
delta_delay_down: 1.0,
delta_delay_up: 2.0,
},
Metric::Baseline {
reflector,
baseline_up_ewma: 1.0,
baseline_down_ewma: 2.0,
recent_up_ewma: 1.0,
recent_down_ewma: 2.0,
},
Metric::Event {
name: "started",
reason: "",
reflector: None,
tags: &[],
},
Metric::Dropped { count: 1 },
];
let host = escape_tag_value("a b,c=d");
for metric in metrics {
let mut output = String::new();
Metrics::write_lines(&metric, 123, &host, &mut output);
for line in output.lines() {
assert!(line.contains("host=a\\ b\\,c\\=d"), "{line}");
assert!(line.ends_with(" 123"), "{line}");
}
}
}
}