エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Phoenixの遊び場でちょっとしたモニタリングツールを作る

遅ればせながら。 Phoenix Playground の存在に気づきました。

hex.pm

以前から。 状態の変化を LiveView を使ってブラウザ上で閲覧できるようにすることを考えているのですが、ごく簡単な情報を表示するために Phoenix app を構築するのが割に合わず、結局コンソール表示で妥協するのが常となっています。

Phoenix Playground を使えば、1 ファイルで LiveView のページが作れるので、シェルスクリプトを書くくらいの気持ちでそういったツールを作れるのではないか、と思った次第。

と、いうわけで。

試しにディレクトリの状態を監視するツールを雑な感じに書いてみました。

ディレクトリの監視には file_system を使いました。

hex.pm

また、時刻を日本時間で表示するために tzdata を使っています。

hex.pm

そして書いたコードがこちら。

Mix.install(
  [
    {:phoenix_playground, "~> 0.1.0"},
    {:file_system, "~> 1.0"},
    {:tzdata, "~> 1.1"}
  ],
  config: [
    elixir: [time_zone_database: Tzdata.TimeZoneDatabase]
  ]
)

defmodule DirWatcherLive do
  use Phoenix.LiveView

  require Logger

  def mount(_params, _session, socket) do
    if connected?(socket) do
      FileSystem.subscribe(:dir_watcher)
    end

    {:ok, assign(socket, items: [])}
  end

  def render(assigns) do
    ~H"""
    <table>
      <thead>
        <th style="width: 180px;">timestamp</th>
        <th style="width: 240px;">filename</th>
        <th>events</th>
      </thead>
      <tbody>
        <tr :for={item <- @items}>
          <td style="width: 180px;"><tt><%= item.time %></tt></td>
          <td style="width: 240px;"><tt><%= item.basename %></tt></td>
          <td><%= item.events %></td>
        </tr>
      </tbody>
    </table>

    <style type="text/css">
      body { padding: 1em; }
    </style>
    """
  end

  def handle_info({:file_event, _worker_pid, {file_path, events}}, socket) do
    Logger.info("file_path: #{file_path}, events: #{inspect(events)}")
    
    {:noreply, assign(socket, :items, [to_item(file_path, events) | socket.assigns.items])}
  end

  defp to_item(file_path, events) do
    %{
      basename: Path.basename(file_path),
      time: current_time(),
      events: events_to_string(events)
    }
  end

  defp current_time do
    DateTime.now!("Asia/Tokyo") |> Calendar.strftime("%Y-%m-%d %H:%M:%S")
  end

  defp events_to_string(events) do
    for event <- events, event in [:created, :modified, :removed, :renamed] do
      event
    end
    |> Enum.join(", ")
  end
end

{:ok, _pid} = FileSystem.start_link(dirs: ["."], name: :dir_watcher)

PhoenixPlayground.start(live: DirWatcherLive)

Phoenix Playground のリポジトリにある demo_live.exs に FileSystem プロセスを加え、LiveView プロセス内でFileSystem.subscribe/1 を使って購読を登録し、handle_info/2 でイベントをハンドリングする、という愚直な実装です。

スクリプトを実行します。

$ elixir dir_watcher.exs

デフォルトの指定では、自動的に作成したページがブラウザで開きます。

スクリプトを実行したディレクトリでファイルの操作をしてみます。

$ echo Hi > hi.txt
$ echo Hello >> hi.txt
$ mv hi.txt hello.txt
$ rm hello.txt 

ブラウザに表示されている LiveView の内容が更新されることが確認できると思います。