LivebookでElixirアプリケーションにWeb UIを追加する
Webアプリではないけれども、アプリケーションの現在の状況がどうなっているかを可視化するために、ちょっとしたUIをつけたいということがあったりすると思います。たとえば、記録され続ける時系列データをちょっとグラフ化して見てみたいとか。しかし、それだけのためにPhoenixを使ってWebアプリを作るのも面倒です。
そこで、Livebookを使うことでアプリケーションに動的Web UIを簡単に追加してみます。
アプリケーション
こんなアプリケーションを作っているとしましょう。1秒ごとに0〜100までのランダムな数字を生成して、時系列データとして保持しておくというものです。これ自体は特に意味のあるものではないですが、たとえば、デバイスから送られてきたセンサーデータを受け取って記録するアプリケーションだと思ってください。
ここで、記録された時系列データをグラフとして可視化したいと思った時にどうするか?というのが本記事でやりたいことになります。
defmodule LivebookSidecar.Worker do
use GenServer
@impl GenServer
def init(_init_arg) do
{:ok, %{data: []}, {:continue, :init_worker}}
end
def start_link(init_arg) do
GenServer.start_link(__MODULE__, init_arg, name: __MODULE__)
end
@impl GenServer
def handle_continue(:init_worker, state) do
Process.send_after(self(), :work, 0)
{:noreply, state}
end
@impl GenServer
def handle_info(:work, state) do
data =
state.data
|> Enum.concat([
%{
datetime: Timex.now("Asia/Tokyo"),
count: Enum.random(0..100)
}
])
Process.send_after(self(), :work, 1_000)
{:noreply, %{state | data: data}}
end
@impl GenServer
def handle_call(:retrieve_data, _from, state) do
{:reply, state.data, state}
end
end
上記のプロセスにアクセスするためのヘルパーAPIを用意しておきます。
defmodule LivebookSidecar do
def retrieve_data() do
GenServer.call(LivebookSidecar.Worker, :retrieve_data)
end
end
Livebookの設定
Livebookとは、ElixirのためのJupyterLabのようなものです。
まず、上記のアプリケーションにLivebookを組み込んでいく必要があります。mix.exs
に依存ライブラリを追加します。
{:livebook, "~> 0.2.3"},
{:vega_lite, "~> 0.1.0"},
{:kino, "~> 0.3.0"},
そして、config/config.exs
にLivebookの設定を追加します。以下では、最低限のLivebookのWebアプリケーションへの接続に関する設定などを記述しています(セキュリティ的な保護は全くしていないので、誰でもアクセス可能な場所に置くのには向いていない設定です)。
mport Config
config :livebook, LivebookWeb.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 8080],
server: true,
pubsub_server: Livebook.PubSub,
secret_key_base: Base.encode64(:crypto.strong_rand_bytes(48)),
live_view: [signing_salt: "livebook"]
config :livebook,
default_runtime: {Livebook.Runtime.Embedded, []},
authentication_mode: :disabled,
cookie: :livebook_sidecar,
root_path: "./livebooks"
config :phoenix, :json_library, Jason
ここで、LivebookのランタイムをLivebook.Runtime.Embedded
に設定しています。Livebookのランタイムには、以下の4つの種類があります。
- Eilxir Standalone: Livebook用に独立したElixirランタイム(ノード)を起動してコードを実行する
- Mix Standalone: 上記と同じだが、mixプロジェクトを指定して依存ライブラリを読み込んだコンテキストでコードを実行する
- Attached Node: すでに起動している他のノードに接続し、そのコンテキストでコードを実行する
- Embedded: 組込みデバイスなどノードを別途起動できない環境のために、Livebookが組み込まれたアプリケーションのコンテキストでコードを実行する(グローバルな環境を共有する)
今回の目的を達成するためには、ターゲットとなるアプリケーションを実行しているノードに接続するAttached Nodeを用いても良かったのですが、Nervesデバイスなどでも同じように使えるEmbeddedランタイムを使ってみることにしました。
時系列データのグラフ化
上記の設定を施した状態で、iexを起動します。
$ iex -S mix
すると、localhost:8080
でLivebookが起動します。適当にノートブックを作って、以下のようなコードを書き込んで実行してみましょう。
alias VegaLite, as: Vl
data =
LivebookSidecar.retrieve_data()
|> Enum.map(fn row ->
{:ok, datetime} = Timex.format(row.datetime, "{ISO:Extended}")
Map.put(row, :datetime, datetime)
end)
Vl.new(width: 800, height: 400)
|> Vl.data_from_values(data)
|> Vl.mark(:line)
|> Vl.encode_field(:x, "datetime", type: :temporal, title: "time")
|> Vl.encode_field(:y, "count", type: :quantitative, title: "count")
Embeddedランタイムを用いているので、ターゲットとなるアプリケーションとグローバルな状態を共有しているため、LivebookSidecar
モジュールにアクセスできます。そのモジュールを通じて、記録されている時系列データを取り出して、VegaLiteライブラリを用いてグラフを作っています。
上記を実行すると、こんな感じでグラフが表示されます。
こうして、時系列データを記録するアプリケーションに対して、簡単にWeb UIを用意してグラフ化することができました。Livebookを用いると、こういう時に簡単に動的なWeb UIが作れるのも便利ですね。
本記事で用いたコードは、以下に置いてあります。適宜ご参照ください。
Discussion
Livebookのバージョンが上がったことで少し設定項目がかわったようです。とりあえず手元で組み込めて動いた程度のものですが、コメントさせてください。
Livebook version: 0.6.1
おお、ありがとうございます!