Phoenix でログファイルをリアルタイムに tail してみる

Phoenix を使ってブラウザ上でログファイルをリアルタイムに tail するアプリケーションを作ってみます。

そう、つまりこれは、Phoenix Tail。

＿人人人人人人人人人人＿
＞　フェニックスの尾[^1]　＜
￣Y^Y^Y^Y^Y^Y^Y^Y^Y￣

[^1]: 残念ながら正確には Phoenix Down と訳すらしい。

# 完成イメージ

こんな感じです。

<img width="600" alt="capture.png" src="https://qiita-image-store.s3.amazonaws.com/0/77513/56003d85-8692-008f-529c-95e37423875d.png">


# Phoenix アプリケーションをセットアップする

早速アプリケーションを作っていきましょう。
PhoenixTail という名前で作ります。

```bash
$ mix phoenix.new phoenix_tail
```

WebSocket の設定をしておきます（詳細は[コチラ](http://qiita.com/mserizawa/items/2c67031e794964f3a740)）

```ex:web/channels/user_socket.ex
defmodule PhoenixTail.UserSocket do
  use Phoenix.Socket

  ## Channels
  # 以下の行を追加
  channel "tails:*", PhoenixTail.TailChannel

  ## Transports
  transport :websocket, Phoenix.Transports.WebSocket
  ...
end
```

TailChannel を作成します。今回はクライアントからの Push は想定していないので、join だけ実装します。

```ex:web/channels/tail_channel.ex
defmodule PhoenixTail.TailChannel do
  use Phoenix.Channel

  def join("tails:sample", auth_msg, socket) do
    {:ok, socket}
  end
  def join("tails:" <> _private_room_id, _auth_msg, socket) do
    {:error, %{reason: "unauthorized"}}
  end
end
```

# ログファイルを監視するプロセスを作る

次に、tail の対象とするログファイルを監視する部分を実装します。
処理の流れとしては、

* 定期的に対象のファイルをチェック
* 変更が入っていれば、前回取得行以降の行を取得
* 取得した内容を WebSocket に broadcast

という感じで、サーバへのリクエストなどとは一切関係ないものとなります。
つまり、独立したプロセスを作る必要があるのですが、これは Elixir がすこぶる得意とする分野で、GenServer という概念を使うとシンプルに実装できます。GenServer については、@naoya@github さんの[コチラの記事](http://qiita.com/naoya@github/items/ae17a8166e52fc463012#%E3%83%93%E3%83%98%E3%82%A4%E3%83%93%E3%82%A2%E3%81%AB%E4%B9%97%E3%81%A3%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%8A%E3%81%8B%E3%81%92%E3%81%A7--supervisor)が大変参考になります。

ソースコードの全容はこんな感じです。

```ex:lib/phoenix_tail/tail.ex
defmodule PhoenixTail.Tail do
  use GenServer

  # 親プロセスとのリンク処理
  def start_link(file) do
    # 引数で受け取ったターゲットファイルのパスを渡します
    GenServer.start_link(__MODULE__, {file})
  end

  # 初期化処理
  def init({file}) do
    # 受け取ったファイルパスをストリームにして、cast を呼びます
    stream = File.stream!(file)
    GenServer.cast(self, :check)
    {:ok, {stream, nil, 0}}
  end

  def handle_cast(:check, {stream, last_modified, position}) do
    # 変更をチェックして broadcast します
    {last_modified, position} = cast_new_lines(stream, last_modified, position)
    # 1秒スリープ後、処理を再帰します
    :timer.sleep(1000)
    GenServer.cast(self, :check)
    {:noreply, {stream, last_modified, position}}
  end

  defp cast_new_lines(stream, last_modified, position) do
    cond do
      # ファイルが存在しない場合はスルー
      !File.exists?(stream.path) ->
        {last_modified, position}
      # ファイルが前回チェック時より更新されていない場合もスルー
      File.stat!(stream.path).mtime == last_modified ->
        # カレントポジションはアップデートしておきます
        {last_modified, length(Enum.into(stream, []))}
      true ->
        # パイプライン演算子を使って、カレントポジション以降の行をリスト化
        lines = stream
        |> Stream.drop(position)
        |> Enum.into([])
        if (length(lines) > 0) do
          # "tails:sample" チャンネルに行のリストを broadcast
          PhoenixTail.Endpoint.broadcast! "tails:sample", "new_lines", %{lines: lines}
        end
        # 最終更新日時とカレントポジションをアップデートしておきます
        {File.stat!(stream.path).mtime, position + length(lines)}
    end
  end
end
```

GenServer が用意している callback 用の関数だけで実装できました。

GenServer 理解の１つのポイントとして、「状態を保持させる」ということがあります。この「状態」というのは、`init/1` や `handle_cast/2` で return している内容です。これが次回の関数コールに引き回されることで、状態の保持を実現しています。

プロセス指向独特の概念でちょっと分かりづらいですが...思考をシフトさせましょう。

# プロセスを Supervisor に登録する

プロセスの実装が終わったら、それを Phoenix の Supervisor に登録してみましょう。

```ex:lib/phoenix_tail.ex
defmodule PhoenixTail do
  use Application

  # See http://elixir-lang.org/docs/stable/elixir/Application.html
  # for more information on OTP Applications
  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    children = [
      # Start the endpoint when the application starts
      supervisor(PhoenixTail.Endpoint, []),
      # Start the Ecto repository
      worker(PhoenixTail.Repo, []),
      # Here you could define other workers and supervisors as children
      # worker(PhoenixTail.Worker, [arg1, arg2, arg3]),

      # 以下の1行を追加
      worker(PhoenixTail.Tail, ["data/sample.log"])
    ]
    ...
  end
  ...
end
```

これで起動時にプロセスが立ち上がり、監視されるようになります。
引数で渡している `data/sample.log` が tail のターゲットとなるのですが、このファイルについては後述します。

# フロントエンドを作る

次に、サーバから broadcast されるログ情報をレンダリングする部分を作ります。デフォルトで生成されるページをちょろっと改造して済ませましょう。

まずは依存 JS ライブラリを追加します。
Brunch を使ってもいいのですが、手間なので CDN から直接読み込ませます。

```jsp:web/templates/layout/app.html.eex
    ...

      <%= @inner %>

    </div> <!-- /container -->
    <script src="//code.jquery.com/jquery-2.1.4.min.js"></script>
    <script src="//cdn.jsdelivr.net/jsrender/1.0pre35/jsrender.min.js"></script>
    <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
  </body>
</html>

```

次にコンテンツです。
既存コードを全消しして以下のものだけにして大丈夫です。

```jsp:web/templates/page/index.html.eex
<div class="row">
  <div class="col-xs-12">
    <ul id="loglines" class="list-unstyled">
    </ul>
  </div>
</div>

<script id="tmpl-logline" type="text/x-jsrender">
  <li class="logline">
    {{>#data}}
  </li>
</script>
```

JS はこんな感じです。
これも既存は全消しで OK です。

```js:web/static/js/app.js
import {Socket} from "deps/phoenix/web/static/js/phoenix/"

var socket = new Socket("/socket");
socket.connect();
var channel = socket.channel("tails:sample", {});
channel.join();

var tmpl = $("#tmpl-logline"),
    loglines = $("#loglines");

channel.on("new_lines", function(dt) {
    var lines = tmpl.render(dt.lines);
    $(lines).each(function(i, e) {
        $(e).animate({ "background-color": "#fff" }, 1000);
    });
    loglines.prepend(lines);
});
```

最後に CSS　です。
これは末尾に追加で。ブラウザ差分などは適宜調整してください...

```css:web/static/css/app.css
...
.logline {
  font-size: 8px;
  margin: 5px 0;
  padding: 5px 0;
  border-radius: 4px;
  border-bottom: 1px solid #ddd;
  background-color: #fff;
  animation-name: animation;
  animation-duration: 1s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: 1;
}
.logline:first-child {
  font-size: 16px;
}
@-webkit-keyframes animation {
    0%     {background-color: #f5f5dc;}
    100.0%  {background-color: #ffffff;}
}
```

以上でフロントエンドは完了です。

# ダミーのログファイルを作る

本当は実際に Web サービスが稼働しているサーバの apache ログを対象にしたかったのですが、手頃なものがなかったので、ダミーを生成します。
世の中便利なもので、apache ログを生成してくれるツールが存在しました。

[tamtam180/apache_log_gen]( https://github.com/tamtam180/apache_log_gen)

これをインストールして、先ほど指定した `data/sample.log` にツラツラとダミーを出力させておきましょう（相対パスなので実行するディレクトリにはご留意ください）

```bash
$ apache-loggen --rate=2 data/sample.log
```

# 動作を確認する

さて、以上で準備は整いました。
おもむろにアプリケーションを起動して、`localhost:4000` にアクセスしてみましょう。

![capture.gif](https://qiita-image-store.s3.amazonaws.com/0/77513/3e6cbe11-37da-bf5e-acba-9a1250f0cebc.gif)

いい感じですね！
一定量のダミーが生成されているのでリアルタイム感が薄いですが、紛れもなく tail の挙動です。

# 感想

* ネタでやったつもりが、意外と GenServer の勉強になった
* 「[開発時にログを見るならtailよりもless！！](http://qiita.com/yusabana/items/b5cc2a706be8c031043e)」というツッコミはNG
* ブラウザから tail するファイルを指定できたりすると素敵
* サーバをまたいでファイルを指定できるともっと素敵