Rのパッケージ内でloggerパッケージを使う

by
カテゴリ:

Japan.R 2024が迫ってきまいした。私は開発におけるログ出力の意義と分析方法について話すつもりです。採択されなかったらドンマイということで。忘れものしなければ、「Rが生産性を高める 〜データ分析ワークフロー効率化の実践」を絹本するつもりです。欲しかった人はチャンスかもしれません!

さて、本題に入り、パッケージ開発におけるloggerパッケージの使い方を解説します。 Japan.Rの準備していて躓いたので、ここでまとめておきます。

loggerパッケージを使ってログを出す場合、ログレベルやフォーマットの設定は不可欠です。

たとえばログレベルはlogger::log_threshold関数で設定できます。 "INFO"など所望のログレベルを指定するだけで使えるので一見は簡単です。

logger::log_threshold("INFO")

ただし、パッケージで使う場合には、namespace引数に注意が必要です。デフォルトでは"global"が指定されています。もしパッケージ内でglobal namespaceのログレベルを変更してしまうと、他にloggerパッケージを使っている箇所に影響が出ます。

logger::log_threshold |>
  formals() |>
  str()
#> Dotted pair list of 3
#>  $ level    : NULL
#>  $ namespace: chr "global"
#>  $ index    : num 1

たとえば、globalのログレベルをWARNにすると、example1やexample2のログもWARN以上しか表示されなくなります。

# globalのログレベルは全体に影響するため、example1やexample2のINFOログも表示されなくなる
logger::log_threshold("WARN", namespace = "global")
logger::log_info("namespaceを指定していないログ", namespace = "global")
logger::log_info("namespaceを指定したログ", namespace = "example1")
logger::log_info("namespaceを指定したログ", namespace = "example2")

ログ出力時は、同じnamespaceの設定が優先されるため、必要に応じてnamespaceごとに設定するといいでしょう。パッケージ開発時はマストと言えます。

# example namespaceのログはexample namespaceの設定を利用するため、INFOログも表示される
logger::log_threshold("WARN", namespace = "global")
logger::log_info("namespaceを指定していないログ", namespace = "global")
logger::log_threshold("INFO", namespace = "example")
logger::log_info("namespaceを指定したログ", namespace = "example")
#> INFO [2024-12-09 22:13:59] namespaceを指定したログ

ちなみにlog_info関数などのログを出力する関数namespace引数の既定値がNAになっており、実行時にパッケージ名などから自動的判定します。

logger::log_info |>
  formals() |>
  str()
#> Dotted pair list of 5
#>  $ ...      : symbol 
#>  $ namespace: chr NA
#>  $ .logcall : language sys.call()
#>  $ .topcall : language sys.call(-1)
#>  $ .topenv  : language parent.frame()

したがって、明示的にnamespaceを指定すべき関数は設定系の関数に限られます。パッケージ開発であれば、.onLoad関数内で設定してやると、この関数のpkgname引数を流用できます。ログレベルはデフォルトでOFFにしておき、環境変数で設定すると、ユーザーに無用にログを出さないで済むかと思います。

.onLoad <- function(libname, pkgname) {
  logger::log_threshold(Sys.getenv("PROJECT_LOG_LEVEL", "OFF"), pkgname)
  logger::log_formatter(logger::formatter_json, pkgname)
  logger::log_layout(
    logger::layout_json_parser(fields = c("time", "level", "ns", "fn" )),
    pkgname
  )
}

ENJOY!