RailsのActiveSupport::BacktraceCleanerという機能をご存知でしょうか。
「fukuoka.ex Elixir/Phoenix Advent Calendar 2021 22日目」の記事で紹介したActiveRecord::LogSubscriber#log_query_source(private)をElixir/Ectoに移植するぞの実装での悩みが始まりです。
↑の記事でecto_sqlの発生箇所をロギングする際のfiltter処理が若干不格好で、どうにかならないかなとRailsのコード呼んでいたところ、ActiveSupport::BacktraceCleaner
の実装を見つけました。
BacktraceCleaner内容はとてもシンプル。backtracesの中からgem等のログを排除してくれて、Rails内の自分で書いたログに絞ってくれるというナイスなやつです。
StacktraceCleanerというライブラリを作った
こちらです。
要件
-
deps/*
のライブラリを弾く -
.erl
,process
,ex_unit
系のログを弾く
functionは3種類
- clean/2: tacktracesの中からdeps系のログを排除
- current_stacktraces/1: 現在のstacktrace一覧の取得
- current_stacktrace/1: 現在のstacktraceの中から先頭を取得する
使い方
$ mix hex.new playground # ① テスト用の環境作成
$ cd playground # ② 移動
# mix.exsのdepsに `{:stacktrace_cleaner, "~> 0.1.1"},` を追加
$ mix deps.get # ③ ライブラリinstall
$ iex -S mix # mix環境でのiexの立ち上げ
iex(1)> StacktraceCleaner.current_stacktrace # 現在のstacktraceの中から先頭を取得する
{IEx.Evaluator, :handle_eval, 3, [file: 'lib/iex/evaluator.ex', line: 310]}
iex(2)> StacktraceCleaner.current_stacktraces # 現在のstacktrace一覧の取得
[
{IEx.Evaluator, :handle_eval, 3, [file: 'lib/iex/evaluator.ex', line: 310]},
{IEx.Evaluator, :do_eval, 3, [file: 'lib/iex/evaluator.ex', line: 285]},
{IEx.Evaluator, :eval, 3, [file: 'lib/iex/evaluator.ex', line: 274]}
]
iex(3)> try do
...(3)> raise "Oh no!"
...(3)> rescue
...(3)> e in RuntimeError -> __STACKTRACE__ |> StacktraceCleaner.clean |> IO.inspect # tacktracesの中からdeps系のログを排除
...(3)> end
[
{IEx.Evaluator, :handle_eval, 3, [file: 'lib/iex/evaluator.ex', line: 310]},
{IEx.Evaluator, :do_eval, 3, [file: 'lib/iex/evaluator.ex', line: 285]},
{IEx.Evaluator, :eval, 3, [file: 'lib/iex/evaluator.ex', line: 274]},
{IEx.Evaluator, :loop, 1, [file: 'lib/iex/evaluator.ex', line: 169]}
]
iex上での例になってしまったので少しわかりにくいかもですが、挙動としては上記通りです。
tryで囲っている部分や、副作用が強く、汎用的に呼ばれるコードにログとして貼っておくと、debugが少しだけ楽になるはずです。
シンプルなライブラリですので、他に良い使い方が思いついたら是非教えていただきたいです。
実装解説
実装自体はとても単純です。折角なのでご紹介させていただきます。
# @spec, @type, docsを省いたコードです
defmodule StacktraceCleaner do
@noise_paths ["process", "ex_unit", ".erl"]
def current_stacktrace(match_path \\ nil) do
# current_stacktracesをmatch_pathを引き継いで呼び出して、先頭のみ取得して返却
[stacktrace | _] = current_stacktraces(match_path)
stacktrace
end
def current_stacktraces(match_path \\ nil) do
# Process.info(self(), :current_stacktrace)でstacktraceのリストを取得
# cleanをmatch_pathを引き継いで呼び出して、不要箇所を排除しstacktraceのリストを返却
Process.info(self(), :current_stacktrace)
|> elem(1)
|> clean(match_path)
end
def clean(stacktraces, match_path \\ nil) do
# 事前に決めている排除したいリスト(process, ex_unit, .erl) + deps/* に一致する正規表現のリストを作成
deps_app_regexes = create_deps_app_regexes()
cleaned =
stacktraces
|> Enum.reject(fn stacktrace ->
# stacktraceの中からfile pathのみを取得
stacktrace_path = stacktrace |> elem(3) |> Keyword.get(:file) |> to_string()
# deps_app_regexesの中からfile pathに一致したものがあった場合排除
deps_app_regexes |> Enum.find(&(stacktrace_path =~ &1))
end)
if match_path do
# match_pathが指定されていた場合、そのpathの正規表現に一致したstacktraceのみに絞る
cleaned
|> Enum.filter(fn stacktrace ->
stacktrace |> elem(3) |> Keyword.get(:file) |> to_string() =~ Regex.compile!(match_path)
end)
else # そうでなければそのまま返却
cleaned
end
end
defp create_deps_app_regexes do
(Mix.Project.deps_apps() ++ @noise_paths)
|> Enum.filter(&(!is_nil(&1)))
|> Enum.map(fn app ->
app
|> to_string()
|> Regex.compile!()
end)
end
end
※ ちょっとリファクタしたい気もしますがそれはまた今度
current_stacktrace(match_path \ nil)
current_stacktracesの中から先頭を取得するだけです。
current_stacktraces(match_path \ nil)
Process.info(self(), :current_stacktrace)
でstacktracのリストを取得して、cleanを呼び出すことにより、不要箇所が排除されたstacktraceのリストを返却
clean(stacktraces, match_path \ nil)
create_deps_app_regexes
で排除したいfileの条件の正規表現リスト *1 を作成し、それを元にfile pathが *1 に一致する場合は排除。
match_pathの指定がある場合はそれらにfile pathが一致した場合も排除する
(*1 命名しくった。create_noise_path_regexes
にしたい。)
以上。
小さい機能を関数(アプリケーション)として、公開してみると、もしかしたら誰かが予想もしない使い方をしてくれるかもしれません。
hexに関しての勉強にもなりますし、皆様も是非、ちょっとした便利scriptをhexとして公開してみてください。
(改めて見るとリファクタしたくなるのもアウトプットの醍醐味ですね、、、、)
(stacktraceとbacktraceってなにが違うんだろう、、、わかる人教えてください、、、)