Skip to main content

iroh_http_core/endpoint/
observe.rs

1//! Observability and peer-info methods on [`super::IrohEndpoint`].
2//!
3//! Split from `mod.rs` so the facade file stays ≤ 200 LoC. These methods
4//! are read-only and have no lifecycle side effects.
5
6use std::sync::atomic::Ordering;
7
8use iroh::endpoint::TransportAddrUsage;
9
10use super::{
11    stats::{EndpointStats, NodeAddrInfo, PathInfo, PeerStats},
12    IrohEndpoint,
13};
14
15impl IrohEndpoint {
16    /// Snapshot of current endpoint statistics.
17    ///
18    /// All fields are point-in-time reads and may change between calls.
19    pub fn endpoint_stats(&self) -> EndpointStats {
20        let (active_readers, active_writers, active_sessions, total_handles) =
21            self.inner.ffi.handles.count_handles();
22        let pool_size = self.inner.http.pool.entry_count_approx() as usize;
23        let active_connections = self.inner.http.active_connections.load(Ordering::Relaxed);
24        let active_requests = self.inner.http.active_requests.load(Ordering::Relaxed);
25        EndpointStats {
26            active_readers,
27            active_writers,
28            active_sessions,
29            total_handles,
30            pool_size,
31            active_connections,
32            active_requests,
33        }
34    }
35
36    /// Returns the local socket addresses this endpoint is bound to.
37    pub fn bound_sockets(&self) -> Vec<std::net::SocketAddr> {
38        self.inner.transport.ep.bound_sockets()
39    }
40
41    /// Full node address: node ID + relay URL(s) + direct socket addresses.
42    pub fn node_addr(&self) -> NodeAddrInfo {
43        let addr = self.inner.transport.ep.addr();
44        let mut addrs = Vec::new();
45        for relay in addr.relay_urls() {
46            addrs.push(relay.to_string());
47        }
48        for da in addr.ip_addrs() {
49            addrs.push(da.to_string());
50        }
51        NodeAddrInfo {
52            id: self.inner.transport.node_id_str.clone(),
53            addrs,
54        }
55    }
56
57    /// Home relay URL, or `None` if not connected to a relay.
58    pub fn home_relay(&self) -> Option<String> {
59        self.inner
60            .transport
61            .ep
62            .addr()
63            .relay_urls()
64            .next()
65            .map(|u| u.to_string())
66    }
67
68    /// Known addresses for a remote peer, or `None` if not in the endpoint's cache.
69    pub async fn peer_info(&self, node_id_b32: &str) -> Option<NodeAddrInfo> {
70        let bytes = crate::base32_decode(node_id_b32).ok()?;
71        let arr: [u8; 32] = bytes.try_into().ok()?;
72        let pk = iroh::PublicKey::from_bytes(&arr).ok()?;
73        let info = self.inner.transport.ep.remote_info(pk).await?;
74        let id = crate::base32_encode(info.id().as_bytes());
75        let mut addrs = Vec::new();
76        for a in info.addrs() {
77            match a.addr() {
78                iroh::TransportAddr::Ip(sock) => addrs.push(sock.to_string()),
79                iroh::TransportAddr::Relay(url) => addrs.push(url.to_string()),
80                other => addrs.push(format!("{:?}", other)),
81            }
82        }
83        Some(NodeAddrInfo { id, addrs })
84    }
85
86    /// Per-peer connection statistics.
87    ///
88    /// Returns path information for each known transport address, including
89    /// whether each path is via a relay or direct, and which is active.
90    pub async fn peer_stats(&self, node_id_b32: &str) -> Option<PeerStats> {
91        let bytes = crate::base32_decode(node_id_b32).ok()?;
92        let arr: [u8; 32] = bytes.try_into().ok()?;
93        let pk = iroh::PublicKey::from_bytes(&arr).ok()?;
94        let info = self.inner.transport.ep.remote_info(pk).await?;
95
96        let mut paths = Vec::new();
97        let mut has_active_relay = false;
98        let mut active_relay_url: Option<String> = None;
99
100        for a in info.addrs() {
101            let is_relay = a.addr().is_relay();
102            let is_active = matches!(a.usage(), TransportAddrUsage::Active);
103
104            let addr_str = match a.addr() {
105                iroh::TransportAddr::Ip(sock) => sock.to_string(),
106                iroh::TransportAddr::Relay(url) => {
107                    if is_active {
108                        has_active_relay = true;
109                        active_relay_url = Some(url.to_string());
110                    }
111                    url.to_string()
112                }
113                other => format!("{:?}", other),
114            };
115
116            paths.push(PathInfo {
117                relay: is_relay,
118                addr: addr_str,
119                active: is_active,
120            });
121        }
122
123        // Enrich with QUIC connection-level stats if a pooled connection exists.
124        let (rtt_ms, bytes_sent, bytes_received, lost_packets, sent_packets, congestion_window) =
125            if let Some(pooled) = self.inner.http.pool.get_existing(pk, crate::ALPN).await {
126                let s = pooled.conn.stats();
127                let rtt = pooled.conn.rtt(iroh::endpoint::PathId::ZERO);
128                (
129                    rtt.map(|d| d.as_secs_f64() * 1000.0),
130                    Some(s.udp_tx.bytes),
131                    Some(s.udp_rx.bytes),
132                    None,
133                    None,
134                    None,
135                )
136            } else {
137                (None, None, None, None, None, None)
138            };
139
140        Some(PeerStats {
141            relay: has_active_relay,
142            relay_url: active_relay_url,
143            paths,
144            rtt_ms,
145            bytes_sent,
146            bytes_received,
147            lost_packets,
148            sent_packets,
149            congestion_window,
150        })
151    }
152
153    /// Subscribe to path changes for a specific peer.
154    ///
155    /// Spawns a background watcher task the first time a given peer is subscribed.
156    /// The watcher polls `peer_stats()` every 200 ms and emits on the returned
157    /// channel whenever the active path changes.
158    pub fn subscribe_path_changes(
159        &self,
160        node_id_str: &str,
161    ) -> tokio::sync::mpsc::UnboundedReceiver<PathInfo> {
162        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
163        // Replace any existing sender; old watcher exits when it detects is_closed().
164        self.inner
165            .session
166            .path_subs
167            .insert(node_id_str.to_string(), tx);
168
169        let ep = self.clone();
170        let nid = node_id_str.to_string();
171        let event_tx = self.inner.session.event_tx.clone();
172
173        tokio::spawn(async move {
174            let mut last_key: Option<String> = None;
175            let mut closed_rx = ep.inner.session.closed_rx.clone();
176            loop {
177                // Exit immediately if the endpoint has been closed.
178                if *closed_rx.borrow() {
179                    ep.inner.session.path_subs.remove(&nid);
180                    break;
181                }
182                let is_closed = ep
183                    .inner
184                    .session
185                    .path_subs
186                    .get(&nid)
187                    .map(|s| s.is_closed())
188                    .unwrap_or(true);
189                if is_closed {
190                    ep.inner.session.path_subs.remove(&nid);
191                    break;
192                }
193
194                if let Some(stats) = ep.peer_stats(&nid).await {
195                    if let Some(active) = stats.paths.iter().find(|p| p.active) {
196                        let key = format!("{}:{}", active.relay, active.addr);
197                        if Some(&key) != last_key.as_ref() {
198                            last_key = Some(key);
199                            if let Some(sender) = ep.inner.session.path_subs.get(&nid) {
200                                let _ = sender.send(active.clone());
201                            }
202                            let _ = event_tx.try_send(
203                                crate::http::events::TransportEvent::path_change(
204                                    &nid,
205                                    &active.addr,
206                                    active.relay,
207                                ),
208                            );
209                        }
210                    }
211                }
212
213                // Sleep 200 ms, but wake early if the endpoint is being closed.
214                tokio::select! {
215                    _ = tokio::time::sleep(std::time::Duration::from_millis(200)) => {}
216                    result = closed_rx.wait_for(|v| *v) => {
217                        let _ = result;
218                        ep.inner.session.path_subs.remove(&nid);
219                        break;
220                    }
221                }
222            }
223        });
224
225        rx
226    }
227}