LoginSignup
28
26

More than 5 years have passed since last update.

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

Posted at

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

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

_人人人人人人人人人人_
> フェニックスの尾1 <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

完成イメージ

こんな感じです。

capture.png

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

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

$ mix phoenix.new phoenix_tail

WebSocket の設定をしておきます(詳細はコチラ

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 だけ実装します。

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 さんのコチラの記事が大変参考になります。

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

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

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

プロセスを Supervisor に登録する

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

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 から直接読み込ませます。

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>

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

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 です。

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 です。
これは末尾に追加で。ブラウザ差分などは適宜調整してください...

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

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

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

動作を確認する

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

capture.gif

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

感想

  • ネタでやったつもりが、意外と GenServer の勉強になった
  • 開発時にログを見るならtailよりもless!!」というツッコミはNG
  • ブラウザから tail するファイルを指定できたりすると素敵
  • サーバをまたいでファイルを指定できるともっと素敵

  1. 残念ながら正確には Phoenix Down と訳すらしい。 

28
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
26