Amarula.Telemetry (amarula v0.1.0)

View Source

:telemetry events emitted by Amarula — the operational-observability surface.

This is orthogonal to the consumer event stream ({:whatsapp, type, data} delivered to parent_pid): those are application callbacks carrying real content/JIDs; these are metrics for operators (counts, durations, kinds).

Privacy

Telemetry payloads NEVER carry phone numbers, JIDs, message content, or key material — only counts, byte sizes, durations, booleans, kinds, and the connection's :profile. Every emit goes through emit/3 / span/4 here, which inject :profile, so this is the single file to audit for leaks.

Events

All events are prefixed [:amarula, ...]. Spans follow the :telemetry.span/3 convention (:start / :stop / :exception).

EventMeasurementsMetadata
[:amarula, :connection, :update]%{count: 1}%{profile, state}
[:amarula, :send, :start]%{monotonic_time, system_time}%{profile, kind, media?, media_kind}
[:amarula, :send, :stop]%{duration, bytes}%{profile, kind, media?, media_kind}
[:amarula, :send, :exception]%{duration}%{profile, kind, kind: :error/:exit/:throw, reason}
[:amarula, :send, :not_on_whatsapp]%{count: 1}%{profile}
[:amarula, :message, :received]%{count: 1, media_bytes}%{profile, from_me?, group?, offline?, media?, media_kind}
[:amarula, :decrypt, :exception]%{count: 1}%{profile, reason}
[:amarula, :reconnect, :scheduled]%{count: 1, delay_ms, attempt}%{profile}
[:amarula, :stream_error, :restart] / :received%{count: 1}%{profile, code}
[:amarula, :prekey, :upload]%{count}%{profile}
[:amarula, :retry, :received]%{count: 1}%{profile}
[:amarula, :retry, :sent]%{count: 1, attempt}%{profile}attempt = escalating per-peer retry count; a high/rising value flags an unrecoverable peer

media_bytes on :message, :received is the declared fileLength from the message (what the sender claims), not a downloaded size — Amarula doesn't download media eagerly. bytes on :send, :stop is the declared media size of the outgoing message (0 for text).

Deferred (planned, not yet emitted): an [:amarula, :iq, ...] round-trip latency span, and [:amarula, :handshake|:app_state, ...] spans. See docs/plans/TELEMETRY.plan.md.

Attaching handlers

:telemetry.attach_many(
  "my-app-amarula",
  Amarula.Telemetry.events(),
  &MyApp.handle_event/4,
  nil
)

Or with telemetry_metrics (a consumer dep — Amarula stays backend-agnostic):

Telemetry.Metrics.summary("amarula.send.stop.duration", tags: [:profile, :kind])
Telemetry.Metrics.sum("amarula.send.stop.bytes", tags: [:profile])
Telemetry.Metrics.sum("amarula.message.received.media_bytes", tags: [:profile])

Summary

Types

A connection profile (atom or string), injected into every payload.

Functions

Emit a single event. profile is injected into metadata. A :count of 1 is the default measurement when none is given.

Every event name Amarula emits — for :telemetry.attach_many/4.

Run fun as a span, emitting name ++ [:start] then name ++ [:stop] (or [:exception] if fun raises), with :profile injected into metadata.

Types

profile()

@type profile() :: atom() | String.t()

A connection profile (atom or string), injected into every payload.

Functions

emit(name, profile, measurements \\ %{count: 1}, metadata \\ %{})

@spec emit([atom()], profile(), map(), map()) :: :ok

Emit a single event. profile is injected into metadata. A :count of 1 is the default measurement when none is given.

events()

@spec events() :: [[atom()]]

Every event name Amarula emits — for :telemetry.attach_many/4.

span(name, profile, metadata, fun)

@spec span([atom()], profile(), map(), (-> {term(), map()})) :: term()

Run fun as a span, emitting name ++ [:start] then name ++ [:stop] (or [:exception] if fun raises), with :profile injected into metadata.

Unlike :telemetry.span/3, the fun returns {result, extra_measurements} so the :stop event can carry extra measurements (e.g. bytes) — those aggregate (sum/summary) in telemetry_metrics, where span metadata only tags. :duration (native time units) is always added. Returns fun's result.