metrics: export each session's TCP connection figures
ci / docs (push) Canceled after 0s
ci / check (push) Canceled after 0s
ci / interop (push) Canceled after 0s
ci / fuzz (push) Canceled after 0s

The kernel's view of a session's TCP connection, which the status page
shows on request since the last change, could not be looked at after
the fact: a feed that crawled overnight left no record of whether the
peer's window or our own side held it up.

This change samples it at every scrape of `/metricz`: for each
established session, one `NETLINK_SOCK_DIAG` round trip, and twenty
gauges labelled by peer, `bgp_peer_tcp_*`: smoothed, least and variance
of the round-trip time; the congestion window and segment size; the
peer's receive window and ours; the send queue, the part of it not yet
sent, the receive queue and the segments in flight; retransmitted and
lost segments; the busy time and the time held up by the peer's window
and by our send buffer, cumulative, so that their rates give the share
of time a session is held up; octets sent and received; and the
delivery and pacing rates. A session the kernel has no connection for
loses its series, as does one that ends.

`SessionHandle::metric_label` is the one place a peer's metric label is
made (its name, or its address), used by the advertiser's gauge and here.

Documentation: the metrics reference lists them; the monitoring guide
gives the query for the share of time a session is held up by the
neighbor's receive window, which is the question the overnight feed
raised.

Testing: a daemon in the Linux VM with one session shows the series and
drops them when the session ends; the lab's basics phase checks the
scrape for the FRR session's round-trip time; the workspace tests pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-09 12:12:53 +02:00
1 parent 99a47da2af
commit d7d7d39bb9
8 files changed
+244 -11

No files matched your search

+1 -7
View File
@@ -51,7 +51,6 @@ use tokio::sync::{Notify, OwnedSemaphorePermit, Semaphore, watch};
use tokio::task::JoinHandle;
use tracing::{debug, error, info, warn};
use crate::address::PeerAddress;
use crate::error::SessionError;
use crate::export::{self, Egress, Outgoing, Reflection, Source};
use crate::family::FamilySet;
@@ -175,12 +174,7 @@ impl Advertiser {
.as_ref()
.map(|_| session.families.clone()),
converged: Some(self.converged.clone()).filter(|converged| !*converged.borrow()),
// The peer goes by its configured name, or by its address, as
// in its other metrics.
advertised_metric: PEER_ADVERTISED.with_label_values(&[&session
.name
.clone()
.unwrap_or_else(|| PeerAddress::from(session.peer.addr).to_string())]),
advertised_metric: PEER_ADVERTISED.with_label_values(&[&session.metric_label()]),
};
let running = Running {
session: session.clone(),
+2 -1
View File
@@ -44,7 +44,7 @@ use crate::fsm::{Action, Event, Fsm, FsmConfig, Negotiated, Received};
use crate::holddown::{HeldDown, HoldDowns};
use crate::metrics::{
EXPORT_LIMIT_EXCEEDED, MESSAGES_RECEIVED, MESSAGES_SENT, PEER_ADVERTISED, PEER_ROUTES,
PREFIX_LIMIT_EXCEEDED, SESSIONS_ESTABLISHED_TOTAL, UPDATE_ERRORS, message_type_name,
PEER_TCP, PREFIX_LIMIT_EXCEEDED, SESSIONS_ESTABLISHED_TOTAL, UPDATE_ERRORS, message_type_name,
};
use crate::panic::contained;
use crate::policy::{self, ExportLimits, ExportPolicy, ImportPolicy, PeerRole};
@@ -316,6 +316,7 @@ async fn release_routes(
// Fails only if the session never set it, which is nothing to do.
let _ = PEER_ROUTES.remove_label_values(&[session.rejections.label()]);
let _ = PEER_ADVERTISED.remove_label_values(&[session.rejections.label()]);
PEER_TCP.remove(session.rejections.label());
if *session.leave.borrow() == Some(Leave::Restart) {
// The daemon is about to go, and its routes with it; the kernel
// keeps its own. Taking them out first would only hold the
+136 -2
View File
@@ -4,8 +4,8 @@ use std::sync::LazyLock;
use bgp_packet::message::Message;
use prometheus::{
IntCounter, IntCounterVec, IntGauge, IntGaugeVec, register_int_counter,
register_int_counter_vec, register_int_gauge, register_int_gauge_vec,
GaugeVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, register_gauge_vec,
register_int_counter, register_int_counter_vec, register_int_gauge, register_int_gauge_vec,
};
pub static MESSAGES_SENT: LazyLock<IntCounterVec> = LazyLock::new(|| {
@@ -78,6 +78,139 @@ pub static PEER_ADVERTISED: LazyLock<IntGaugeVec> = LazyLock::new(|| {
.expect("metric registration")
});
/// A session's TCP connection as the kernel reports it (`TCP_INFO`),
/// sampled when the metrics are scraped. Seconds, octets, segments, and
/// octets per second; the busy, limited and byte figures are cumulative
/// over the connection's life.
pub struct PeerTcp {
pub rtt_seconds: GaugeVec,
pub min_rtt_seconds: GaugeVec,
pub rtt_variance_seconds: GaugeVec,
pub cwnd_segments: IntGaugeVec,
pub mss_bytes: IntGaugeVec,
pub send_window_bytes: IntGaugeVec,
pub receive_window_bytes: IntGaugeVec,
pub send_queue_bytes: IntGaugeVec,
pub not_sent_bytes: IntGaugeVec,
pub receive_queue_bytes: IntGaugeVec,
pub unacked_segments: IntGaugeVec,
pub retransmitted_segments: IntGaugeVec,
pub lost_segments: IntGaugeVec,
pub busy_seconds: GaugeVec,
pub receive_window_limited_seconds: GaugeVec,
pub send_buffer_limited_seconds: GaugeVec,
pub sent_bytes: IntGaugeVec,
pub received_bytes: IntGaugeVec,
pub delivery_rate_bytes_per_second: IntGaugeVec,
pub pacing_rate_bytes_per_second: IntGaugeVec,
}
impl PeerTcp {
/// Forgets the peer's series, when its session has ended.
pub fn remove(&self, label: &str) {
for gauge in [
&self.rtt_seconds,
&self.min_rtt_seconds,
&self.rtt_variance_seconds,
&self.busy_seconds,
&self.receive_window_limited_seconds,
&self.send_buffer_limited_seconds,
] {
let _ = gauge.remove_label_values(&[label]);
}
for gauge in [
&self.cwnd_segments,
&self.mss_bytes,
&self.send_window_bytes,
&self.receive_window_bytes,
&self.send_queue_bytes,
&self.not_sent_bytes,
&self.receive_queue_bytes,
&self.unacked_segments,
&self.retransmitted_segments,
&self.lost_segments,
&self.sent_bytes,
&self.received_bytes,
&self.delivery_rate_bytes_per_second,
&self.pacing_rate_bytes_per_second,
] {
let _ = gauge.remove_label_values(&[label]);
}
}
}
pub static PEER_TCP: LazyLock<PeerTcp> = LazyLock::new(|| {
let seconds = |name: &str, help: &str| {
register_gauge_vec!(format!("bgp_peer_tcp_{name}"), help, &["peer"])
.expect("metric registration")
};
let count = |name: &str, help: &str| {
register_int_gauge_vec!(format!("bgp_peer_tcp_{name}"), help, &["peer"])
.expect("metric registration")
};
PeerTcp {
rtt_seconds: seconds(
"rtt_seconds",
"Smoothed round-trip time of the session's TCP connection",
),
min_rtt_seconds: seconds(
"min_rtt_seconds",
"Least round-trip time seen on the connection",
),
rtt_variance_seconds: seconds(
"rtt_variance_seconds",
"Round-trip time variance of the connection",
),
cwnd_segments: count("cwnd_segments", "Congestion window, in segments"),
mss_bytes: count("mss_bytes", "Maximum segment size"),
send_window_bytes: count(
"send_window_bytes",
"The peer's receive window: what it lets us send",
),
receive_window_bytes: count(
"receive_window_bytes",
"Our receive window: what we let the peer send",
),
send_queue_bytes: count(
"send_queue_bytes",
"Octets written and not yet acknowledged by the peer",
),
not_sent_bytes: count("not_sent_bytes", "Octets written and not yet sent"),
receive_queue_bytes: count("receive_queue_bytes", "Octets received and not yet read"),
unacked_segments: count("unacked_segments", "Segments sent and not yet acknowledged"),
retransmitted_segments: count(
"retransmitted_segments",
"Segments retransmitted over the connection's life",
),
lost_segments: count("lost_segments", "Segments thought lost now"),
busy_seconds: seconds(
"busy_seconds",
"Time the connection had something to send, over its life",
),
receive_window_limited_seconds: seconds(
"receive_window_limited_seconds",
"Of the busy time, time held up by the peer's receive window",
),
send_buffer_limited_seconds: seconds(
"send_buffer_limited_seconds",
"Of the busy time, time held up by our send buffer being full",
),
sent_bytes: count("sent_bytes", "Octets sent over the connection's life"),
received_bytes: count(
"received_bytes",
"Octets received over the connection's life",
),
delivery_rate_bytes_per_second: count(
"delivery_rate_bytes_per_second",
"Delivery rate the kernel measured most recently",
),
pacing_rate_bytes_per_second: count(
"pacing_rate_bytes_per_second",
"Rate the kernel paces the connection at",
),
}
});
pub static PEER_MAX_PREFIXES: LazyLock<IntGaugeVec> = LazyLock::new(|| {
register_int_gauge_vec!(
"bgp_peer_max_prefixes",
@@ -238,6 +371,7 @@ pub fn init() {
LazyLock::force(&ROUTES_EXPORTED);
LazyLock::force(&PEER_UP);
LazyLock::force(&PEER_ADVERTISED);
LazyLock::force(&PEER_TCP);
BUILD_INFO
.with_label_values(&[env!("CARGO_PKG_VERSION")])
.set(1);
+8
View File
@@ -296,6 +296,14 @@ impl SessionHandle {
/// Whether `other` is a handle to this same session, and not to another
/// session with the same peer.
/// What the peer goes by in the metrics: its configured name, or its
/// address.
pub fn metric_label(&self) -> String {
self.name
.clone()
.unwrap_or_else(|| crate::address::PeerAddress::from(self.peer.addr).to_string())
}
pub fn is_same_session(&self, other: &SessionHandle) -> bool {
self.outbound.same_channel(&other.outbound)
}
+80 -1
View File
@@ -5,7 +5,7 @@ use axum::extract::State;
use axum::http::{StatusCode, header};
use axum::response::IntoResponse;
use bgp_server::metrics::{
LOCAL_ROUTES, RIB_ATTRIBUTE_SETS, RIB_JOURNAL_ENTRIES, RIB_PREFIXES, RIB_ROUTES,
LOCAL_ROUTES, PEER_TCP, RIB_ATTRIBUTE_SETS, RIB_JOURNAL_ENTRIES, RIB_PREFIXES, RIB_ROUTES,
};
use prometheus::{Encoder, TextEncoder};
@@ -24,6 +24,7 @@ pub async fn metricz(State(state): State<AppState>) -> impl IntoResponse {
RIB_PREFIXES.set(gauge(state.rib.prefix_count()));
RIB_ATTRIBUTE_SETS.set(gauge(state.rib.attrs().len()));
RIB_JOURNAL_ENTRIES.set(gauge(state.rib.journal_len()));
sample_tcp(&state).await;
let mut buf = Vec::new();
let encoder = TextEncoder::new();
@@ -37,3 +38,81 @@ pub async fn metricz(State(state): State<AppState>) -> impl IntoResponse {
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
}
/// Asks the kernel about every established session's TCP connection and
/// sets the peer's gauges: one netlink round trip a session, at scrape
/// time, so that there is history to look back on. A session the kernel
/// has no connection for any more loses its series.
async fn sample_tcp(state: &AppState) {
let tcp = &*PEER_TCP;
for session in state.registry.list() {
let label = session.metric_label();
let connection =
bgp_fib::sockdiag::tcp_connection(session.local_addr, session.peer.addr).await;
let Ok(Some(c)) = connection else {
tcp.remove(&label);
continue;
};
let seconds = |us: u64| us as f64 / 1e6;
let int = |n: u64| i64::try_from(n).unwrap_or(i64::MAX);
let label = [label.as_str()];
tcp.rtt_seconds
.with_label_values(&label)
.set(seconds(c.rtt.into()));
tcp.min_rtt_seconds
.with_label_values(&label)
.set(seconds(c.min_rtt.into()));
tcp.rtt_variance_seconds
.with_label_values(&label)
.set(seconds(c.rtt_variance.into()));
tcp.cwnd_segments
.with_label_values(&label)
.set(c.cwnd.into());
tcp.mss_bytes.with_label_values(&label).set(c.mss.into());
tcp.send_window_bytes
.with_label_values(&label)
.set(c.send_window.into());
tcp.receive_window_bytes
.with_label_values(&label)
.set(c.receive_window.into());
tcp.send_queue_bytes
.with_label_values(&label)
.set(c.send_queue.into());
tcp.not_sent_bytes
.with_label_values(&label)
.set(c.not_sent.into());
tcp.receive_queue_bytes
.with_label_values(&label)
.set(c.recv_queue.into());
tcp.unacked_segments
.with_label_values(&label)
.set(c.unacked.into());
tcp.retransmitted_segments
.with_label_values(&label)
.set(c.total_retrans.into());
tcp.lost_segments
.with_label_values(&label)
.set(c.lost.into());
tcp.busy_seconds
.with_label_values(&label)
.set(seconds(c.busy));
tcp.receive_window_limited_seconds
.with_label_values(&label)
.set(seconds(c.limited_by_receive_window));
tcp.send_buffer_limited_seconds
.with_label_values(&label)
.set(seconds(c.limited_by_send_buffer));
tcp.sent_bytes
.with_label_values(&label)
.set(int(c.bytes_sent));
tcp.received_bytes
.with_label_values(&label)
.set(int(c.bytes_received));
tcp.delivery_rate_bytes_per_second
.with_label_values(&label)
.set(int(c.delivery_rate));
tcp.pacing_rate_bytes_per_second
.with_label_values(&label)
.set(int(c.pacing_rate));
}
}