Table of Contents

Observability

Metrics, watchers, path and connection introspection, net reports, qlog, and the TLS key log — and the one thing go-iroh does not have, a debug log.

go-iroh exposes its runtime state through typed APIs rather than a log stream: counters you snapshot, watchers you subscribe to, and per-connection introspection you poll or stream. This page walks the whole surface, then is honest about the gap.

Endpoint counters

Endpoint.Metrics() returns a point-in-time snapshot: endpoint-level connect/accept counters plus two nested groups, SocketMetrics (per-transport datagram counters, path counts, holepunch attempts, relay home changes) and NetReportMetrics. The same two groups exist in Rust iroh’s EndpointMetrics, so dashboards translate directly.

The snapshot integrates with the standard ecosystems three ways:

http.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
	if err := ep.Metrics().WriteOpenMetrics(w); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
})

Watchers

Two endpoint properties are observable as they change, through watch.Observer (the Go analog of Rust’s n0-watcher): Current() for the value now, Updated(ctx) to block for the next change, Stream(ctx) for an iterator.

// React to this endpoint's address changing (new direct address,
// relay change, path loss).
for addr := range ep.WatchAddr().Stream(ctx) {
	fmt.Println("addresses now:", addr)
}

Endpoint.HomeRelayStatus() is the same shape for the home relay connection: the watched value is nil until a home relay is selected and updates whenever the relay or its connection state changes.

Per-connection introspection

paths, err := conn.WatchPaths(ctx)
if err != nil {
	return err
}
for snapshot := range paths {
	for _, p := range snapshot {
		fmt.Println(p.ID, p.Validated, p.Addr, p.RTT)
	}
}

Net reports

With iroh.WithNetReport(), the endpoint refreshes a network report in the background after Bind. Endpoint.NetReport() returns the most recent report — UDP reachability per family, whether the observed public address varies by destination (the NAT-mapping question), and related findings. The second result is false until the first report completes. See Troubleshooting for reading one.

qlog

The QUIC transport writes qlog traces when the QLOGDIR environment variable names a directory:

mkdir -p qlogs
QLOGDIR=qlogs go run .

Files are named <odcid>_<perspective>.sqlog, one per connection, and capture packet-level transport behavior — including the multipath and NAT traversal frames. This is the deepest view available and the one to reach for when a connection misbehaves in a way counters cannot explain.

TLS key log

iroh.WithKeyLogWriter(w) writes TLS traffic secrets for direct peer QUIC handshakes in NSS SSLKEYLOGFILE format, which lets Wireshark decrypt a packet capture. Its doc comment carries the warning that matters: it is for debugging only; writing these secrets compromises connection confidentiality.

Watching connections happen

iroh.WithHooks observes (and can reject) outbound dials before they start and handshakes as they complete — see Observing and rejecting connections. For a worked diagnostic program that prints relay status, net report, latency, and path information, see example 37-doctor; the gistat tool prints connection statistics and path information for a live target.

What go-iroh does not have: a debug log

The transport internals — the magic socket, the relay client, path management and holepunching — do not log. The only logger in the public API is RouterConfig.Logger, which covers protocol dispatch, not the transport. Rust iroh instruments all of this with tracing spans filterable by RUST_LOG; go-iroh currently has no equivalent, and a live connectivity problem is debugged from the APIs above (metrics deltas, WatchPaths, hooks) plus qlog, not from a narrative log. Endpoint-level structured tracing is an open design item for go-iroh.

Next steps

Last updated: 2026-08-20