Monitoring Connection Stats and Path Metrics in iroh: A Complete Guide
Iroh provides a built-in, lock-free metrics collection system that exposes real-time counters for socket traffic, connection lifecycle events, and path discovery through the Endpoint::metrics() API.
Monitoring connection stats and path metrics in iroh is essential for debugging network performance and understanding how your application utilizes direct versus relayed connections. The library ships with a comprehensive instrumentation layer located in the iroh::metrics module that automatically tracks bytes sent over IPv4, IPv6, and relay paths, as well as hole-punching attempts and connection state changes. These metrics are maintained in memory using monotonic counters and are exposed via a public, zero-cost API on the Endpoint struct.
Architecture of the Metrics System
The monitoring stack is split between socket-level telemetry and endpoint-level aggregation, both implemented using the iroh_metrics crate's Counter type and #[metrics] macro.
Socket-Level Metrics (Metrics)
The core counters reside in iroh/src/socket/metrics.rs within the Metrics struct. This structure tracks low-level network activity including:
- Traffic counters:
send_ipv4,send_ipv6,send_relay, and corresponding receive counters - Path changes:
transport_ip_paths_added,transport_relay_paths_removed - Hole-punching:
holepunch_attemptsand success indicators - Actor loop statistics:
actor_tick_main,actor_tick_msg,actor_tick_re_stun
These counters implement the MetricsGroup trait and are wrapped in Arc for thread-safe sharing across the socket's internal transports.
Endpoint-Level Aggregation (EndpointMetrics)
Defined in iroh/src/metrics.rs, EndpointMetrics wraps the socket-level Metrics and adds connection-specific counters such as num_conns_opened, num_conns_closed, paths_direct, paths_relay, and paths_custom. The Endpoint::metrics() method in iroh/src/endpoint.rs returns a reference to this struct, allowing applications to query aggregated statistics at any time.
Transport Layer Integration
Each transport implementation—IP (iroh/src/socket/transports/ip.rs), Relay (iroh/src/socket/transports/relay/actor.rs), and custom transports—receives a shared Arc<Metrics> instance. When a transport sends or receives data, it increments the relevant counter directly in the hot path (e.g., self.metrics.send_ipv4.inc_by(bytes)), ensuring metrics reflect real-time activity without locking overhead.
How Metrics Are Collected
Collection happens automatically during normal endpoint operation through five primary stages:
- Endpoint Initialization: When
Endpoint::builder().bind_default().awaitsucceeds, the constructor initializesEndpointMetrics::default(), creating fresh counters for the socket's lifecycle. - Data Plane Events: The IP transport increments
send_ipv4orsend_ipv6for every datagram sent, while the Relay actor updatessend_relaywhen traffic flows through fallback servers. - Connection Lifecycle: The socket actor increments
num_conns_openedonly after a successful TLS handshake completes, and incrementsnum_conns_closedwhen the connection terminates. - Path Discovery: As the endpoint discovers direct paths or falls back to relays, it updates
paths_direct,paths_relay, andholepunch_attemptscounters. - Actor Loop Telemetry: The main async loop in the relay actor periodically increments
actor_tick_main,actor_tick_msg, and timer-related counters to expose internal scheduling health.
Querying Connection Metrics
Access metrics by calling Endpoint::metrics(), which returns a snapshot of all current counters. Because the underlying Counter type uses atomics, you can safely read metrics from multiple threads without blocking the endpoint's event loop.
use iroh::Endpoint;
use std::time::Duration;
use tokio::time;
// Build an endpoint (metrics are enabled by default)
let endpoint = Endpoint::builder()
.bind_default()
.await
.expect("failed to start iroh endpoint");
// Spawn a task to periodically log statistics
tokio::spawn(async move {
loop {
let metrics = endpoint.metrics();
// Traffic statistics
println!("IPv4 sent: {} bytes", metrics.socket.send_ipv4.get());
println!("IPv6 sent: {} bytes", metrics.socket.send_ipv6.get());
println!("Relay sent: {} bytes", metrics.socket.send_relay.get());
// Connection lifecycle
println!("Connections opened: {}", metrics.num_conns_opened.get());
println!("Connections closed: {}", metrics.num_conns_closed.get());
// Path discovery
println!("Direct paths: {}", metrics.paths_direct.get());
println!("Relay paths: {}", metrics.paths_relay.get());
time::sleep(Duration::from_secs(10)).await;
}
});
Exporting Metrics to External Systems
Because EndpointMetrics implements serde::Serialize, you can serialize snapshots to JSON for ingestion by Prometheus, Grafana, or custom observability pipelines.
use serde_json::json;
let snapshot = endpoint.metrics();
let payload = json!({
"send_ipv4": snapshot.socket.send_ipv4.get(),
"send_ipv6": snapshot.socket.send_ipv6.get(),
"send_relay": snapshot.socket.send_relay.get(),
"connections_opened": snapshot.num_conns_opened.get(),
"connections_closed": snapshot.num_conns_closed.get(),
"direct_paths": snapshot.paths_direct.get(),
"relay_paths": snapshot.paths_relay.get(),
});
println!("{}", serde_json::to_string_pretty(&payload).unwrap());
Integrating with Tracing
Iroh emits internal tracing events for certain metric thresholds. To capture these, initialize a subscriber before starting the endpoint:
use tracing_subscriber::FmtSubscriber;
let subscriber = FmtSubscriber::builder()
.with_max_level(tracing::Level::INFO)
.finish();
tracing::subscriber::set_global_default(subscriber)
.expect("setting default subscriber failed");
This surfaces diagnostic events emitted by the socket actor and transport layers without requiring manual metric polling.
Summary
- Lock-free counters: Iroh uses the
iroh_metricscrate'sCountertype for zero-overhead, atomic metric collection across threads. - Two-tier architecture:
Metricstracks socket-level traffic iniroh/src/socket/metrics.rs, whileEndpointMetricsaggregates connection state iniroh/src/metrics.rs. - Public API: Call
Endpoint::metrics()to retrieve a serializable snapshot of all counters, including bytes sent via IPv4, IPv6, and relay paths. - Automatic collection: Transports increment counters directly during send/receive operations, ensuring real-time accuracy without manual instrumentation.
- Serialization ready: The metrics struct implements
serde::Serialize, enabling easy export to JSON or binary formats likepostcardfor remote diagnostics.
Frequently Asked Questions
How do I access connection metrics in iroh?
Call the Endpoint::metrics() method on your endpoint instance. This returns an EndpointMetrics struct containing socket-level traffic counters (send_ipv4, send_relay) and connection lifecycle counters (num_conns_opened, paths_direct). Access individual values using the .get() method on each Counter field.
What is the performance impact of enabling metrics?
The impact is negligible. Metrics are enabled by default and use lock-free atomic counters from the iroh_metrics crate. Increments occur via Arc<Metrics> shared across threads, and reads through Endpoint::metrics() do not block the socket's async event loop.
Can I export iroh metrics to Prometheus?
Yes. Since EndpointMetrics implements serde::Serialize, you can serialize the struct to JSON and convert it to Prometheus exposition format. Poll endpoint.metrics() periodically in your application code and expose the values via your existing metrics endpoint.
What is the difference between socket-level and endpoint-level metrics?
Socket-level metrics (defined in iroh/src/socket/metrics.rs) track raw network activity such as bytes sent and path changes. Endpoint-level metrics (defined in iroh/src/metrics.rs) aggregate these socket counters and add higher-level connection state tracking, such as the total number of opened connections and discovered direct versus relay paths.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →