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, :stop error_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).

FieldTypeNotes
Measurements (:stop)durationNative time units
Metadataserial_numberThe 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.

FieldKeys
Measurements (:stop)duration, success_count, error_count
Metadatatoken_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 familyWrapsIdentifier metadata
…, :google, :create_or_update_classGoogle.Api.create_or_update_class/2 (PUT, fallback to POST)class_id (full <issuer_id>.<suffix>)
…, :google, :create_objectGoogle.Api.create_object/3 (POST, fallback to PUT on 409)serial_number
…, :google, :update_objectGoogle.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.

FieldKeys
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.

FieldKeys
Measurements (:stop)duration (zero on cache hit)
Metadatacached (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.

FieldKeys
Metadataserial_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.

FieldKeys
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
end

Call 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: