Fortranの標準ライブラリstdlibの紹介（ロガー）

# 概要
[Fortran-lang](https://fortran-lang.org)コミュニティが主導して開発しているFortraの標準ライブラリ[stdlib](https://github.com/fortran-lang/stdlib)について紹介します．紹介する内容は，stdlibの現行バージョン0.1.0に基づいています．

本記事では，stdlibに実装されているロガーを紹介します．

- stdlib_logger

# stdlib_logger
`stdlib_logger`モジュールでは，ロギングに用いる派生型`logger_type`を定義しています．派生型`logger_type`を用いることで，エラー/警告の報告，デバッグ情報の報告などを統一的に実現できます．

`stdlib_logger`モジュールの中では，派生型`logger_type`の型束縛手続きや，ログレベルを表す定数などが定義されています．ロガーを利用する度に`logger_type`型の変数を作るのは煩わしいという意見に応えて，モジュール変数`global_logger`が利用できるようになっています．

`global_logger`は，`global_logger`に追加された装置に情報を出力します．**何も装置が追加されていない場合**は，標準出力`output_unit`に情報を出力します．この挙動が少し判りにくいので，後ほど実例で説明します．

## 使い方
全ての型束縛手続を網羅的に解説するのは骨が折れるので，使い方の中で解説をしていきます．
### メッセージ出力
エラー・警告などの情報を報告するには，`log_debug`等の型束縛手続きを用います．

- `log_debug`<br> debugログを画面に出力する．接頭辞`"DEBUG: "`を付けて，`"タイムスタンプ: DEBUG: メッセージ"`の形式で出力する．
- `log_information`<br> informationログを画面に出力する．接頭辞`"INFO: "`を付けて，`"タイムスタンプ: INFO: メッセージ"`の形式で出力する．
- `log_warning`<br> warningログを画面に出力する．接頭辞`"WARN: "`を付けて`"タイムスタンプ: WARN: メッセージ"`の形式で出力する．
- `log_error`<br> errorログを画面に出力する．接頭辞`"ERROR: "`を付けて`"タイムスタンプ: ERROR: メッセージ"`の形式で出力する．
- `log_message`<br> メッセージを画面に出力する．接頭辞を付けずに`"タイムスタンプ: メッセージ"`の形式で出力する．

装置を追加していないので，標準出力に情報が出力されます．

```Fortran
program main
    use :: stdlib_logger
    implicit none

    call global_logger%log_debug(message="log message debug")
    ! 標準設定では出力されない．

    call global_logger%log_information(message="log message information")
    ! 2021-12-04 10:48:22.886: INFO: log message information

    call global_logger%log_warning(message="log message warning")
    ! 2021-12-04 10:48:22.887: WARN: log message warning

    call global_logger%log_error(message="log message error")
    ! 2021-12-04 10:48:22.888: ERROR: log message error

    call global_logger%log_message(message="log message")
    ! 2021-12-04 10:48:22.888: log message
end program main
```

メッセージ出力には，手続名やモジュール名を出力する引数が存在します．
`message`以外に，モジュール名，手続名を指定する引数`module`，`procedure`も存在しています．引数を指定すると，`"タイムスタンプ: モジュール名 % 手続名: INFO: メッセージ"`の形式で出力されます．

```Fortran
! ロガーを呼び出しているモジュール名と手続名の出力．
call global_logger%log_information("log message information", &
                                    module="ex_stdlib_system", &
                                    procedure="ex_stdlib_logger")
! 2021-12-04 10:48:22.888: ex_stdlib_system % ex_stdlib_logger: INFO: log message information
```

## ログ出力の設定
`logger_type`には，ログ出力を設定する型束縛手続き`configure`が用意されています．

### 出力するメッセージのレベル設定
引数`level`を指定することで，出力されるメッセージのレベルを設定できます．`level`は整数で指定しますが，`stdlib_logger`にはレベルを指定するための定数が定義されています．

数値はあくまで参考情報で覚えておく必要はなく，定数で指定するようにしてください．

- `debug_level = 10`
- `information_level = 20`
- `warning_level = 30`
- `error_level = 40`
- `io_error_level = 40`
- `text_error_level = 50`

標準では`level = information_level`です．下の例では，標準では出力されないdebugログを出力するように設定を変更しています．設定変更前のdebugログ`"log message debug 1"`は出力されず，設定を変更した後のdebugログ`"log message debug 2"`は画面に出力されます．

```Fortran
call global_logger%log_debug(message="log message debug 1")

! debugログを出力するよう設定
call global_logger%configure(level=debug_level)

call global_logger%log_debug(message="log message debug 2")
! 2021-12-04 11:26:53.059: DEBUG: log message debug 2
```

### メッセージの出力幅および改行時のインデントの設定
引数`max_width`に数値を与えることで，出力されるメッセージ幅の上限を整数値で設定できます．`4`より大きいと出力幅として設定され，それより小さいと上限なしと設定されます．これはあくまで上限であって，上限を超えないように適切にスペースで改行されます．
メッセージ幅の上限を解除するには，`0`を指定します．

改行時にメッセージが字下げされます．この字下げの制御には，`indent`オプションを利用します．論理値を引数として取り，`.true.`で改行されたメッセージを字下げし，`.false.`で字下げを抑制します．

```Fortran
! debugログを出力するよう設定
call global_logger%configure(level=debug_level)

! メッセージ幅の上限を40文字に設定
call global_logger%configure(max_width=40)

call global_logger%log_debug(message="log message debug", &
                             module="ex_stdlib_system", &
                             procedure="ex_stdlib_logger")
! 2021-12-04 11:53:24.523:
!     ex_stdlib_system %
!     ex_stdlib_logger: DEBUG: log
!     message debug
! 1234567890123456789012345678901234567890

! メッセージが改行されたときの字下げを無効
call global_logger%configure(indent=.false.)

call global_logger%log_debug(message="log message debug", &
                             module="ex_stdlib_system", &
                             procedure="ex_stdlib_logger")
! 2021-12-04 11:53:24.523:
! ex_stdlib_system % ex_stdlib_logger:
! DEBUG: log message debug
! 1234567890123456789012345678901234567890

! メッセージ幅の上限をなしに設定
call global_logger%configure(max_width=0)
call global_logger%log_debug(message="log message debug", &
                             module="ex_stdlib_system", &
                             procedure="ex_stdlib_logger")
! 2021-12-04 11:44:48.302: ex_stdlib_system % ex_stdlib_logger: DEBUG: log message debug
```

### タイムスタンプの出力の設定
ログメッセージにタイムスタンプを付けるか否かの設定を，`time_stamp`引数で行います．論理値を引数として取り，`.true.`でタイムスタンプを出力し，`.false.`でタイムスタンプの出力を抑制します．

```Fortran
! debugログを出力するよう設定
call global_logger%configure(level=debug_level)

! タイムスタンプを出力しない
call global_logger%configure(time_stamp=.false.)

call global_logger%log_debug(message="long long long long log message debug", &
                             module="ex_stdlib_system", &
                             procedure="ex_stdlib_logger")
! ex_stdlib_system % ex_stdlib_logger: DEBUG: long long long long log message debug
```

### メッセージ間の改行
ログメッセージを出力する際に，改行を入れるか否かを制御します．引数`add_blank_line`に`.true.`を指定すると，メッセージ出力**前**に改行を出力します．

```Fortran
! ログメッセージを出力する前に改行を出力する
call global_logger%configure(add_blank_line=.true.)

! debugログを出力＋メッセージが改行されたときの字下げを無効＋メッセージ幅を30文字に設定
call global_logger%configure(level=debug_level, indent=.false., max_width=30)

call global_logger%log_debug(message="long long long long log message debug", &
                             module="ex_stdlib_system", &
                             procedure="ex_stdlib_logger")
call global_logger%log_debug(message="next log message debug", &
                             module="ex_stdlib_system", &
                             procedure="ex_stdlib_logger")
!
! ex_stdlib_system %
! ex_stdlib_logger: DEBUG: long
! long long long log message
! debug
!
! ex_stdlib_system %
! ex_stdlib_logger: DEBUG: next
! log message debug
```

## ログメッセージを出力する装置の追加と削除
ここまで，標準出力にログメッセージを出力する例を見てきました．`logger_type`型変数に出力先の装置を追加することで，標準出力`output_unit`以外にも出力できます．

出力先を追加する方法は2通りあります．

- `add_log_unit`<br>出力する装置番号を追加する．標準出力やエラー出力，既に開いてあるファイルの装置番号を追加する用途で用いられる．
- `add_log_file`<br>ファイルを開いた上で，その装置番号をログ出力先装置として追加する．`open`文でよく用いられる引数が利用可能．

|引数名|型|用途|
|:--|:--|:--|
|`unit`|`integer, intent(out), optional`|追加されたファイルの装置番号．|
|`action`|`character(*), intent(in), optional`|ファイルに対する処理を指定する．書き込み`"write"`か追記`"readwrite"`かを指定し，標準は`"write"`．|
|`position`|`character(*), intent(in), optional`|開いたファイル内の位置を指定する．ファイル内の位置を変更しない場合は`asis`，ファイルの先頭に移動する場合は`rewind`，ファイル終端に移動する場合は`append`を指定する．標準は`"rewind"`．|
|`status`|`character(*), intent(in), optional`|ファイルが存在しているかを指定する．既に存在している場合は`"old"`，存在しておらず新しく作成する場合は`"new"`，存在しているファイルを新しく置き換える場合は`"replace"`，ファイルの存在が事前に判らないは`"unknown"`を指定する．標準は`"replace"`．|
|`stat`|`integer, intent(out), optional`|ファイルオープンの結果を返す．定数`success`と一致しない場合はファイルオープンに失敗している．|


装置の追加には一つ落とし穴があります．`logger_type`は，**装置を追加していなければ標準出力に出力します**．何も装置を追加していない状態から一つ**装置を追加すると，標準出力には出力されなくなります**．ファイルに出力すると同時に標準出力にも出力するには，明示的に`output_unit`を追加する必要があります．

ロガーに関連付けられている装置の数を出力するには，`log_units_assigned`手続を用います．ロガーから装置を削除するには，`remove_log_unit`を用います．

使用例が少し長くなるので，一つのプログラムを途中で区切りながら説明を加えていきます．

```Fortran
program main
    use, intrinsic :: iso_fortran_env
    use :: stdlib_logger
    use :: stdlib_strings
    implicit none

    integer(int32) :: log_file_unit, stat

    ! debugログを出力するように設定
    call global_logger%configure(level=debug_level)
```

既定のロガー（`global_logger`）にファイルを追加し，ログメッセージをファイル`"log_message.txt"`に出力します．`"log_message.txt"`の装置番号が`log_file_unit`に格納されます．
この手続実行以降，ログメッセージは画面には出力されなくなります．

ロガーへのファイル追加に成功していれば，その装置番号を文字列に変換して出力するようにしています．数字と文字の変換には，`stdlib_strings`で定義されている`to_string`を用いました．

```Fortran
    call global_logger%add_log_file("log_message.txt", unit=log_file_unit, stat=stat)

    if (stat == success) then
        call global_logger%log_information("log file unit ="//to_string(log_file_unit))
    end if
```

このメッセージは画面に出力されず，`"log_message.txt"`に書き込まれます．

```plaintext:log_message.txt
2021-12-04 13:22:36.558: INFO: log file unit =-10
```

いくつかログメッセージを出力してみます．

```Fortran
    call global_logger%log_debug("log message debug")
    call global_logger%log_information("log message information")
    call global_logger%log_warning("log message warning")
    call global_logger%log_error("log message error")
```

これらのメッセージも画面に出力されず，`"log_message.txt"`に書き込まれます．

```plaintext:log_message.txt
2021-12-04 13:22:36.558: INFO: log file unit =-10
2021-12-04 13:22:36.558: DEBUG: log message debug
2021-12-04 13:22:36.558: INFO: log message information
2021-12-04 13:22:36.558: WARN: log message warning
2021-12-04 13:22:36.558: ERROR: log message error
```

ロガーの出力装置に標準出力を追加し，ログファイルと画面の双方にメッセージが出力されるようにします．

```Fortran
    ! ロガーの出力装置に，標準出力を追加．
    call global_logger%add_log_unit(unit=output_unit)

    ! ログメッセージの出力．ファイルと画面の両方に同じメッセージが出力される．
    call global_logger%log_debug("2nd log message debug")
    call global_logger%log_information("2nd log message information")
    call global_logger%log_warning("2nd log message warning")
    call global_logger%log_error("2nd log message error")
    ! 2021-12-04 13:22:36.558: DEBUG: 2nd log message debug
    ! 2021-12-04 13:22:36.558: INFO: 2nd log message information
    ! 2021-12-04 13:22:36.558: WARN: 2nd log message warning
    ! 2021-12-04 13:22:36.558: ERROR: 2nd log message error
```

`"log_message.txt"`の内容も更新されます．

```plaintext:log_message.txt
2021-12-04 13:22:36.558: INFO: log file unit =-10
2021-12-04 13:22:36.558: DEBUG: log message debug
2021-12-04 13:22:36.558: INFO: log message information
2021-12-04 13:22:36.558: WARN: log message warning
2021-12-04 13:22:36.558: ERROR: log message error
2021-12-04 13:22:36.558: DEBUG: 2nd log message debug
2021-12-04 13:22:36.558: INFO: 2nd log message information
2021-12-04 13:22:36.558: WARN: 2nd log message warning
2021-12-04 13:22:36.558: ERROR: 2nd log message error
```

ロガーに登録されている装置の数を出力し，その後ロガーから`"log_message.txt"`を削除してみましょう．

```Fortran
    ! ロガーに登録されている装置数を出力．
    call global_logger%log_debug("number of log units = "//to_string(global_logger%log_units_assigned()))
    ! 2021-12-04 13:22:36.558: DEBUG: number of log units = 2

    ! ロガーからログファイル"log_message.txt"の装置番号を削除．
    call global_logger%remove_log_unit(unit=log_file_unit, close_unit=.true.)

    ! ログメッセージの出力．これは画面にのみ出力される．
    call global_logger%log_information("3rd log message information")
    ! 2021-12-04 13:22:36.559: INFO: 3rd log message information
```

`"log_message.txt"`には，装置数までは出力されていますが，`"3rd log message information"`は出力されていません．

```plaintext:log_message.txt
2021-12-04 13:22:36.558: INFO: log file unit =-10
2021-12-04 13:22:36.558: DEBUG: log message debug
2021-12-04 13:22:36.558: INFO: log message information
2021-12-04 13:22:36.558: WARN: log message warning
2021-12-04 13:22:36.558: ERROR: log message error
2021-12-04 13:22:36.558: DEBUG: 2nd log message debug
2021-12-04 13:22:36.558: INFO: 2nd log message information
2021-12-04 13:22:36.558: WARN: 2nd log message warning
2021-12-04 13:22:36.558: ERROR: 2nd log message error
2021-12-04 13:22:36.558: DEBUG: number of log units = 2
```

既存のログファイルに追記できることを確認するために，先ほど削除したログファイルを再び開き，メッセージを追記します．

`add_log_file`に引数を指定して，`"log_message.txt"`を開いた後，メッセージを出力してファイルを閉じます．このとき，上で標準出力`output_unit`を追加しているので，`add_log_file`の後であっても標準出力にも出力されます．

```Fortran
    ! ロガーにファイルを追加．既存のログファイルに追記するように設定．
    call global_logger%add_log_file("log_message.txt", unit=log_file_unit, &
                                    action="write", position="append", status="old")

    ! ログメッセージの出力．このメッセージは画面に出力され，ログファイル"log_message.txt"に追記される．
    call global_logger%log_information("4th log message information")
    ! 2021-12-04 13:22:36.561: INFO: 4th log message information

    ! ロガーからログファイル"log_message.txt"の装置番号を削除．
    call global_logger%remove_log_unit(unit=log_file_unit, close_unit=.true.)
end program main
```

最終的な出力内容は，下記のようになりました．

```console
2021-12-04 13:22:36.558: DEBUG: 2nd log message debug
2021-12-04 13:22:36.558: INFO: 2nd log message information
2021-12-04 13:22:36.558: WARN: 2nd log message warning
2021-12-04 13:22:36.558: ERROR: 2nd log message error
2021-12-04 13:22:36.558: DEBUG: number of log units = 2
2021-12-04 13:22:36.559: INFO: 3rd log message information
2021-12-04 13:22:36.561: INFO: 4th log message information
```

```plaintext:log_message.txt
2021-12-04 13:22:36.558: INFO: log file unit =-10
2021-12-04 13:22:36.558: DEBUG: log message debug
2021-12-04 13:22:36.558: INFO: log message information
2021-12-04 13:22:36.558: WARN: log message warning
2021-12-04 13:22:36.558: ERROR: log message error
2021-12-04 13:22:36.558: DEBUG: 2nd log message debug
2021-12-04 13:22:36.558: INFO: 2nd log message information
2021-12-04 13:22:36.558: WARN: 2nd log message warning
2021-12-04 13:22:36.558: ERROR: 2nd log message error
2021-12-04 13:22:36.558: DEBUG: number of log units = 2
2021-12-04 13:22:36.561: INFO: 4th log message information
```

## ロガーの設定の取得
ロガーの現在の設定を取得するには，型束縛手続き`configuration`を用います．`configuration`では，`configure`で設定できる項目と，開いている装置番号一覧を取得できます．

|引数名|型|用途|
|:--|:--|:--|
|`add_blank_line`|`logical,intent(out),optional`|ログメッセージを出力する際に，改行を入れる設定になっていれば`.true.`，そうでなければ`.false.`が返る．|
|`indent`|`logical,intent(out),optional`|出力するメッセージが改行された際に，字下げをする設定になっていれば`.true.`，そうでなければ`.false.`が返る．|
|`level`|`integer,intent(out),optional`|出力するメッセージのレベルが返る．|
|`max_width`|`integer,intent(out),optional`|メッセージの出力幅が返る．|
|`time_stamp`|`logical,intent(out),optional`|メッセージにタイプスタンプを付けて出力する設定になっていれば`.true.`，そうでなければ`.false.`が返る．|
|`log_units`|`integer,dimension(:),allocatable,intent(out),optional`|ロガーに関連付けられた装置の番号が返る．|

```Fortran
integer(int32), allocatable :: log_units(:)
integer(int32) :: log_level

! ロガーに関連付けられた装置番号，ログレベルを取得
call global_logger%configuration(level=log_level, log_units=log_units)

if (size(log_units) == 0) call global_logger%add_log_unit(output_unit)
if (log_level >= information_level) call global_logger%configure(level=debug_level)
```

## その他のログ出力手続
`logger_type`には，一般的な用途のログ出力以外に，特定の用途に使うログ出力手続が二つ用意されています．

一つは，IOに関係するエラーログを出力する`log_io_error`で，もう一つはテキスト内の誤り位置を指定する`log_text_error`です．

### log_io_error

`log_io_error`では，接頭辞`"I/O ERROR: "`を付けてメッセージを出力します．また，`open`文の動作結果状態`iostat`やエラーメッセージ`iomsg`を引数で受け付けます．

下の例では，`log_io_error`の使い方を確認するために，存在しないファイルを開いています．

```Fortran
program main
    use, intrinsic :: iso_fortran_env
    use :: stdlib_logger
    implicit none

    integer(int32) :: iostat, io_unit
    character(128) :: iomsg

    ! 存在しないファイルを開く．
    open (newunit=io_unit, file="test.txt", status="old", iostat=iostat, iomsg=iomsg)

    ! ioエラーをログメッセージとして画面に出力．
    call global_logger%log_io_error("file "//"test.txt"//" open failure", iostat=iostat, iomsg=iomsg)
    ! 2021-12-04 13:22:36.561: I/O ERROR: file test.txt open failure
    ! With iostat = 2
    ! With iomsg = "Cannot open file 'test.txt': No such file or directory"
end program main
```

### log_text_error
`log_text_error`は，下記のような書式でテキスト内の誤り位置を指定する手続です．

```
test.txt:0:7

text massage
      ^
Error: spelling mistake
```

テキスト内の誤りを指定するために，他の手続とは異なった引数を取ります．

|引数名|型|用途|
|:--|:--|:--|
|`line`|`character(*),intent(in)`|エラーを含んでいる文字列．|
|`column`|`integer,intent(in)`|エラーが生じている箇所の先頭列番号（1開始）．|
|`summary`|`character(*),intent(in)`|エラーの要約．|
|`filename`|`character(*),intent(in),optional`|`line`が見つかったファイルの名前．|
|`line_number`|`integer,intent(in),optional`|ファイル`filename`内で`line`が存在する行番号．|
|`caret`|`character,intent(in),optional`|エラーが生じている箇所を明示する記号．標準は`"^"`．|
|`stat`|`integer,intent(out),optional`|`log_text_error`の実行結果を返す．定数`success`と一致しない場合は失敗しており，停止コードが返される．|

テキスト内のエラーを自動で検出するわけではないので，比較的手間はかかりますが，非常に判りやすくエラーを指摘できます．

```Fortran
call global_logger%log_text_error(line="text massage", &
                                  column=7, &
                                  summary="spelling mistake", &
                                  filename="test.txt", &
                                  line_number=0, &
                                  caret="^")
! 2021-12-04 14:21:57.161
! test.txt:0:7
!
! text massage
!       ^
! Error: spelling mistake
```

## 複数のロガーの利用
ここまでは全て既定のモジュール変数である`global_logger`を使ってきました．`stdlib_logger`で定義されているロガーは派生型`logger_type`なので，`logger_type`型変数として新たにロガーを設ける事ができます．

`global_logger`以外のロガーを設けることで，ロギングに柔軟性を持たせることができます．例えば，画面には`Information`あるいは`Warning`レベル以上を出力して，ログファイルには`Debug`レベルで出力するといった使い分けができます．

```Fortran
! 新しいロガーを宣言．
type(logger_type) :: logger

! global_loggerの出力レベルをwarning以上に変更．
call global_logger%configure(level=warning_level)
call logger%configure(level=debug_level)

call logger%log_debug("user logger")

! global_logger%log_informationは出力されない．
call global_logger%log_information("global logger")
call logger%log_information("user logger")

call global_logger%log_warning("global logger")
call logger%log_warning("user logger")

call global_logger%log_error("global logger")
call logger%log_error("user logger")

! 2021-12-04 15:40:38.737: DEBUG: user logger
! 2021-12-04 15:40:38.737: INFO: user logger
! 2021-12-04 15:40:38.738: WARN: global logger
! 2021-12-04 15:40:38.738: WARN: user logger
! 2021-12-04 15:40:38.739: ERROR: global logger
! 2021-12-04 15:40:38.739: ERROR: user logger
```