---
title: Observability
description: 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.
icon: chart-line
---

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:

- `String()` implements `expvar.Var`, so `expvar.Publish("iroh", ep.Metrics())`
  works — though note it captures one snapshot, not a live view.
- `Snapshot()` returns named counters for a [`metrics.Registry`](api-map).
- `WriteOpenMetrics(w)` writes OpenMetrics text under the `endpoint` prefix,
  which is what a Prometheus scrape wants:

```go
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.

```go
// 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

- `Conn.Paths()` — snapshot of the connection's QUIC multipath paths:
  `PathInfo` carries the path ID, whether the path is validated, its
  transport address, and its smoothed RTT when observed.
- `Conn.WatchPaths(ctx)` — the same snapshots as a stream. The first value is
  the current state; later values arrive when the endpoint observes a path
  change for the peer. This is how you watch a connection migrate from relay
  to a direct path.
- `Conn.Stats()` — transport totals for the connection: `MinRTT`,
  `LatestRTT`, `SmoothedRTT`, `MeanDeviation`, plus bytes and packets sent
  and received.
- `Endpoint.RemoteInfo(id)` — everything the endpoint currently knows about
  how to reach a remote: its transport addresses and their state.

```go
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](troubleshooting) for reading one.

## qlog

The QUIC transport writes [qlog](https://datatracker.ietf.org/doc/draft-ietf-quic-qlog-main-schema/)
traces when the `QLOGDIR` environment variable names a directory:

```sh
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](endpoint-hooks). For a worked
diagnostic program that prints relay status, net report, latency, and path
information, see example
[`37-doctor`](https://github.com/tmc/go-iroh-examples/tree/main/cmd/37-doctor);
the [`gistat`](tools) 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

- [Troubleshooting](troubleshooting) — applying these tools to a connection
  that will not establish.
- [Observing and rejecting connections](endpoint-hooks) — the hooks API.
- [Testing and interop](testing-and-interop) — evidence gates beyond a single
  process.
