Overview
whatsapp-rust ships an optionalmetrics Cargo feature that emits wa_* counters, histograms, and gauges through the metrics facade. Spans from the tracing feature tell you the story of a single case; metrics give you the rates and latency percentiles you need for dashboards and alerts.
The library only emits through the facade. It never installs a recorder and does not depend on Prometheus or OTLP — your application chooses the recorder and exposes the scrape endpoint.
The
metrics feature is off by default. With it disabled there is no metrics dependency, every emit is an inlined no-op, and the duration Timer is a zero-sized type that reads no clock. There is zero runtime cost.When to use it
Turn onmetrics when you want to:
- Build Grafana, Datadog, or Honeycomb dashboards for connect success rate, IQ latency percentiles, retry receipts, send throughput, or app-state sync health.
- Page on connection loss, identity-change spikes, or rising decrypt failure rates.
- Compare aggregate behavior across deployments without enabling per-trace export.
Enabling the feature
Addwhatsapp-rust with the metrics feature, plus the recorder you want to expose. The example below uses Prometheus:
Cargo.toml
metrics-exporter-opentelemetry (or any other recorder that implements the metrics::Recorder trait).
Wiring a recorder
Install the recorder once at startup, then build yourClient as usual. A runnable version of this wiring ships as examples/metrics.rs in the source repo:
src/main.rs
Metric catalogue
All metrics are prefixed withwa_ and emitted at the same boundaries as the matching wa.* tracing spans. Counters and gauges carry the categorical breakdown; the matching duration histograms are unlabeled.
Counters
wa_unkeyable_device_total is the rate to watch after a session-repair change, or to page on when a chat sits on “Waiting for this message”. It is an attempt counter, not a per-delivered-device one — a retry that fails the same way counts again. 406 keeps its own label since it’s the only code that changes client behavior. refused_batch is kept apart from a named rejected_* label because a batch refusal names no device. stats()’s devices_unkeyed_* fields carry the same totals without the per-code split.Both are process-wide: they say a device went unkeyed somewhere, never which message lost it. As of #1362, the per-message question is answered by SendResult::recipient_fanout instead — a DM’s result carries how many recipient devices it addressed, how many actually encrypted, and whether the recipient’s primary device was among the ones that didn’t.Histograms (seconds)
Gauges
Recording your own durations
The sameTimer the library uses internally is part of the public API. Hold the returned guard for the scope of the operation; it records elapsed seconds on drop:
PII and cardinality
Labels are strictly low-cardinality categorical values (outcome, kind, result, reason). The library never uses a JID, phone number, or message ID as a label — that would explode the metrics backend and leak PII. Histograms are unlabeled; the matching _total counter carries the categorical breakdown.
Overhead
Durations use the pluggable
wacore::time::Instant, so WASM and deterministic builds are unaffected.
Related
- Observability with tracing — per-case spans that complement the aggregate metrics here.
- Installation — feature flags — full feature matrix including
metrics,tracing, andtracing-pii.