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:

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.rs for tracing::warn!("Failure while handling connection") messages. Use RUST_LOG=iroh=trace to see the full connection lifecycle.
  • Relay lookup returning no candidates: Look for tracing::info!("home relay found") in iroh/src/protocol.rs to verify relay discovery. Filter with RUST_LOG=iroh=debug.
  • Hole-punching stalls: Examine tracing::debug! messages in iroh/src/socket/transports/relay/actor.rs around NAT traversal logic. Use RUST_LOG=iroh=debug,iroh-relay=trace.
  • DNS resolution errors: Inspect the request handling path in iroh-dns-server/src/main.rs with RUST_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=debug or initialize tracing_subscriber::fmt programmatically in your application entry point.
  • Target specific modules like iroh::socket, iroh::endpoint, or iroh-relay to filter noise and focus on relevant subsystems.
  • Use integration tests with --nocapture to observe real-world client-server interactions in iroh/tests/integration.rs.
  • Leverage n0_tracing_test when writing unit tests to assert against specific log events and error conditions.
  • Inspect span fields in iroh/src/protocol.rs to 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →