Loggerの
構造と拡張
Genki Sugawara
Elixir Meetup #1 in Drecom
お前誰よ
• twitter: @sgwr_dts
• github/bitbicket: winebarrel
• インフラまわりのお仕事やってます
• RubyとかAWSとか
• Elixirレベル: たまねぎ剣士
事の始まり
• mariaex
• MariaDB/MySQLのクライアント
{:ok, p} = Mariaex.Connection.start_link(
username: "ecto", database: "ecto_test")
rs = Mariaex.Connection.query(
p, "SELECT id, title FROM test1")
事の始まり
• 接続時のエラーをハンドルしたい
• ユーザ名・パスワードの間違い
• DB名の間違い…etc
事の始まり
start_linkでのエラー時に{:error, _}が返ってくるので
ハンドルしてみる
# mariaex: v0.4.3
_pid = case Mariaex.Connection.start_link(username: "ecto", database: "ecto_test") do
{:ok, pid} -> pid
{:error, err} ->
IO.puts err.mariadb.message
exit(1)
end
事の始まり
!?
事の始まり
handle_otp_reportsをfalseにしてみる
config :logger, handle_otp_reports: false
事の始まり
!?
事の始まり
• handle_otp_reportsをfalseにするとErlangのエラーハンドラ
が有効になる
• …仕方ないのでLoggerのソースコードを読むことに
Logger
• http://elixir-lang.org/docs/stable/logger/Logger.html
• https://github.com/elixir-lang/elixir/tree/master/lib/logger
• 標準ライブラリではない?ビルトインのロギングモジュール
Logger
mixでプロジェクトを作ると、デフォルトで起動
defmodule Hoge.Mixfile do
use Mix.Project
# ...
def application do
[applications: [:logger],
mod: {Hoge, []}]
end
Logger
mixでプロジェクトを作ると、デフォルトで起動
defmodule Hoge.Mixfile do
use Mix.Project
# ...
def application do
[applications: [:logger],
mod: {Hoge, []}]
end
Logger
こんな感じでコンソールにログ出力
iex(1)> require Logger
nil
iex(2)> Logger.info "oops"
:ok
18:45:59.211 [info] oops
Supervision Tree
Logger.App (supervisor: rest_for_one)
+ Logger.Watcher (worker①)
| + Logger.Watcher (supervisor: simple_one_for_one)
| + Logger.Watcher (worker②)
+ Logger.Watcher (worker③)
Supervision Tree
[worker(GenEvent, [[name: Logger]]),
worker(Logger.Watcher, [Logger, Logger.Config, []],
[id: Logger.Config, function: :watcher]),
supervisor(Logger.Watcher, [Logger.Config, :handlers, []]),
worker(Logger.Watcher,
[:error_logger, Logger.ErrorHandler,
{otp_reports?, sasl_reports?, threshold}, :link],
[id: Logger.ErrorHandler, function: :watcher])]
Logger.App
• アプリケーションモジュール
• プロジェクト開始時にLogger.App.start()を実行
Logger.Watcher
• supervisor兼worker
• GenEvent(name: Logger)のmonitor
• GenEventにハンドラを登録
• 再起動戦略のためにsupervisor化?
Logger.Watcher
# mod: GenEvent(Logger)
def init({mod, handler, args, :monitor}) do
ref = Process.monitor(mod)
res = GenEvent.add_mon_handler(mod, handler, args)
do_init(res, mod, handler, ref)
end
def init({mod, handler, args, :link}) do
res = :gen_event.add_sup_handler(mod, handler, args)
do_init(res, mod, handler, nil)
end
Handlers
• GenEventのハンドラ
• handle_event()でログ出力(とか)を行う
Handlers
以下のような感じで、
イベントハンドラにメッセージが送られる
Logger.info(msg)
-> Logger.bare_log(msg)
-> Logger.notify(msg)
-> GenEvent.notify(Logger, msg)
Handlers
• Logger.Watcher worker①
• → Logger.Config
• Logger.Watcher worker②
• → Logger.Backends.Consoleとか
• Logger.Watcher worker③
• → Logger.ErrorHandler
Logger.Config
• log levelやmodeなどの設定を保持
• なんでイベントをハンドルしてんの?
• → sync/asyncを変更するため
Logger.Config
:sync_threshold - if the Logger manager has more than
sync_threshold messages in its queue, Logger will change to sync
mode, to apply backpressure to the clients.
• ログメッセージのイベントを受信
• → メッセージキューの長さをみて動的に同期・非同期を変更
Logger.Backends.Console
• コンソールに出力するBackend
• ログ出力先(Backend)の一実装
• ビルトインで実装されているのはこれだけ
Logger.Backends.Console
設定の:consoleはLogger.Backends.Consoleに変換される
カスタムバックエンドはモジュールを渡す必要がある
config :logger, backends: [:console] # デフォルト値
def translate_backend(:console), do: Logger.Backends.Console
def translate_backend(other), do: other
Logger.ErrorHandler
• ErlangのOTP/SASLのエラーログをハンドル
Logger.ErrorHandler
そもそもErlangのOTP/SASLのエラーはハンドルされてるっぽい
• :error_logger
• http://www.erlang.org/doc/man/error_logger.html
以下のハンドラがデフォルトで追加されている
• error_logger_tty_h
• sasl_logger_tty_h
OTPのエラーハンドリング
ErrorHandler追加時の動作:
handle_otp_reports: trueの場合
• error_loggerからerror_logger_tty_hを削除
• GenEventにErrorHandlerを追加
• state: otp: true
OTPのエラーハンドリング
ErrorHandler追加時の動作:
handle_otp_reports: falseの場合
• error_loggerの操作は行わない
• GenEventにErrorHandlerを追加
• state: otp: false
OTPのエラーハンドリング
handle_otp_reportsがtrue/falseに関わらず
Logger.ErrorHandlerには
OPTのエラーイベント(error_report等)は飛んでくる
ただし、state: otp: trueの場合、ログ出力し
state: otp: falseの場合はログ出力しない
OTPのエラーハンドリング
結果として
• handle_otp_reports: true
• ErrorHandler: ログ出力あり (state: otp: trueのため)
• error_logger: ログ出力なし (ハンドラが削除されているため)
• handle_otp_reports: false
• ErrorHandler: ログ出力なし (state: otp: falseのため)
• error_logger: ログ出力あり (ハンドラがそのままのため)
OTPエラー出力の抑止
config :logger, handle_otp_reports: false
# 手動でerror_loggerからハンドラを削除
:error_logger.delete_report_handler(:error_logger_tty_h)
# mariaex: v0.4.3
_pid = case Mariaex.Connection.start_link(username: "ecto", database: "ecto_test") do
{:ok, pid} -> pid
{:error, err} ->
IO.puts err.mariadb.message
exit(1)
end
おまけ: 雑カスタムBackend
defmodule MyBackend do
use GenEvent
def init(__MODULE__) do
init({:user, []})
end
def init({device, _opts}) do
config = Application.get_env(:logger, :my_backend, [])
format = Keyword.get(config, :format) |> Logger.Formatter.compile
level = Keyword.get(config, :level, :info)
metadata = Keyword.get(config, :metadata, [])
{:ok, %{format: format, metadata: metadata,
level: level, colors: %{}, device: device}}
end
おまけ: 雑カスタムBackend
def handle_call({:configure, _options}, state) do
{:ok, :ok, state}
end
def handle_event({_level, gl, _event}, state)
when node(gl) != node() do
{:ok, state}
end
def handle_event({level, _gl, {Logger, msg, _ts, _md}}, state) do
IO.inspect [level, msg]
{:ok, state}
end
end
おまけ: 雑カスタムBackend
config :logger, backends: [MyBackend]
$ mix run test.exs
[:info, "hello"]
以上です
ご静聴ありがとうございました

Elixir Meetup #1 Loggerの構造と拡張