Skip to main content

Overview

whatsapp-rust ships an optional tracing 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

Add whatsapp-rust with the tracing feature, plus tracing-subscriber for the consumer side:
Cargo.toml
The existing 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.
Do not enable the log feature on the tracing crate together with the log → tracing bridge. That recurses. whatsapp-rust already pins tracing with default-features = false so the hazard cannot happen inside the library, but be careful when adding tracing to your own dependencies.

Wiring a subscriber

A minimal tracing-subscriber setup driven by RUST_LOG:
src/main.rs
Run it with the feature on:
A runnable version of this wiring (with OpenTelemetry stubs) ships as examples/observability.rs in the source repo.

OpenTelemetry / OTLP

To export spans to an OTLP collector (Jaeger, Tempo, Honeycomb, etc.), add opentelemetry, opentelemetry-otlp, and tracing-opentelemetry, then append a layer to the subscriber:
src/main.rs
Every wa.* span is then exported as an OTLP span with its fields intact.

Span taxonomy

Spans are grouped under a stable wa.<area>.<op> naming scheme so you can filter or build dashboards per area. The current areas are:

Levels

Most spans are emitted at debug or trace. The connection-lifecycle spans (wa.conn.connect, wa.conn.disconnect, wa.conn.reconnect, 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 — with two exceptions: wa.conn.connect surfaces failures at WARN instead (its caller already classifies the real failures as error!, so the default ERROR was double-reporting transient handshake retries), and wa.conn.read_loop returns Ok (not Err) for a routine server-initiated stream recycle, so its ERROR capture fires only for genuine failures — never for WhatsApp’s normal periodic reconnects.
There is no span wrapping the client’s outer auto-reconnect loop (Client::run). A span there would live for the entire client lifetime and only report at shutdown, distorting duration/throughput dashboards the same way a whole-connection keepalive span would — so it is deliberately left uninstrumented. Connection-lifecycle visibility comes from the per-attempt spans above; account identity comes from the per-operation spans below.
A downstream binary can statically strip lower levels at compile time with tracing’s release_max_level_info / release_max_level_warn features.

Account identity in spans

The wa.conn.connect, wa.conn.read_loop, wa.iq, and wa.send.message spans carry lid and pn fields for your own account, so traces are filterable and groupable per account in multi-account deployments. pn is redacted via Jid::observe() like any other phone-number field (see PII handling below); lid is rendered in full since it is pseudonymous. To tag your own spans or error context with the same account identity, call Client::identity_tags() (available whenever the tracing feature is enabled on the whatsapp-rust dependency — no local feature of your own is required):
tracing’s Value impl for Option<T> records the field only when the value is Some, so a missing lid or pn is left absent on the event instead of printing as None or an empty string — matching how wa.conn.connect / wa.iq / wa.send.message behave internally, and keeping both tags on a single event instead of splitting them across two. identity_tags() returns an IdentityTags { lid: Option<String>, pn: Option<String> } snapshot — a named struct rather than a tuple, so LID/PN cannot be silently transposed at the call site. pn is already the redacted pn#<token> form produced by Jid::observe(), not the raw phone number, so logging it directly — as in the example above — does not leak PII by default; lid is rendered in full since it is pseudonymous. This guarantee only holds while the tracing-pii feature stays disabled (the default) — with tracing-pii enabled, identity_tags() inherits Jid::observe()’s raw-number behavior like every other redacted value on this page, so pn becomes the actual phone number (see tracing-pii below). Both fields read from the same device snapshot used internally, so they stay consistent with what the library’s own spans record.

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 with pn#<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 Signal ProtocolAddress names 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.
Redaction only covers identifiers the library emits. Anything your own application code logs — raw JIDs, phone numbers, message bodies — reaches the exporter unredacted under your own targets. Scrub them with Jid::observe() (and observe_protocol_address() for Signal addresses) before logging.

tracing-pii (local debugging only)

For local debugging where you need to see raw phone numbers, enable the tracing-pii feature:
This makes Jid::observe() and observe_protocol_address() render raw numbers instead of the pn#<token> placeholder. Never enable this in production.

Overhead

Because spans are mostly debug/trace/info, a release build with release_max_level_info strips the rest without code changes.