Skip to main content

guides/telemetry.md

# Telemetry

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: [...]}` |

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

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

### ナビゲーション

| イベント | 計測値 | メタデータ |
|---------|--------|-----------|
| `[: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` エラーになる。

```elixir
# 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
```

```elixir
# 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` を使ったダッシュボード向けのメトリクス定義例:

```elixir
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
```