Overview
whatsapp-rust ships an optionaltracing Cargo feature that instruments the library end-to-end: connect/disconnect, receive and decrypt, send, IQ, app state, pairing, media, receipts, retries, notifications, and session/crypto flows. With the feature on you can map a production error to who (which account), where (which span), how (the call path), and why (the failure attached to the span).
The library only emits tracing spans and events. It never installs a subscriber and does not depend on OpenTelemetry — your application owns the subscriber, the filtering, and any OTLP/Jaeger exporter.
The
tracing feature is off by default. With it disabled there is no tracing dependency and the instrumentation attributes vanish at compile time, so there is zero runtime cost.Enabling the feature
Addwhatsapp-rust with the tracing feature, plus tracing-subscriber for the consumer side:
Cargo.toml
log::{info,warn,error}! calls inside the library continue to work. tracing-subscriber’s default tracing-log feature bridges them into the subscriber, so they appear as events attached to the active wa.* span — even before you adopt any new span yourself.
Wiring a subscriber
A minimaltracing-subscriber setup driven by RUST_LOG:
src/main.rs
examples/observability.rs in the source repo.
OpenTelemetry / OTLP
To export spans to an OTLP collector (Jaeger, Tempo, Honeycomb, etc.), addopentelemetry, opentelemetry-otlp, and tracing-opentelemetry, then append a layer to the subscriber:
src/main.rs
wa.* span is then exported as an OTLP span with its fields intact.
Span taxonomy
Spans are grouped under a stablewa.<area>.<op> naming scheme so you can filter or build dashboards per area. The current areas are:
| Area | Covers |
|---|---|
wa.conn.* | Connect, disconnect, reconnect, handshake, read loop, keepalive, frame decrypt, stream errors |
wa.recv.* | Incoming message parsing and decrypt path |
wa.send.* | Outgoing send path (DM, group, peer, encryption) |
wa.iq | IQ request/response round-trips |
wa.appstate.* | App state sync, patch build/send, key requests |
wa.pair.* | QR code and pair code authentication |
wa.media.* | Upload, download, history sync, sticker packs, media conn refresh |
wa.receipt.* | Receipt processing (delivered, read, played) |
wa.retry.* | Retry receipt handling |
wa.pdo.* | Peer Data Operations (message recovery via primary device) |
wa.notif.* | Notification dispatch (group, devices, chatstate, identity change, privacy token) |
wa.session.* | Signal session establishment and crypto |
wa.usync.* | usync queries |
wa.bot.* | Bot builder, run loop, and MessageContext helpers (send_message, react, edit_message, revoke_message) |
Levels
Most spans are emitted atdebug or trace. The connection-lifecycle spans (wa.conn.connect, wa.conn.disconnect, wa.conn.reconnect, wa.conn.run, wa.conn.logout) are at info so connection state is visible at the default level. Failures surface at ERROR via err(Debug) on the instrumented function, and the existing warn!/error! log calls surface through the bridge.
The wa.conn.run session-root span records your account’s own LID, so traces are attributable per account in multi-account deployments.
A downstream binary can statically strip lower levels at compile time with tracing’s release_max_level_info / release_max_level_warn features.
Filtering examples
RUST_LOG accepts span/event targets the same way it accepts log targets:
PII handling
WhatsApp identifiers contain phone numbers, so the library redacts them before they reach a span field or a log line.Jid::observe()renders LID, group, broadcast, newsletter, and bot JIDs in full — they are pseudonymous or non-personal, so the same peer or chat still correlates across spans. Phone-number user JIDs are replaced withpn#<token>, where the token is a keyed SipHash (the key is a process-lifetime random seed kept only in memory). An unkeyed hash of an E.164 number is reversible by precomputation; the keyed scheme is not.- Legacy group IDs of the form
<creator-phone>-<timestamp>keep the timestamp and redact only the numeric prefix. observe_protocol_address()applies the same scheme to SignalProtocolAddressnames embedded in logs.- The library’s own
log!calls already pipe JIDs and addresses through these helpers, so the bridged log lines carry the same redaction as the span fields.
tracing-pii (local debugging only)
For local debugging where you need to see raw phone numbers, enable the tracing-pii feature:
Jid::observe() and observe_protocol_address() render raw numbers instead of the pn#<token> placeholder. Never enable this in production.
Overhead
| Configuration | Cost |
|---|---|
| Feature off (default) | No dependency, attributes vanish at compile time, zero runtime cost |
| Feature on, no subscriber installed | Near-zero (tracing’s callsite caching) |
| Feature on, subscriber installed | Pay per emitted span at your chosen level |
debug/trace/info, a release build with release_max_level_info strips the rest without code changes.
Related
- Metrics with the metrics facade — aggregate
wa_*counters, histograms, and gauges that complement these per-case spans. - Configuring log targets —
log-based filtering that also works with the tracing bridge. - Installation — feature flags — full feature matrix including
tracing,tracing-pii, andmetrics.