Phoenixでアクセスログとエラーログを出力する①~Loggerとlogrotate~


今回は、Webアプリケーションでアクセスログ、エラーログを出す設定をしました。
よくある設定だと思うのですがきちんと設定したのは初めてだったので手順をまとめます。

## 要件
- アクセスログとエラーログをそれぞれ別ファイルで保管したい
- 1日1ファイルで日付が変わると新しいファイルにログが書き込まれるようにしたい
- ファイル名はファイルが作られた時の日付
- ログのローテーション(保存期間)は1週間


## LoggerとLoggerFileBackend
`Logger`はコンソールに出力するのみなので、ファイルにログを書き出したい場合は`LoggerFileBackend`とセットで使います。

### Loggerとは
---
- ログツール
- ELixirの標準ライブラリ
- 特別な依存関係を追加することなく利用可能
- 4つのログレベルを提供(:debug、:info、:warn、:error)

#### Loggerの使用例
---
以下のようにコード内で出力したい箇所に記述することでログを出力することができます。

```elixir
require Logger
Logger.debug("This is a debug message.")
Logger.info("This is an info message.")
Logger.warn("This is a warning.")
Logger.error("This is an error message.")

```

### LoggerFileBackendとは
---

- ログメッセージを処理するためのカスタムバックエンドをサポート
- デフォルトではコンソールへの出力が有効
- ファイルへのログ出力や外部ログ収集システムへの送信など、さまざまな出力先に対応可能
- LoggerFileBackendを使用することでファイルにログを書き込むことができる

#### LoggerFileBackendの使用例

- `mix.exs` に依存関係を追加
    ```elixir
        defp deps do
          [
            {:logger_file_backend, "~> 0.0.13"}
          ]
        end

    ```
    
- `config/config.exs` でバックエンドを設定
   ファイルにエラーログとアクセスログを出す設定にします。 
    ```elixir
        # Configures Elixir's Logger
        config :logger,
          backends: [
            :console,
            {LoggerFileBackend, :error_log},
            {LoggerFileBackend, :info_log}
          ]
        
        # :console の設定
        config :logger, :console,
          format: "$time $metadata[$level] $message\n",
          metadata: [:request_id]
        
        # :error_log の設定
        config :logger, :error_log,
          # ログの保存先, ファイル名
          path: "log/error.log",
          # 対象とするレベル
          level: :error,
          # ログフォーマット
          format: "$time $metadata[$level] $message\n",
          # メタデータの要素
          metadata: [:request_id]
        
        # :info_log の設定
        config :logger, :info_log,
          path: "log/info.log", # 実際には動的に変更する必要があります
          level: :info,
          format: "$time $metadata[$level] $message\n",
          metadata: [:request_id]

    ```

- 依存関係をダウンロード
    ```
    $ mix deps.get
    ```

[参考：hexdocs logger](https://hexdocs.pm/logger/1.12.3/Logger.html)
[参考：hexdocs LoggerFileBackend](https://hexdocs.pm/logger_file_backend/readme.html)

### 余談

ログのformat形式を時間から日付に変更したい場合は`$time`から`$date`に変更するとOK
[参考：hexdocs Logger.Formatter ](https://hexdocs.pm/logger/1.12.3/Logger.Formatter.html)

## logrotateの設定
ローカル環境でのログの確認を行いました。サーバー自体はDockerで構築しています。

`config.exs`はサーバー起動時に実行されるため、実行タイミングの時に、Elixirの`TimerX`を読み込んでおらずファイル名を動的に変えることは難しいということがわかりました。
そこで`logrotate`というLinux や UNIX 系システムで使用される、ログファイルの管理を自動化するためのツールを使用。

`logrotate`を使用することでログファイルの自動的なローテーション、圧縮、削除、およびメール送信を行うことができます。

### logrotateの主な機能
- ログファイルのローテーション: 特定のサイズに達したり、特定の期間が経過したりしたログファイルを新しいファイルにローテート（切り替え）
- 圧縮: 古いログファイルを圧縮して、ディスクスペースを節約
- 古いログファイルの削除: 設定した期間より古いログファイルを自動的に削除
- ログファイルのメール送信: ログファイルをメールで送信し、その後ローテー
これらについて必要な機能をピックアップして設定

やりたいことが全部詰まっている...!

### 設定
---
#### `/etc/logrotate.d/`に任意の名前でファイルを作成
---
今回は`/etc/logrotate.d/`に`sample-project`というファイルを作成。

```elixir
sudo touch sample-project
```

#### ファイル内に設定する
---
```elixir
apps/sample-project/log/*.log {
    daily # 毎日実行する
    rotate 7 # 7日間保持
    nocompress # ローテーションしたログを圧縮しない
    missingok # ログファイルが存在しなくてもエラーを出さずに処理を続行する
    notifempty # ログファイルが空ならスキップ。
    copytruncate # ログファイルをコピーし、元ファイルを空にする
    dateext # ローテーションしたログのsuffixに番号をつけるのではなく、日付8桁(-YYYYMMDD) をつける。
    dateformat %Y-%m-%d
    dateyesterday
    create 640 root adm
    olddir /apps/sample-project/log/old

}

```

#### `logrotate`コマンドの実行
---
dockerにログインし、以下のコマンドを実行します
```
sudo logrotate -f /etc/logrotate.d/sample-project
```


ファイルが出来あがります！

<pre>
├── dialyzer.error.log
├── dialyzer.log
├── error.log
├── info.log
└── old
    └── info2024-03-25.log
</pre>

#### (存在しない場合のみ)logrotateインストールの確認
---
`whici logrote`で確認する。

#### インストールされていない場合`logrote`をインストール
---
`logrote`がインストールされていないと設定ファイルを作っても動きません。
以下のファイルでインストールの設定をします。
`/sample-project/apps/docker/elixir_web/Dockerfile`

```elixir
RUN apt-get -y install git vim sudo inotify-tools logrotate cron
```
今回`cron`も合わせてインストールしました。

#### インストールの実行

`docker compose stop`を実行し、`docker compose up -d --build`

うまくいかないかなかったので、`RUN apt upgrade`(OSのアップグレード)を`Dockerfile`に追加して再度実行します。
```elixir
RUN apt upgrade
RUN apt-get update
RUN apt-get -y install git vim sudo inotify-tools logrotate cron
```
---

#### ディレクトリやファイルの存在を確認

上記で`logrotate`と`cron`をインストールすることで以下2つができ上がります。

- `/etc/logrotate.d`
- `/etc/crontab`


### 気になったこと調べました
#### logroteコマンドはどこで実行されている？
---

`etc`の中に以下のようなディレクトリがあります。

```shell
ls | grep cron
```
```shell

anacrontab # anacronによって使用されるジョブの設定を保持するファイル
cron.d # 個別のcronジョブ設定ファイルを配置するディレクトリ。システム管理者はここに特定のアプリケーションやサービス用のcronジョブを設定するファイルを置くことができる
cron.daily # 日次で実行されるスクリプトやコマンドを配置するディレクトリ
cron.hourly # 時間ごとに実行されるスクリプトやコマンドを配置するディレクトリ
cron.monthly #月次で実行されるスクリプトやコマンドを配置するディレクトリ
cron.weekly # 週次で実行されるスクリプトやコマンドを配置するディレクトリ
crontab #  cronジョブを定義するためのファイルや、crontabコマンドによって編集・管理されるファイル。ユーザーやシステムのcronジョブスケジュールを含む
```
`anacron`は、`cron`のようにシステムが稼働している必要がある定時実行スケジューラと異なり、システムが稼動していない時に予定されていたジョブを後で実行できるように設計されています。これは主に、常時稼働しないデスクトップやラップトップのようなシステムで有用です。

`anacrontab`ファイルは、ジョブの実行間隔、ジョブを遅延させる日数、ジョブの識別名、そして実行するコマンドやスクリプトのパスを指定するために使用されます。

日時に関しては`etc/cron.daily/logrotate`に書かれています(基本ここの中は触らない)

```shell
$ cat logrotate
# skip in favour of systemd timer
if [ -d /run/systemd/system ]; then
    exit 0
fi

# this cronjob persists removals (but not purges)
if [ ! -x /usr/sbin/logrotate ]; then
    exit 0
fi

/usr/sbin/logrotate /etc/logrotate.conf
EXITVALUE=$?
if [ $EXITVALUE != 0 ]; then
    /usr/bin/logger -t logrotate "ALERT exited abnormally with [$EXITVALUE]"
fi
exit $EXITVALUE
```
`/etc/logrotate.conf`コマンドを実行し、システムのログファイルのローテーションを行います。

#### 日時、月次の時間帯の指定はどこで？
---

`etc/crontab`で指定しています。

見つかりづらいので`ls | grep cron`で探すと良いです。

```
SHELL=/bin/sh
# You can also override PATH, but by default, newer versions inherit it from the environment
#PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed
17 *    * * *   root    cd / && run-parts --report /etc/cron.hourly
5 7     * * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
59 7    * * 7   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
12 5    1 * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
#

```
つまり日時は午前7時5分にログファイルができると書かれています。


## 基本の整理

### Loggerはマクロを使用している

以下はコードの実行時ではなく、コンパイル時に`Logger`マクロによって評価されます。
これにより、Loggerはアプリケーションの現在のログレベル設定に基づいて、このメッセージを実際にログに記録するかどうかを決定します。

```elixir
Logger.info("Application started")
```

### モジュールの取り込み`use`と`require`と`import`の違い
---
Elixirでのモジュールの取り込み方法にはuse、require、importがあり、それぞれ異なる目的で使用されます。ここで、より簡潔に各用語の違いを説明し、使用例を示します。

### use
---
- 特定のモジュールの機能や振る舞いを現在のモジュールに組み込むために使用
- 指定されたモジュール内の__using__/1マクロを実行し、その結果として関数の定義やマクロの使用が可能に
- フレームワークやDSLの作成時によく使われる

#### require
---
- マクロを使用する場合に必要で、関数やマクロを名前空間付きで呼び出す際に使う
- マクロやコンパイル時に実行される関数を使用する前に、その機能を持つモジュールを明示的にコンパイル時のコンテキストに取り込む必要がある


#### import
---
- 特定のモジュールから関数やマクロを現在のモジュールのコンテキストに直接取り込み、モジュール名なしでアクセスできるようにする
- コードを簡潔に書くために使われ、特定の関数やマクロだけを選択的に取り込むことも可能

#### コード例
---

```elixir
defmodule MyModule do
  defmacro __using__(_) do
    quote do
      def hello do
        IO.puts("Hello, used Module!")
      end
    end
  end

  defmacro my_macro do
    quote do
      IO.puts("Hello, Macro!")
    end
  end

  def my_function do
    IO.puts("Hello, Function!")
  end
end

# useの例
defmodule UsingModule do
  use MyModule

  def test do
    hello()  # MyModuleの__using__/1マクロにより定義された関数
  end
end

# requireの例
defmodule RequiringModule do
  require MyModule

  def test do
    MyModule.my_macro()  # マクロは名前空間付きで呼び出し
  end
end

# importの例
defmodule ImportingModule do
  import MyModule, only: [my_function: 0]  # 特定の関数のみ取り込み

  def test do
    my_function()  # モジュール名なしで関数を直接呼び出し
  end
end

```

`quote`はElixirにおいてAST（抽象構文木）の生成を行う特別な構文です。

### ASTとは
Elixirでは、ソースコードはまずASTに変換され、このASTがさらにコンパイルされて実行可能なコードになります。ElixirのASTは、Elixirのデータ構造（主にタプル、リスト、およびリテラル）を使用して表されます。


[参考：hexdocs Macro](https://hexdocs.pm/elixir/main/Macro.html)
[参考：hexdocs elixir-ast](https://hexdocs.pm/elixir/1.12/syntax-reference.html#the-elixir-ast)
[参考：hexdocs kernel](https://hexdocs.pm/elixir/1.12/Kernel.html#use/2)


### さいごに
---

かなり長くなってしまったので、Unixのコマンド確認までにとどめました。日次でのローテーションの確認や設定はまた次回とします！
誤りなどありましたらご指摘お願いします！
また、thewaggleではElixirでの開発にジョインしてくださる仲間を募集しています！
ご興味ある方、お気軽にご連絡ください。