Elixir にはログ出力の機能を提供する Logger
モジュールがあります。が、標準ではログはコンソールに出力されます。
これをファイルに出力する方法を調べたのでまとめておきます。
Logger
モジュールのドキュメントはこちら。
ここで書いたコードは GitHub にも push しています。
custom backend を書く
Logger
モジュールは、アプリケーションがログを記録するための Logger.info/1
, Logger.error/1
といった関数と、それらの関数が呼ばれたときに実際にコンソールやファイルに出力する機能を持っています。この実際に出力する部分は backends として Logger
モジュール本体から分離されていて、出力先を追加したり差し替えたりできるようになっています。
デフォルトのコンソールへの出力は次の場所で実装されています。
ログをファイルに出力するための custom backend を作成し、Logger
モジュールに設定することでファイル出力を実現していきます。
プロジェクトを用意する
mix new
コマンドでプロジェクトを作成します。
mix new
で作成されたプロジェクトは実行時にデフォルトで Logger アプリケーションを起動するので後の作業が楽ですし。
プロジェクト名、ソースファイル名は自身でつけた名前に読み替えて読み進めてください。
$ mix new logger_sample_backend
$ cd logger_sample_backend
mix.exs
ファイルを見ると Logger アプリケーションの起動が設定されているのがわかります。
def application do
[
extra_applications: [:logger]
]
end
behaviour を設定する
custom backend は :gen_event
として振舞う必要があるので
backend として利用するモジュールに @behaviour :gen_event
を記述します。
defmodule LoggerSampleBackend do
@behaviour :gen_event
end
この状態でコンパイルすると、実装が必要な関数が実装されていないと警告が出ます(コンパイル自体はできます)。
$ mix compile
Compiling 1 file (.ex)
warning: function handle_call/2 required by behaviour :gen_event is not implemented (in module Sample)
lib/logger_sample_backend.ex:1
warning: function handle_event/2 required by behaviour :gen_event is not implemented (in module Sample)
lib/logger_sample_backend.ex:1
warning: function init/1 required by behaviour :gen_event is not implemented (in module Sample)
lib/logger_sample_backend.ex:1
Generated logger_sample_backend app
init を書く - 初期化
init/1
を記述します。
引数にはモジュール名を指定します。ここでは init/1
を記述するモジュール自身なので __MODLE__
マクロで指定しています。
戻り値は :ok
と backend が利用する任意の情報とからなるタプルです。
ここではログの出力先のファイルパスとログのフォーマットを値にもつ Map を与えています。
@path "./log/sample.log"
@format "$date $time [$level] $message"
def init(__MODULE__) do
{:ok, %{path: @path, format: @format}}
end
フォーマットについては Logger.Formetter
モジュールのドキュメントを参照してください。
handle_call を書く - 実行時の設定
Logger.configure_backend/2
関数を利用して実行時に backend の設定を変更することができます。変更を受け付けるには handle_call/2
関数を実装する必要があります。
第 1 引数には : configure
と Logger.configure_backend/2
から渡されるキーワードリストのペアのタプルを受け取ります。
第 2 引数には設定情報を受け取ります。初期化直後であれば init/2
で設定した値が格納されています。
例えば次のように実行した場合、
Logger.configure_backend(LoggerSampleBackend, path: "./log/error.log)
handle_call/2
が handle_call({:configure, [path: "./log/error.log]}, state)
という形で呼び出されます。
戻り値には次の 3 つの値のタプルを返します。
:ok
Logger.configure_backend/2
の戻り値になる値
- 更新した設定情報
ここでは :path
と :format
というキーでファイルパスとログのフォーマットを受け取って情報を更新し、更新された情報を戻り値として返しています。
def handle_call({:configure, opts}, state) do
path = Keyword.get(opts, :path, state.path)
format = Keyword.get(opts, :format, state.format)
new_state = %{state | path: path, format: format}
{:ok, {:ok, new_state}, new_state}
end
handle_event を書く(1) - ログの出力
Logger.info/1
や Logger.error/1
が呼ばれると handle_event/2
が呼び出されます。ここで実際にファイルにログ情報を出力します。
第 1 引数にはレベル(info や error など)、ログメッセージ、タイムスタンプなどを格納したタプルを受け取ります。
第 2 引数には設定情報を受け取ります。
戻り値には :ok
と設定情報のペアのタプルを返します。ここでは設定の変更などはないので引数で受け取った値をそのまま返しています。
受け取った情報とフォーマットから出力するログを生成する部分は Logger.Formatter
のドキュメントを参照してください。
def handle_event({level, , {Logger, message, timestamp, metadata}}, state) do
state.path |> Path.dirname() |> File.mkdir_p()
log_line =
Logger.Formatter.format(
Logger.Formatter.compile(state.format),
level,
message,
timestamp,
metadata
)
File.write(state.path, "#{log_line}\n", [:append])
{:ok, state}
end
handle_event を書く(2) - 出力のフラッシュ
Logger.flush/0
関数が呼ばれた時にも handle_event/2
が呼び出されます。
この時は第 1 引数には : flush
が与えられます。
バッファリングしていた場合などにそれをフラッシュするための機能のようですが、今回は必要がないので何もしていません。
def handle_event(:flush, state) do
{:ok, state}
end
handle_info を書く - io_reply をハンドルする
動作の詳細を調べられていないのですが :console
と一緒に利用した場合、 :io_reply
を含むメッセージを受け取るようです。
処理を記述する必要はありませんが関数を定義しておかないとメッセージをハンドリングできないためエラーが発生します。
エラーが発生するとエラーログが出力され、その出力によってメッセージが送られ、そのメッセージをハンドリングできないためエラーが発生し、エラーが発生すると…と止まらくなります。
def handle_info({:io_reply, , :ok}, state) do
{:ok, state}
end
実行時の backend の設定
実行時、あるいはモジュール内のコードでこの backend の利用を設定するには Logger.add_backend/1
を利用します。
Logger.add_backend(LoggerSampleBackend)
実行すると、init/1
が init(LoggerSampleBackend)
という形で呼び出されます。
Logger.add_backend/1
の引数が LoggerSampleBackend.init/1
の引数として渡されるので、例えば次のようにするとモジュール名以外に情報を渡すこと不可能というわけではありません。
def init({__MODULE__, path}) do
...
end
Logger.add_backend({LoggerSampleBackend, "./log/info.log"})
config ファイルでの backend の設定
config ファイル config/config.exs
でも backend を設定できます。
config :logger
の設定に :backends
のキーを追加し値に backend のリストを記述します。
config :logger, backends: [LoggerSampleBackend]
backends は複数指定することができるので例えばデフォルトの :console
も利用するなら次のように記述します。
config :logger, backends: [:console, LoggerSampleBackend]
また Logger.add_backend/1
の例のように init/1
の引数が記述されていた場合、:backends
でも形式を合わせて記述します。
config :logger, backends: [{LoggerSampleBackend, "./log/info.log"}]
他のプロジェクトから利用する
ここでは logger_sample_backend
をプロジェクトとして用意しているので、mix.exs
の依存パッケージに追加することで利用できます。
defp deps do
[
{:logger_sample_backend, "~> 0.1", github: "mattsan/logger_sample_backend"}
]
end
追加した後は上記と同じように設定を記述するだけで利用できるようになります。
いつか読むはずっと読まない:艦隊集め
なにやら気づくと艦隊をコレクションしていた。
The Lost Fleet
彷徨える艦隊第一巻。このシリーズで battle cruiser という言葉を知りました。ただし巡るのは洋でないため「巡航戦艦」と訳出されています。
The Lost Fleet: Beyond the Frontier
彷徨える艦隊の新シリーズ。邦訳は前のシリーズから通巻になっています。
彷徨える艦隊の前日譚…数世紀ぐらい。これから読みます。
Black Fleet Trilogy
原題を確かめようとページを繰って THE BOOK ONE OF THE BLACK FLEET TRILOGY の文字を見つけた時に、思いました。
…またシリーズものに手を出してしまった。
The Man of War Trilogy
原題を確かめようとページを繰って THE MAN OF WAR TRILOGY BOOK 1 の文字を見つけた時に、思いました。
…またシリーズものに手を出してしまった。