今回は Logger のレベルの種類を増やすのに挑戦してみました。祝! Elixir 初コンパイル!
はじめに
2019年9月29日に東京で tokyo.ex#13 elixir本体ソースコードもくもくリード会 がありました。さらに東京の現地に行けない方むけに fukuoka.ex#30:tokyo.exとコラボ、Elixir本体コード読込会(リモートのみ) という会も同時開催されました。スタートの解説からして大変ディープで、自分で飛び込んだらメッチャ時間がかかりそうな Elixir の言語処理系の生成の話を、大変簡潔に説明していただけました。おかげでこれまでやったことのない Elixir 本体の手動での make を初めてやるという体験ができました。一度 make 出来たらちょっとソースを変えてみたくなるというのが人情というものです。そこで、以前から不満タラタラだった Logger をいじってみることにしました。
Elixir のバージョンに対するコメント (2020.01.28)
この記事は Elixir v1.9.xに対する改変を試みるものです。Elixir v1.10.x では Erlang/OTP21 以降の logger に準拠するため大きな変更が加えられています。このためこの記事での変更はそのままでは Elixir 1.10 以降では使えません。
Elixir のバージョンに対するコメント (2020.10.17)
Erlang/OTP における logger の大規模な変更が Elixir v1.11.x に引き継がれました。これにより Elixir 1.11.0 以降では、この記事の内容にあるようなレベル数不足が解消されています。
編集履歴
- 2019.11.07 コンソール出力の色について誤解していたので修正しました。
- 2020.01.28 Elixir 1.10.0 がリリースされたことによるコメント追加
- 2020.10.17 Elixir 1.11.0 がリリースされたことによるコメント追加
Loggerとは
動いているプログラムの状態を把握するのに、プログラム中に明示的に状態を出力する記述をすることが多いです。出力された内容をログ、それがファイルに落ちる場合にはログファイル、ログを取ることをロギングといい、デバッグには欠かせません。
ほとんどの言語にログを扱うライブラリ等が準備されています。Elixirではログを処理するためのLoggerライブラリが標準で提供されています。ちょっとしたことやるには十分な機能を持っています。2019年の元日に はじめてな Elixir(23) ログを出力する ってのを書いたので御覧ください。加えてログ出力を抑制する方法を はじめてな Elixir(24) ログを出力しない に書いたので合わせてご覧ください。
出力レベル
いろんなログシステムではログレベルの概念があります。デバッグには詳細なログがほしいですが、運用レベルになったらディスクスペースやパフォーマンスの問題で必要最小限のログだけにしたいとかなります。このため、ログの重要度をレベルという概念で表します。ElixirのLoggerには軽微な方から重篤な方に向かって以下の4つのレベルが用意されてます。
- :debug - デバッグ関連メッセージ
- :info - いろんな情報提供
- :warn - 警告
- :error - エラー
iex(1)> require Logger
Logger
iex(2)> Logger.info("This is a message for you")
17:37:39.376 [info] This is a message for you
:ok
iex(3)> Logger.warn("This is the other message for you")
17:38:33.110 [warn] This is the other message for you
:ok
ログ出力が全部見えていて煩わしい場合は出力レベルを設定することでログ出力を抑制できます。デフォルトでは全部のレベルが出力されますが、これをある程度以上の重要なログしか出力できないように出来ます。このレベルの設定はいくつかの方法があって、例えば実行中に動的に変更したければ Logger.configure/1
関数を用います。
iex(4)> Logger.configure(level: :warn)
:ok
iex(5)> Logger.warn("This is the other message for you")
17:51:33.922 [warn] This is the other message for you
:ok
iex(6)> Logger.info("This is a message for you")
:ok
この例は warn
レベル以上のログ出力のみ出すような設定にしたところです。warn レベルは出力されますが、info レベルは出力されてません。
Elixir ではレベルが4つに限定されている
ElixirのLoggerライブラリではこのレベル数が4という制限があります。通常はそう困ることはないのですが、ときどき5つ以上あれば良いのになと思う瞬間があります。
- すでに4つのレベルを使い切っている状況で、どのレベルにもうまく当てはまらない出力用にもう1レベル欲しい場合
- MacOSX で syslog を使って開発している場合
私は MacOSX ユーザで今 10.13.6 を使っています。MacOSX では warn と error レベルが syslog に記録されます。しかし、info や debug レベルはデフォルトの状態では記録されません。実質、レベルが2つしかなくなってしまうので、かなり不便です。
Elixir以外のロガー
Elixir 以外でログ取得にはどんな種類があるのかというと、代表的なのは syslog のプロトコルです。これは IETF の RFC5424: The Syslog Protocol で規定されています。UNIXでは標準的に実装されていて、Erlangのロガーもレベルについてはこれに準拠しています。
UNIXでのsyslog
syslog プロトコル周りは UNIX マシンなら syslog
コマンドとして実装されているので、簡単にお試し出来ます。以下は MacOSX の場合です。
- コンソール.app を起動する(アプリケーション → ユーティリティ にあります)
- 画面に大量のログメッセージが出てくるので右上の検索窓に
sysylog
と入れておくと必要なログのみ見えます
- 画面に大量のログメッセージが出てくるので右上の検索窓に
- ターミナル.app を起動する(これもユーティリティ下にあります)
- syslog コマンドでメッセージを投入する
ターミナル.app で syslog コマンドを以下のように打ってみます。
$ syslog -s -l Emergency 'Emerg message'
$ syslog -s -l Alert 'A message'
$ syslog -s -l Critical 'C message'
$ syslog -s -l Error 'Error message'
$ syslog -s -l Warning 'W message'
$ syslog -s -l Notice 'N message'
$ syslog -s -l Info 'I message'
$ syslog -s -l Debug 'D message'
するとコンソール.app の画面に以下のように出てきて OS が記録しているのが分かります。
ただしよく見てみると Info レベルと Debug レベルのメッセージが出ていません。これが MacOSX のデフォルトで、これを出すようにするのはちょっと厄介です。このため Mac で Elixir プログラムの開発をする場合、Logger を使っても Warning レベルと Error レベルしか使えなくて、かなり窮屈です。
なお、Linux や *BSD 等でも同様の事ができて、おそらく /var/log
の下のファイルに記録が残っているはずです。この場合は全てのレベルのログを出すのは難しくないです。
Erlang のロガー
Erlang にも logger という名前のロガーがあります。これの詳細がユーザズガイド Kernel User's Guide, 2.2 Logger APIにあり、これによるとログレベルについては RFC5424 に準拠しているということが分かります。
Elixir の Logger の公式ドキュメントの記述を見ると「Erlang の logger を使ってる」と先頭に記述があります。ですのでログレベルが4つしかないのは Erlang が原因ではなく Elixir 側にありそうです。とすると工夫すれば Elixir でも同様のレベルで出力が可能そうかなと期待を抱かせます。以下、結果的にうまく出来たようなので、備忘録も兼ねて記録を残しておきます。
Elixir の Logger ライブラリを改造する
さてイントロが長くなりました。では Logger に手を加えてログ出力レベルを増やしてみましょう。今回はElixirのコードを読むというもくもく会での活動なので、ソースコードを読みつつ、コードに手を入れてみるということをします。環境は以下です。
- MacOS 10.13.6
- Erlang/OTP 22.1
- Elixir 1.9.1
Erlang と Elixir のバージョンは以下のリポジトリから持ってきたときのバージョンです。あと、参照しているドキュメントもこれに従っています。
$ iex
Erlang/OTP 22 [erts-10.5] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]
Interactive Elixir (1.9.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
準備
まずは Elixir の github リポジトリを clone か download して手元に持ってきてください。ローカルPCの git のディレクトリの下に elixir
というディレクトリが新たに出来ているはずです。
$ tree -d -L 2 elixir
elixir
├── bin
├── lib
│ ├── eex
│ ├── elixir
│ ├── ex_unit
│ ├── iex
│ ├── logger
│ └── mix
└── man
この elixir
ディレクトリに行って make
コマンドを使うと(マシンの性能によりますが数分で)Elixirが出来上がります。正常に make が終了したら bin
ディレクトリの下に実行形式が出来ているのを確認してください。開発中のディレクトリ git/elixir
で明示的にパスを指定して ./bin/iex
と実行すると何やら怪しい version 番号 1.10.0-dev で iex が起動されているのが分かります。
$ ./bin/iex
Erlang/OTP 22 [erts-10.5] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]
Interactive Elixir (1.10.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
Logger プログラムを変更する
準備ができたので Logger の中身を変更します。まずは、ディレクトリ構造を見てみます。Logger は lib の下にあります。
$ tree -d -L 2
.
├── bin
├── lib
│ ├── eex
│ ├── elixir
│ ├── ex_unit
│ ├── iex
│ ├── logger
│ └── mix
└── man
Elixir 本体や ExUnit や mix などと並んで logger
という一つのディレクトリに入っていて、これはなかなか扱いがよろしいようです。さらに中を見てみます。
$ tree lib/logger/lib
lib/logger/lib
├── logger
│ ├── app.ex
│ ├── backend_supervisor.ex
│ ├── backends
│ │ └── console.ex
│ ├── config.ex
│ ├── erlang_handler.ex
│ ├── error_handler.ex
│ ├── formatter.ex
│ ├── translator.ex
│ ├── utils.ex
│ └── watcher.ex
└── logger.ex
2 directories, 11 files
この中のファイルをいぢると良さそうですね。以下、紆余曲折の果に出来た結果ですが、私が潮干狩りした時間軸を追わずに「これをやれば出来る」というのを記録しておきます。
logger.ex での level の定義
これに level が定義されています。先頭は logger の説明文がどっさりあり、440行目ぐらいに出てきます。
@type level :: :error | :info | :warn | :debug
@type metadata :: keyword()
@levels [:error, :info, :warn, :debug]
レベルの定義が4種類しかないだけでなく、重大なレベルの順にも並んでません。これ、おそらく :error と :info は Elixir 的に特別扱いをしているせいなのだと思います。なお、ここでは記述順序はプログラムとしては問題ではありません。プログラム上は、重要性を自然数で表していて、ここでの順序とは無関係です(後で出てくる Logger.Config.level_to_number/1
関数で定義しています)。
さて、これにもう4つを加えてRFC5424に準拠して8つにしましょう。付け加えるのは :emergency
:alert
:critical
:notice
です。2行を書き換えます。
# @type level :: :error | :info | :warn | :debug
@type level :: :emergency | :alert | :critical | :error | :warn | :notice | :info | :debug
@type metadata :: keyword()
# @levels [:error, :info, :warn, :debug]
@levels [:emergency, :alert, :critical, :error, :warn, :notice, :info, :debug]
logger.ex での macro の定義
同じく logger.ex ファイルで750行目ぐらいに行きましょう。
Logger を使ったログ出力には2通りあります。
- Logger.log/3
関数を使う。このとき第1引数にログレベルを渡す。
- Logger.warn/2
Logger.info/2
Logger.error/2
Logger.debug/2
関数を使う
これらはどれも、ログ出力の関数をマクロでラップしてます。例えば Logger.log(:error, "Panic!")
と Logger.error("Panic!")
は、最終的には同じ動作をします。後者は関数名がログレベル名になってますので、ログレベルの種類だけ用意することになります。よって以下を defmacro debug
のマクロ定義の後ろの800行目ぐらいに追加します。
defmacro notice(chardata_or_fun, metadata \\ []) do
maybe_log(:notice, chardata_or_fun, metadata, __CALLER__)
end
defmacro emergency(chardata_or_fun, metadata \\ []) do
maybe_log(:notice, chardata_or_fun, metadata, __CALLER__)
end
defmacro alert(chardata_or_fun, metadata \\ []) do
maybe_log(:notice, chardata_or_fun, metadata, __CALLER__)
end
defmacro critical(chardata_or_fun, metadata \\ []) do
maybe_log(:notice, chardata_or_fun, metadata, __CALLER__)
end
config.ex
この中には、ログレベルの名前と自然数の対応が定義されています。もともとは4種類です。
defp level_to_number(:debug), do: 0
defp level_to_number(:info), do: 1
defp level_to_number(:warn), do: 2
defp level_to_number(:error), do: 3
これを8種類にしましょう。
defp level_to_number(:debug), do: 0
defp level_to_number(:info), do: 1
defp level_to_number(:notice), do: 2
defp level_to_number(:warn), do: 3
defp level_to_number(:error), do: 4
defp level_to_number(:critical), do: 5
defp level_to_number(:alert), do: 6
defp level_to_number(:emergency), do: 7
しかしね、これね RFC5424 の Numerical Code と逆順なんですよね。いちいち引っかかるなぁ。
erlang_handler.ex
この中には Erlang のログレベルを Elixir のログレベルに変換するプライベート関数があります。
defp erlang_level_to_elixir_level(:emergency), do: :error
defp erlang_level_to_elixir_level(:alert), do: :error
defp erlang_level_to_elixir_level(:critical), do: :error
defp erlang_level_to_elixir_level(:error), do: :error
defp erlang_level_to_elixir_level(:warning), do: :warn
defp erlang_level_to_elixir_level(:notice), do: :info
defp erlang_level_to_elixir_level(:info), do: :info
defp erlang_level_to_elixir_level(:debug), do: :debug
って、あのなぁ。なぜ縮退させる。そのままでエイやん!君の気持ちが分からない。
defp erlang_level_to_elixir_level(:emergency), do: :emergency
defp erlang_level_to_elixir_level(:alert), do: :alert
defp erlang_level_to_elixir_level(:critical), do: :critical
defp erlang_level_to_elixir_level(:error), do: :error
defp erlang_level_to_elixir_level(:warning), do: :warn
defp erlang_level_to_elixir_level(:notice), do: :notice
defp erlang_level_to_elixir_level(:info), do: :info
defp erlang_level_to_elixir_level(:debug), do: :debug
とまあこれでエイ!
error_handler.ex
このモジュールの先頭に
# TODO: Remove this module when we require Erlang/OTP 21+.
とあります。Logger は Elixir のプロセスを使ってて Supervisor で監視しています。読み込んだわけではないのでなんともですが、微妙なタイミングでプロセスが終了したときの予期せぬ振る舞いを避けるために記述しているモジュールのような印象を受けます。Erlang の logger が進化したら、このモジュールを使うのをやめてシンプルにしようという意図のように捉えてます。
## Helpers
defp log_event({:error, gl, {pid, format, data}}, %{otp: true} = state),
do: log_event(:error, :format, gl, pid, {format, data}, state)
defp log_event({:error_report, gl, {pid, :std_error, format}}, %{otp: true} = state),
do: log_event(:error, :report, gl, pid, {:std_error, format}, state)
defp log_event({:error_report, gl, {pid, :supervisor_report, data}}, %{sasl: true} = state),
do: log_event(:error, :report, gl, pid, {:supervisor_report, data}, state)
defp log_event({:error_report, gl, {pid, :crash_report, data}}, %{sasl: true} = state),
do: log_event(:error, :report, gl, pid, {:crash_report, data}, state)
defp log_event({:warning_msg, gl, {pid, format, data}}, %{otp: true} = state),
do: log_event(:warn, :format, gl, pid, {format, data}, state)
defp log_event({:warning_report, gl, {pid, :std_warning, format}}, %{otp: true} = state),
do: log_event(:warn, :report, gl, pid, {:std_warning, format}, state)
defp log_event({:info_msg, gl, {pid, format, data}}, %{otp: true} = state),
do: log_event(:info, :format, gl, pid, {format, data}, state)
defp log_event({:info_report, gl, {pid, :std_info, format}}, %{otp: true} = state),
do: log_event(:info, :report, gl, pid, {:std_info, format}, state)
defp log_event({:info_report, gl, {pid, :progress, data}}, %{sasl: true} = state),
do: log_event(:info, :report, gl, pid, {:progress, data}, state)
というのがずらずらと書いてあります。マクロで書いてしまったほうが記述が簡潔なように思いますが、ま、ここは黙ってそのままコピペベースでログレベルごとに記述することにしましょう。以下を追加します。
defp log_event({:critical_msg, gl, {pid, format, data}}, %{otp: true} = state),
do: log_event(:critical, :format, gl, pid, {format, data}, state)
defp log_event({:critical_report, gl, {pid, :std_critical, format}}, %{otp: true} = state),
do: log_event(:critical, :report, gl, pid, {:std_critical, format}, state)
defp log_event({:critical_report, gl, {pid, :progress, data}}, %{sasl: true} = state),
do: log_event(:critical, :report, gl, pid, {:progress, data}, state)
defp log_event({:alert_msg, gl, {pid, format, data}}, %{otp: true} = state),
do: log_event(:alert, :format, gl, pid, {format, data}, state)
defp log_event({:alert_report, gl, {pid, :std_alert, format}}, %{otp: true} = state),
do: log_event(:alert, :report, gl, pid, {:std_alert, format}, state)
defp log_event({:alert_report, gl, {pid, :progress, data}}, %{sasl: true} = state),
do: log_event(:alert, :report, gl, pid, {:progress, data}, state)
defp log_event({:emergency_msg, gl, {pid, format, data}}, %{otp: true} = state),
do: log_event(:emergency, :format, gl, pid, {format, data}, state)
defp log_event({:emergency_report, gl, {pid, :std_emergency, format}}, %{otp: true} = state),
do: log_event(:emergency, :report, gl, pid, {:std_emergency, format}, state)
defp log_event({:emergency_report, gl, {pid, :progress, data}}, %{sasl: true} = state),
do: log_event(:emergency, :report, gl, pid, {:progress, data}, state)
formatter.ex
この中に細かいハックというか、ログレベル名の長短でフォーマットが壊れるのを防ぐための空白文字のパッドを入れる関数があります。
defp levelpad(:debug), do: ""
defp levelpad(:info), do: " "
defp levelpad(:warn), do: " "
defp levelpad(:error), do: ""
これを最長のに合わせる…
defp levelpad(:debug), do: " "
defp levelpad(:info), do: " "
defp levelpad(:notice), do: " "
defp levelpad(:warn), do: " "
defp levelpad(:error), do: " "
defp levelpad(:critical), do: " "
defp levelpad(:alert), do: " "
defp levelpad(:emergency), do: ""
と emergency に合わせることになるので、これはいかがなものかなぁ。
defp levelpad(:debug), do: " "
defp levelpad(:info), do: " "
defp levelpad(:notice), do: ""
defp levelpad(:warn), do: " "
defp levelpad(:error), do: " "
defp levelpad(:critical), do: ""
defp levelpad(:alert), do: " "
defp levelpad(:emergency), do: ""
とかでも良いように思います。
backends/console.ex
このモジュールでは出力の色に関する記述があります。コンソール出力の場合で ANSI に従って色が出せる場合はログレベルに従った色で出力します。
defp configure_colors(config) do
colors = Keyword.get(config, :colors, [])
%{
debug: Keyword.get(colors, :debug, :cyan),
info: Keyword.get(colors, :info, :normal),
warn: Keyword.get(colors, :warn, :yellow),
error: Keyword.get(colors, :error, :red),
enabled: Keyword.get(colors, :enabled, IO.ANSI.enabled?())
}
end
これは例えば以下のように増やせば良いです。
defp configure_colors(config) do
colors = Keyword.get(config, :colors, [])
%{
debug: Keyword.get(colors, :debug, :cyan),
info: Keyword.get(colors, :info, :normal),
notice: Keyword.get(colors, :notice, :magenta),
warn: Keyword.get(colors, :warn, :yellow),
error: Keyword.get(colors, :error, :light_red),
critical: Keyword.get(colors, :critical, IO.ANSI.color(130)),
alert: Keyword.get(colors, :alert, IO.ANSI.color(5,2,1)),
emergency: Keyword.get(colors, :emergency, :red),
enabled: Keyword.get(colors, :enabled, IO.ANSI.enabled?())
}
end
ANSI の色は elixir/lib/elixir/lib/io/ansi/ansi.ex
の中に以下の行があるので、これから選択します。
colors = [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white]
お好きな色をどうぞ… と言いたいところですがこれですと RGB の on/off で 8色しかなくて、そのうち2つは白黒なので避けるとすると残りは6色しかないです。
この他には :light_black, :light_red, :light_green, :light_yellow,:light_blue, :light_magenta, :light_cyan, :light_white
も定義されています。
さらに異なる色はつけるには IO.ANSI の color/1
関数か color/3
関数を用います。上では :notice に新しく :magenta を、後の新規に加えた :critical :alert :emergency は全部 :error 以上のレベルなので赤いに近い色にしました。
改造版 Logger をためしてみる
以上の改変をした上で git/elixir
ディレクトリで make します。すでに一通り make した後ならあっという間にコンパイルは終わります。
$ make
==> logger (compile)
Generated logger app
$
では新しく作った私の Elixir を実行してみます。
$ ./bin/iex
Erlang/OTP 22 [erts-10.5] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]
Interactive Elixir (1.10.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> require Logger
Logger
iex(2)> Logger.emergency("Emergency: system is unusable")
00:53:07.373 [emergency] Emergency: system is unusable
:ok
iex(3)> Logger.alert("Alert: action must be taken immediately")
00:53:23.485 [alert] Alert: action must be taken immediately
:ok
iex(4)> Logger.critical("Critical: critical conditions")
00:53:54.198 [critical] Critical: critical conditions
:ok
iex(5)> Logger.error("Error: error conditions")
00:54:09.337 [error] Error: error conditions
:ok
iex(6)> Logger.warn("Warning: warning conditions")
00:54:25.080 [warn] Warning: warning conditions
:ok
iex(7)> Logger.notice("Notice: normal but significant condition")
00:54:44.873 [notice] Notice: normal but significant condition
:ok
iex(8)> Logger.info("Informational: informational messages")
00:55:01.803 [info] Informational: informational messages
:ok
iex(9)> Logger.debug("Debug: debug-level messages")
00:55:14.875 [debug] Debug: debug-level messages
:ok
定義したレベルは全部出力できています。
次に出力を抑制してみます。:notice 以上のレベルのみ出力するように設定してみます。
iex(10)> Logger.configure(level: :notice)
:ok
iex(11)> Logger.notice("Notice: normal but significant condition")
00:55:52.293 [notice] Notice: normal but significant condition
:ok
iex(12)> Logger.info("Informational: informational messages")
:ok
iex(13)> Logger.warn("Warning: warning conditions")
00:58:43.521 [warn] Warning: warning conditions
:ok
:notice や :warn レベルは出力されますが :info レベルのログは抑制されました。うまいこと出来たようです。
まとめ
Elixir のソースコードを変更して、ログレベルが4に限定されていたのを、RFC5424 や Erlang 同様の8レベルにすることが出来ました。しかしながら、とにかく8レベルにするのにコードだけ改変したので、以下は例によって置き去りです。
- @doc をまるで書いていない(のでドキュメントを生成しても以前のまま)
- formatter を使っていない(のでテストすると一部のファイルが怒られる)
- 新規に増やした部分に関するテストを書いていない(ので動作確認が不十分)
- 動作を理解した上での改変でない(おそらく大丈夫という程度)
- 空白文字のパッドはちょっと考えたほうが良い(メッセージ本体の出力部分が少なくなりすぎるきらいがある)
Elixir 1.10 では (2020.01.28)
Elixir 1.9 であった erlang_handler.ex はなくなって、代わりに handler.ex というファイルがあります。この Logger.Handler モジュールの中の先頭近くに以下があります。
# TODO: Remove this mapping once we support all of Erlang types
def erlang_level_to_elixir_level(:none), do: :error
def erlang_level_to_elixir_level(:emergency), do: :error
def erlang_level_to_elixir_level(:alert), do: :error
def erlang_level_to_elixir_level(:critical), do: :error
def erlang_level_to_elixir_level(:error), do: :error
def erlang_level_to_elixir_level(:warning), do: :warn
def erlang_level_to_elixir_level(:notice), do: :info
def erlang_level_to_elixir_level(:info), do: :info
def erlang_level_to_elixir_level(:debug), do: :debug
def erlang_level_to_elixir_level(:all), do: :debug
ここで # TODO: Remove this mapping once we support all of Erlang types
とあるように、将来的には Erlang の log level を完全に Elixir でも使えるようにするつもりのようです。インストールしたらそのままで Logger を使えるようになるまで、もう少し待ちましょう。
謝辞
Elixir のコードを読もうというもくもく会のおかげで去年の年末頃から気になってた案件が解決しました。準備してくれた tokyo.ex と fukuoka.ex の関係者のみなさま、ありがとうございました。感謝します。
参考文献
- 言語仕様等
- 解説記事
- 上から見るか下から見るか by twitter @hayabusa33 さん
- Hacking Elixir HowTo by twitter @ohrdev さん
- Loggerの構造と拡張 by Sugawara Genki さん
- 自分の記事