Dialup は :telemetry ライブラリを使って計測ポイントを提供する。WebSocket 接続数、イベント処理時間、エラー率などを監視できる。

イベント一覧

WebSocket

イベント計測値メタデータ
[:dialup, :websocket, :connect]%{count: 1}%{session_id: "..."}
[:dialup, :websocket, :disconnect]%{count: 1}%{session_id: "..."}

イベント処理

イベント計測値メタデータ
[:dialup, :event, :start]%{system_time: integer}%{event: "increment", path: "/"}
[:dialup, :event, :stop]%{duration: integer}%{event: "increment", path: "/"}
[:dialup, :event, :exception]%{duration: integer}%{event: "increment", path: "/", kind: :error, reason: %RuntimeError{}, stacktrace: [...]}

durationSystem.monotonic_time() のネイティブ単位。ミリ秒に変換するには System.convert_time_unit(duration, :native, :millisecond) を使う。

すべての start に対して stopexception のどちらかが必ず発火する。

ナビゲーション

イベント計測値メタデータ
[:dialup, :navigate, :start]%{system_time: integer}%{path: "/users/123"}
[:dialup, :navigate, :stop]%{duration: integer}%{path: "/users/123"}
[:dialup, :navigate, :exception]%{duration: integer}%{path: "/users/123", kind: :error, reason: %RuntimeError{}, stacktrace: [...]}

ハンドラの登録

アプリケーション起動時に :telemetry.attach_many でハンドラを登録する。

重要: use Dialup を含むモジュールはホットリロード時に再コンパイルされるため、テレメトリハンドラは別モジュールに切り出す必要がある。同じモジュール内に defp で定義すると、関数参照が無効化されて :badfun エラーになる。

# lib/my_app.ex
defmodule MyApp do
  use Application

  use Dialup,
    app_dir: __DIR__ <> "/app",
    title: "My App"

  @impl Application
  def start(_type, _args) do
    MyApp.Telemetry.attach()

    children = [{Dialup, app: __MODULE__, port: 4000}]
    Supervisor.start_link(children, strategy: :one_for_one)
  end
end
# lib/telemetry.ex
defmodule MyApp.Telemetry do
  require Logger

  def attach do
    :telemetry.attach_many(
      "my-app-telemetry",
      [
        [:dialup, :websocket, :connect],
        [:dialup, :websocket, :disconnect],
        [:dialup, :event, :stop],
        [:dialup, :event, :exception],
        [:dialup, :navigate, :stop],
        [:dialup, :navigate, :exception]
      ],
      &__MODULE__.handle/4,
      nil
    )
  end

  def handle([:dialup, :websocket, :connect], _measurements, meta, _config) do
    Logger.info("[ws] connected: #{meta.session_id}")
  end

  def handle([:dialup, :websocket, :disconnect], _measurements, meta, _config) do
    Logger.info("[ws] disconnected: #{meta.session_id}")
  end

  def handle([:dialup, :event, :stop], %{duration: d}, meta, _config) do
    ms = System.convert_time_unit(d, :native, :millisecond)
    Logger.info("[event] #{meta.event} on #{meta.path} (#{ms}ms)")
  end

  def handle([:dialup, :event, :exception], %{duration: d}, meta, _config) do
    ms = System.convert_time_unit(d, :native, :millisecond)
    Logger.error("[event] #{meta.event} on #{meta.path} failed (#{ms}ms): #{Exception.message(meta.reason)}")
  end

  def handle([:dialup, :navigate, :stop], %{duration: d}, meta, _config) do
    ms = System.convert_time_unit(d, :native, :millisecond)
    Logger.info("[navigate] #{meta.path} (#{ms}ms)")
  end

  def handle([:dialup, :navigate, :exception], %{duration: d}, meta, _config) do
    ms = System.convert_time_unit(d, :native, :millisecond)
    Logger.error("[navigate] #{meta.path} failed (#{ms}ms): #{Exception.message(meta.reason)}")
  end

  def handle(_event, _measurements, _meta, _config), do: :ok
end

telemetry_metrics との連携

Telemetry.Metrics を使ったダッシュボード向けのメトリクス定義例:

defp metrics do
  [
    Telemetry.Metrics.counter("dialup.websocket.connect.count"),
    Telemetry.Metrics.counter("dialup.websocket.disconnect.count"),
    Telemetry.Metrics.distribution("dialup.event.stop.duration",
      unit: {:native, :millisecond},
      tags: [:event, :path]
    ),
    Telemetry.Metrics.counter("dialup.event.exception.duration",
      tags: [:event, :path]
    ),
    Telemetry.Metrics.distribution("dialup.navigate.stop.duration",
      unit: {:native, :millisecond},
      tags: [:path]
    )
  ]
end