エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

ElixirのログをOTP標準装備のハンドラでファイルに出力する覚え書き

数年前に、Elixir の Logger モジュールのカスタムバックエンドを書く、という記事を書きました。

blog.emattsan.org

ごく最近になって。 Elixir Forum で Logger の設定の記事を読み、OTP が標準で用意しているハンドラを使えば、バックエンドを書かなくてもログをファイルに出力できると知りました。 調べてみると、ハンドラは OTP 21.0 で追加された模様。

奇しくも OTP 21.0 のリリース日は、先の記事の公開日と同じ 2018/06/19 だったことに何かの縁を感じたり感じなかったり。

いまだ全貌を掴みきれていないのですが、ログをファイルに出力する最低限の設定を確認したので、忘れないうちに記録しておきます。

道具立て

logger_std_h モジュール

ハンドラには Erlang のドキュメントの次のページで紹介されている logger_std_h モジュールを利用します。

www.erlang.org

設定項目がいくつかあるのですが、ここでは出力先のファイル名を指定する file という項目のみ利用します。

ハンドラを追加する方法

Erlanglogger モジュールの add_handlers/1 関数を利用してハンドラを追加登録します。

www.erlang.org

ドキュメントに "Reads the application configuration parameter logger and calls add_handlers/1 with its contents." とあるように、設定は config ファイルに記述することになります。

設定の記述法

設定はどのように記述するのか。 logger:add_handlers/1 関数をどこで呼び出すのか。 これらは、Elixir の Logger モジュールのドキュメントに記述があります。

hexdocs.pm

Erlang/OTP handlers must be listed under your own application:

config :my_app, :logger, [
  {:handler, :name_of_the_handler, ACustomHandler, configuration = %{}}
]

And then, explicitly attached in your Application.start/2 callback:

:logger.add_handlers(:my_app)

見ての通り、ドキュメントの記述は Application モジュールを利用することを前提としています。 logger:add_handlers/1 関数を適当なタイミングで呼び出せれば Application モジュールを使わなくてもよいようですが、ここではドキュメントにならって Application ありで書いてゆきます。

実際にログをファイルに出力してみる

プロジェクトを用意する

Application モジュールを利用する雛形を生成するため、--sup オプション付きで mix new コマンドを実行します。

$ mix new my_app --sup
$ cd my_app

config/config.exs ファイルを書く

logger_std_h モジュールを利用する設定を config/config.exs に記述します。

コード中 config: %{...} で記述しているオプションは、Erlang のドキュメントにあったオプションの内容です。 Erlang のオプションであるため、ファイル名は Elixir の文字列(バイナリ)ではなく、Erlang の文字列(文字リスト)で指定する必要があります。

import Config

config :my_app, :logger, [
  {
    :handler,
    :my_log,       # ハンドラを識別するための ID
    :logger_std_h, # 利用するハンドラ
    %{
      config: %{
        # 注意! ファイル名は、String (binary) でなく charlist で指定する
        file: 'log/my_log.log'
      }
    }
  }
]

lib/my_app/application.ex にハンドラを追加するコードを書く

logger:add_handlers/1 関数を呼び出すコードを lib/my_app/application.ex に追加します。

defmodule MyApp.Application do
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
    :logger.add_handlers(:my_app) # この行を追加

    children = [
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

ここまで記述できたら IEx を起動します。

$ iex -S mix

ハンドラが追加されていることを確認する

登録されているハンドラは logger:get_handler_ids/0 関数で確認することができます。

iex> :logger.get_handler_ids()
[:my_log, Logger]

config/config.exs に記述したハンドラの ID が表示されると思います。 そして、見ての通り、Elixir 標準の Logger も登録されていることがわかります。

この方法は、起動済みのハンドラを変更することはせず、あくまで追加で登録するためのもののようです。

ログを出力してみる

準備ができたのでログを出力して確認してみます。

iex> require Logger                       
Logger
iex> Logger.info("Hello")                 

19:28:42.693 [info] Hello
:ok

標準のハンドラも有効なので、コンソールにログが出力されました。

ファイルを確認してみます。

iex> File.read!("log/my_log.log") |> IO.puts()
2023-01-30T19:28:37.413517+09:00 info: Application: my_app. Started at: nonode@nohost.
2023-01-30T19:28:42.693326+09:00 info: Hello

:ok

ファイルには、Logger.info/1 で出力する前にアプリケーションが起動した時のログも記録されていました。

ディスク用のハンドラもある

ここでは logger_std_h モジュールを使いましたが、それとは別にディスク出力向けのハンドラ logger_disk_log_h も用意されています。

www.erlang.org

ドキュメントを見てみると、ログローテーションをはじめディスクに出力することを意識したとおぼしきオプションが並んでいます。 こちらのハンドラも折を見て試してみようと思います。

いつか読むはずっと読まない:Science と Fiction のはざま

しまった。 まだ「三体」読んでない。