wallet_passes instruments every significant operation — building Apple
.pkpass bundles, calling the Google Wallet REST API, sending APNs pushes,
and dispatching wallet-event callbacks — using the standard Erlang
:telemetry library. This guide is the
complete event reference plus a short primer on attaching handlers.
Overview
Events live under one of three prefixes — [:wallet_passes, :apple, …],
[:wallet_passes, :google, …], and [:wallet_passes, :event_handler, …] —
and follow the standard :start | :stop | :exception pattern. They're
emitted unconditionally; if no handler is attached they're no-ops.
:telemetry is a hard dependency (added in 0.5.0), so you don't need to
declare it separately. Typical use cases:
- Metrics — wire to PromEx, StatsD, Telemetry.Metrics, Datadog.
- Structured logging — trace a serial number through build → push → callback.
- Tracing — forward span events to OpenTelemetry.
- Alerting — page when
:apple, :push, :stoperror_count> 0.
Concepts
Skip to the event reference if you've used
:telemetry before.
The start / stop / exception pattern
:telemetry standardises a three-event pattern around any operation:
[…, :start]— measurements%{system_time: integer()}.[…, :stop]— measurements%{duration: native_time}.[…, :exception]— measurements%{duration: native_time}. Metadata gains:kind,:reason, and:stacktrace.
Most operations in this library use :telemetry.span/3, which emits all
three automatically. A couple use manual :telemetry.execute/3 and don't
emit :exception — those are flagged below.
duration is in native time units. Convert via
System.convert_time_unit(duration, :native, :microsecond).
Measurements vs metadata
Measurements are numeric (durations, counts, bytes) and feed histograms and counters. Metadata is contextual labels (serial numbers, IDs, status atoms) and feeds metric dimensions / tags. Avoid using high-cardinality metadata like raw serial numbers as Prometheus tags.
Attaching handlers
Handlers are functions called synchronously when an event fires. Attach
them once at boot (typically in your Application.start/2):
:telemetry.attach_many(
"wallet-passes-handler",
[
[:wallet_passes, :apple, :build_pass, :stop],
[:wallet_passes, :google, :save_url, :stop],
],
&MyApp.WalletTelemetry.handle_event/4,
_config = nil
)The handler signature is fn event, measurements, metadata, config -> :ok end.
Handlers run in the calling process — keep them fast and side-effect-free,
or hand off to a Task.
Event reference
In every table below, the :start event measurements are always
%{system_time: integer()} and the :stop/:exception events always
include duration (native time units). Only additional measurements are
documented per event.
[:wallet_passes, :apple, :build_pass, :start | :stop | :exception]
Emitted around WalletPasses.Apple.Builder.build_pkpass/4 — the full
.pkpass build pipeline (pass.json assembly, asset collection, manifest
hashing, PKCS#7 signing, ZIP packaging).
| Field | Type | Notes |
|---|---|---|
Measurements (:stop) | duration | Native time units |
| Metadata | serial_number | The PassData.serial_number |
Uses :telemetry.span/3, so :exception fires on raised errors with
:kind, :reason, and :stacktrace metadata.
[:wallet_passes, :apple, :push, :start | :stop]
Emitted around WalletPasses.Apple.Push.notify_devices/1 — sending silent
APNs background pushes to one or more device push tokens.
| Field | Keys |
|---|---|
Measurements (:stop) | duration, success_count, error_count |
| Metadata | token_count (number of tokens dispatched) |
Uses manual :telemetry.execute/3. No :exception event — internal
errors are reported via the success_count / error_count measurements,
not raised. An empty push-token list still emits both events with all
counts at zero.
[:wallet_passes, :google, :create_or_update_class | :create_object | :update_object, …]
Three separate event families, all emitted via :telemetry.span/3 with
the standard :start | :stop | :exception triple, all of which add
status: :ok | :error to the :stop metadata.
| Event family | Wraps | Identifier metadata |
|---|---|---|
…, :google, :create_or_update_class | Google.Api.create_or_update_class/2 (PUT, fallback to POST) | class_id (full <issuer_id>.<suffix>) |
…, :google, :create_object | Google.Api.create_object/3 (POST, fallback to PUT on 409) | serial_number |
…, :google, :update_object | Google.Api.update_object/4 (full-object PATCH) | object_id |
The identifier appears in both :start and :stop metadata; status is
added only to :stop. :create_or_update_class is also invoked by
ensure_class/2 on the first observation of a class within a VM lifetime.
[:wallet_passes, :google, :update_object_state, :start | :stop | :exception]
Emitted around WalletPasses.Google.Api.update_object_state/3 — the
lightweight state-only PATCH used by the pass lifecycle transition
functions (void_pass/1, expire_pass/1, complete_pass/1,
reactivate_pass/1). Added in 0.7.0.
| Field | Keys |
|---|---|
Metadata (:start) | object_id, pass_type, state |
Metadata (:stop) | object_id, pass_type, state, status |
state is one of "ACTIVE" | "INACTIVE" | "EXPIRED" | "COMPLETED".
pass_type is the atom (e.g., :event_ticket). status is :ok when the
HTTP response is 2xx, :error otherwise.
[:wallet_passes, :google, :token_exchange, :start | :stop | :exception]
Emitted around WalletPasses.Google.Api.get_access_token/0. Fires for
every call, including cache hits.
| Field | Keys |
|---|---|
Measurements (:stop) | duration (zero on cache hit) |
| Metadata | cached (true on ETS hit, false on miss) |
Cache hits emit start and stop synchronously with duration: 0. Cache
misses use :telemetry.span/3 around the OAuth2 token exchange call and
emit :exception on raised errors.
[:wallet_passes, :google, :save_url, :start | :stop | :exception]
Emitted around WalletPasses.Google.SaveUrl.url/2 — building the JWT and
the https://pay.google.com/gp/v/save/… URL.
| Field | Keys |
|---|---|
| Metadata | serial_number (extracted from pass_object["id"] after the dot) |
If pass_object["id"] doesn't match the <issuer>.<serial> shape, the
metadata field contains the raw id; if the input isn't a map at all,
it's nil.
[:wallet_passes, :event_handler, :dispatch, :start | :stop]
Emitted around each invocation of a configured WalletPasses.EventHandler
callback (on_pass_added, on_pass_removed, on_pass_fetched). The
handler itself runs under the WalletPasses.EventHandler.TaskSupervisor,
so these events are emitted from a supervised task, not the Plug request
process.
| Field | Keys |
|---|---|
Measurements (:stop) | duration |
Metadata (:start) | handler, callback |
Metadata (:stop) | handler, callback, status (:ok | :error), and on error: kind, reason |
Uses manual :telemetry.execute/3 with an internal try/rescue/catch.
No :exception event — even on handler crashes, a :stop event fires
with status: :error and the error details captured in metadata. The
crash is also logged via Logger.error. This means you can attach a
single :stop handler and reliably observe both success and failure.
Integration patterns
Minimal handler — log every event
defmodule MyApp.WalletTelemetry do
require Logger
@events [
[:wallet_passes, :apple, :build_pass, :stop],
[:wallet_passes, :apple, :push, :stop],
[:wallet_passes, :google, :create_object, :stop],
[:wallet_passes, :google, :update_object, :stop],
[:wallet_passes, :google, :update_object_state, :stop],
[:wallet_passes, :google, :save_url, :stop],
[:wallet_passes, :event_handler, :dispatch, :stop],
]
def attach do
:telemetry.attach_many(
"wallet-passes-log",
@events,
&__MODULE__.handle/4,
nil
)
end
def handle(event, measurements, metadata, _config) do
duration_us =
System.convert_time_unit(measurements.duration, :native, :microsecond)
Logger.info("wallet_passes event",
event: Enum.join(event, "."),
duration_us: duration_us,
metadata: metadata
)
end
endCall MyApp.WalletTelemetry.attach/0 from your Application.start/2 after
the supervision tree starts.
Metrics via Telemetry.Metrics
Define metrics with Telemetry.Metrics
and feed them to PromEx, telemetry_metrics_prometheus,
telemetry_metrics_statsd, or any other reporter:
import Telemetry.Metrics
[
summary("wallet_passes.apple.build_pass.stop.duration",
unit: {:native, :millisecond}
),
summary("wallet_passes.google.update_object_state.stop.duration",
unit: {:native, :millisecond},
tags: [:pass_type, :state, :status]
),
counter("wallet_passes.google.create_object.stop", tags: [:status]),
counter("wallet_passes.event_handler.dispatch.stop",
tags: [:callback, :status]
),
sum("wallet_passes.apple.push.stop.success_count"),
sum("wallet_passes.apple.push.stop.error_count"),
]Avoid using serial_number or object_id as a Prometheus tag — the
cardinality is unbounded. Keep those fields for logging only.
PromEx and OpenTelemetry
There is no built-in PromEx plugin; define a custom plugin whose
metrics/1 callback returns the definitions above.
For tracing, opentelemetry_telemetry
turns any :telemetry.span/3 event triple into an OTel span. Bridge each
prefix that uses span (every event in this guide except :apple, :push
and :event_handler, :dispatch, which use manual execute).
Structured logging
If you use a JSON Logger backend, the handler above produces queryable
logs. Tagging the request process with Logger.metadata/1 before a build
lets the serial number propagate to every log line emitted during it:
Logger.metadata(serial_number: pass_data.serial_number)
WalletPasses.build_apple_pass(pass_data, visual)API Reference
wallet_passes does not expose any library-specific telemetry API. All
attachment and configuration happens via the standard :telemetry module:
The events themselves are the contract; the event names and the keys listed in each table above are stable across minor versions.
See also:
- Getting Started
- Apple Wallet — context for
:apple, :build_passand:apple, :push - Google Wallet — context for
:google, :create_object,:google, :update_object,:google, :save_url, and:google, :create_or_update_class - Event Handling & Wallet Presence — context for
:event_handler, :dispatch - Pass Lifecycle & Updates — context for
:google, :update_object_state