Telemetry & tracing

Copy Markdown

ChannelClient emits :telemetry events for connections, messages and channel operations. Attach handlers to build metrics, dashboards or traces — no configuration is needed inside the library, and nothing is emitted to payload-privacy-sensitive detail: event metadata contains only topics, event names, refs and byte sizes.

Event catalog

EventMeasurementsMetadata
[:channel_client, :connection, :start]%{system_time}%{transport, url}
[:channel_client, :connection, :stop]%{duration}%{transport, url, result} + reason on error
[:channel_client, :connection, :disconnected]%{}%{reason}
[:channel_client, :message, :out]%{payload_bytes}%{topic, event, ref}
[:channel_client, :message, :in]%{duration, payload_bytes}%{topic, event, ref, join_ref}
[:channel_client, :channel_join, :start]%{system_time}%{topic}
[:channel_client, :channel_join, :stop]%{duration}%{topic, result} + reason on error
[:channel_client, :push, :start]%{system_time}%{event}
[:channel_client, :push, :stop]%{duration}%{event, result} + reason on error
[:channel_client, :push, :exception]%{duration}%{event, kind, reason, stacktrace}

Notes:

  • duration values are in native time units — convert with System.convert_time_unit/3.
  • The start / stop / exception triples follow the standard telemetry span conventions, so APM tools can render them as spans.
  • :push spans wrap synchronous ChannelClient.Channel.push/4 calls. A server-side error reply closes the span with result: :error; a timeout emits an :exception event and re-raises as usual.
  • Frames dropped by plugs or encoding failures never emit :message, :out.

Subscribing

# In your application start:
:ok = :telemetry.attach_many(
  "my-app-channel-client",
  ChannelClient.Telemetry.events(),
  &MyApp.Telemetry.handle_event/4,
  nil
)
defmodule MyApp.Telemetry do
  require Logger

  def handle_event([:channel_client, :connection, :stop], %{duration: duration}, meta, _) do
    Logger.info("channel_client connect #{meta.result} (#{us(duration)}s)")
  end

  def handle_event([:channel_client, :push, :stop], %{duration: duration}, meta, _) do
    Logger.debug("push #{meta.event} -> #{meta.result} in #{us(duration)}s")
  end

  def handle_event(_name, _measurements, _meta, _config), do: :ok

  defp us(native), do: System.convert_time_unit(native, :native, :microsecond)
end

Building traces

Because every operation follows the span triple shape, tracing backends can correlate events directly:

handle_event([:channel_client, :channel_join, :start], m, meta, config) do
  # open a span keyed by {self(), meta.topic}
end

handle_event([:channel_client, :channel_join, :stop], %{duration: d}, meta, config) do
  # close the span, record d as its latency, mark red/green by meta.result
end

handle_event([:channel_client, :push, :exception], %{duration: d}, meta, config) do
  # close the span as failed with meta.kind / meta.reason
end

OpenTelemetry users can convert these pairs into OTel spans in a handler, or use a converter library that speaks the same convention.

Correlating messages across services

To propagate trace context through channel payloads, write an outbound plug that stamps context into the payload and mirror it on your server:

outbound_plugs: [
  fn msg, _opts ->
    {:cont, %{msg | payload: Map.put(msg.payload || %{}, "_trace_id", get_trace_id())}}
  end
]

Keep such conventions explicit between client and server; ChannelClient never mutates payloads itself.

Phoenix LiveDashboard

LiveDashboard's metrics page can chart these events out of the box, e.g. queue a counter for [:channel_client, :message, :in] and a summary for [:channel_client, :push, :stop] durations.