How to Debug Issues in Iroh: A Complete Tracing and Diagnostics Guide
Enable verbose logging by setting RUST_LOG=iroh=debug (or trace) before running your binary, and initialize a tracing_subscriber in your code to capture detailed spans and events from the QUIC and relay networking stack.
Iroh is a Rust-based peer-to-peer networking stack maintained by n0-computer/iroh that provides QUIC hole-punching, relay servers, and DNS resolution. When you need to debug issues in iroh, the library exposes comprehensive diagnostics through the tracing crate, emitting structured events for every connection, packet, and state transition. This guide covers the specific techniques and source locations that reveal what is happening inside the networking stack.
Understanding Iroh's Tracing Architecture
The foundation of debugging in iroh relies on the tracing crate, which instruments the entire codebase with spans and events. Every critical operation—from endpoint creation to relay handling—emits diagnostic output that you can capture and analyze.
Key instrumentation points in the source code include:
- Connection lifecycle: The
iroh/src/endpoint/connection.rsfile usestracing::{event, warn}to report failures and state transitions during connection establishment. - Transport layer:
iroh/src/endpoint/quic.rslogs warnings withtracing::warnfor QUIC-specific errors. - Relay operations:
iroh-relay/src/server/http_server.rsemits detailed spans for HTTP-based relay traffic. - DNS resolution:
iroh-dns-server/src/main.rsinitializes its own subscriber withtracing_subscriber::fmt::init().
These components feed into a unified subscriber layer that formats output based on your configured log level.
Command-Line Debugging with RUST_LOG
The simplest method to debug issues in iroh involves setting the RUST_LOG environment variable before executing your binary or test suite.
Enable basic informational logging for all crates:
RUST_LOG=info cargo run --example echo
For granular debugging, use hierarchical filtering to target specific modules while keeping cargo output minimal:
RUST_LOG=iroh=debug,cargo=info cargo run --example listen
To see every trace event during test execution, including internal state changes:
RUST_LOG=trace cargo test -- --nocapture
The --nocapture flag ensures that tracing events and println! output appear in the terminal, revealing the exact order of network operations.
Programmatic Configuration for Embedded Applications
When embedding iroh as a library in a larger application, you must initialize the tracing subscriber programmatically. The iroh/examples/echo.rs file demonstrates this pattern with tracing_subscriber::fmt::init() at the entry point.
Implement custom initialization with environment-based filtering:
use tracing_subscriber::{fmt, EnvFilter};
fn init_tracing() {
fmt::Subscriber::builder()
.with_env_filter(EnvFilter::from_default_env())
.init();
}
After initialization, all spans from iroh/src/protocol.rs and other modules will stream to your configured output, including recorded fields like ALPN identifiers and connection IDs.
Running Integration Tests with Full Diagnostics
The integration test suite in iroh/tests/integration.rs exercises the complete stack including relays, DNS, and QUIC transport. Running these tests with debug logging provides real-world visibility into client-server interactions.
Execute the integration tests with verbose output:
RUST_LOG=iroh=debug cargo test -p iroh --test integration -- --nocapture
This command prints connection IDs, handshake details, and protocol negotiations as the test sets up client and server endpoints, making it invaluable for understanding complex interaction patterns.
Using n0_tracing_test for Test Assertions
Many library tests utilize the #[n0_tracing_test::traced_test] attribute macro defined in iroh/src/test_utils.rs. This macro automatically provisions a temporary subscriber that captures all events during test execution.
You can find usage examples in iroh/src/socket/transports/relay/actor.rs, where the macro enables assertions about log output during NAT traversal scenarios. This approach is particularly useful when writing unit tests that must verify specific warning or error conditions occur under expected failure modes.
Common Debugging Scenarios and Solutions
Target these specific modules when troubleshooting particular issues:
- Connection failures: Check
iroh/src/endpoint/connection.rsfortracing::warn!("Failure while handling connection")messages. UseRUST_LOG=iroh=traceto see the full connection lifecycle. - Relay lookup returning no candidates: Look for
tracing::info!("home relay found")iniroh/src/protocol.rsto verify relay discovery. Filter withRUST_LOG=iroh=debug. - Hole-punching stalls: Examine
tracing::debug!messages iniroh/src/socket/transports/relay/actor.rsaround NAT traversal logic. UseRUST_LOG=iroh=debug,iroh-relay=trace. - DNS resolution errors: Inspect the request handling path in
iroh-dns-server/src/main.rswithRUST_LOG=iroh-dns-server=info.
Advanced Tips for Interactive Debugging
Inspect span fields to verify protocol negotiation. When iroh/src/protocol.rs creates spans with tracing::Span::current().record("alpn", ...), the recorded fields appear in log output, revealing which application protocol is being used (lines 640–650).
Filter by module to reduce noise. Instead of enabling trace for all crates, narrow your focus:
RUST_LOG=iroh::socket=trace cargo run --example your-example
This targets only the socket layer in iroh/src/socket/, excluding verbose output from dependencies.
Capture full event streams in production debugging by combining tracing-subscriber with JSON formatting:
use tracing_subscriber::{fmt, EnvFilter};
fmt::Subscriber::builder()
.json()
.with_env_filter(EnvFilter::from_default_env())
.init();
Summary
- Enable tracing via
RUST_LOG=iroh=debugor initializetracing_subscriber::fmtprogrammatically in your application entry point. - Target specific modules like
iroh::socket,iroh::endpoint, oriroh-relayto filter noise and focus on relevant subsystems. - Use integration tests with
--nocaptureto observe real-world client-server interactions iniroh/tests/integration.rs. - Leverage
n0_tracing_testwhen writing unit tests to assert against specific log events and error conditions. - Inspect span fields in
iroh/src/protocol.rsto verify ALPN identifiers and connection metadata during protocol negotiation.
Frequently Asked Questions
How do I enable debug logging in a binary that uses the iroh library?
Set the RUST_LOG environment variable to iroh=debug or iroh=trace before running your binary. For embedded applications, call tracing_subscriber::fmt::init() at startup to capture all events from the iroh crate according to the source code in iroh/examples/echo.rs.
Why can't I see any log output when running cargo test?
By default, Rust's test harness captures stdout and stderr. Add the --nocapture flag to your test command (e.g., cargo test -- --nocapture) to ensure tracing events and print statements appear in your terminal output, as demonstrated in the integration tests.
What is the difference between using RUST_LOG and the n0_tracing_test macro?
RUST_LOG configures the global tracing subscriber for binaries and general debugging, while #[n0_tracing_test::traced_test] creates a temporary subscriber scoped to a specific test function, allowing you to write assertions about log output without affecting the global logger state.
Where should I look in the source code to understand why my connection is failing?
Check iroh/src/endpoint/connection.rs for tracing::warn calls that indicate connection lifecycle failures, and examine iroh/src/socket/transports/relay/actor.rs for NAT traversal and relay-specific diagnostics that explain hole-punching stalls.
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 →