Scalaのロギングライブラリwoofを試してみた

LEGO GroupのOSSであるPure Scala3なロギングライブラリwoofを眺めてみました。
2021.12.20

はじめに

12/16にLEGO Groupの初めてのOSS ライブラリとしてリリースされたScalaのロギングライブラリwoofを眺めてみました。このタイミングでリリースするのすごいな。

woofの特徴

リポジトリでも紹介されていますが、以下が特徴です。

  • Scala 3のライブラリ(コードが完全に3系で記述されている)
  • Cats Effectを使っている
  • マクロを使って呼び出し側の情報を出力する(リフレクションなし)
  • Scalaのコードで設定が可能

使ってみる

基本的な使い方はREADMEに例があるのでそちらを参照してもらうとしてREADMEでさらっと流されているいくつかのAPIを調べてみます。

Output

例で最初に定義されているのはOutputです。ログの実際の出力はOutputに委譲されるようです。

trait Output[F[_]]:
  def output(str: String): F[Unit]
  def outputError(str: String): F[Unit]
end Output

object Output:
  def fromConsole[F[_]: Console]: Output[F] = new Output[F]:
    def output(str: String): F[Unit]      = Console[F].println(str)
    def outputError(str: String): F[Unit] = Console[F].errorln(str)
end Output

コンパニオンオブジェクトにファクトリがあるので試してみます。

//Output by cats effect Console[F]
val outputFromConsole = Output.fromConsole[IO]

def run: cats.effect.IO[Unit] =   for
    given Logger[IO]  <- Logger.makeIoLogger(outputFromConsole)
    _                 <- program.withLogContext("trace-id", "4d334544-6462-43fa-b0b1-12846f871573")
    _                 <- Logger[IO].info("Now the context is gone")
  yield ()

これはErrorレベル以上で標準エラー出力が使われます。

Filter

Filterはログイベント(LogLine)を出力するかどうか決定する述語で、レベルやクラス名でフィルタリングするビルダーがプリセットされています。

type Filter = LogLine => Boolean

object Filter:

  val atLeastLevel: LogLevel => Filter = level => line => line.level >= level
  val exactLevel: LogLevel => Filter   = level => line => line.level == level
  val regexFilter: Regex => Filter     = regex => line => regex.matches(line.info.enclosingClass)
  val nothing: Filter                  = _ => false
  val everything: Filter               = _ => true

  given Monoid[Filter] with
    def empty: Filter                         = nothing
    def combine(f: Filter, g: Filter): Filter = f or g

end Filter

extension (f: Filter)
  infix def and(g: Filter): Filter = line => f(line) && g(line)
  infix def or(g: Filter): Filter  = line => f(line) || g(line)

フィルターの合成を試してみます。

given Filter = Filter.exactLevel(LogLevel.Info) or Filter.exactLevel(LogLevel.Error)
// 上記と同じ; Monoidを使う場合
import org.legogroup.woof.Filter.given
given Filter = Filter.exactLevel(LogLevel.Info) |+| Filter.exactLevel(LogLevel.Error)
2021-12-20 22:19:12 [INFO ] trace-id=4d334544-6462-43fa-b0b1-12846f871573 Main$: HEY! (Main.scala:26)
2021-12-20 22:19:12 [INFO ] Main$: Now the context is gone (Main.scala:20)
2021-12-20 22:19:12 [ERROR] trace-id=4d334544-6462-43fa-b0b1-12846f871573 Main$: I give up (Main.scala:28)

Printer

ログのフォーマットを決めるのがPrinterです。

trait Printer:
  def toPrint(
      epochMillis: EpochMillis,
      level: LogLevel,
      info: LogInfo,
      message: String,
      context: List[(String, String)],
  ): String
end Printer

例で使われているNoColorPrinterの他にColorPrinterがあります。

given Filter = Filter.everything
given Printer = ColorPrinter()

まとめ

woofをざっと眺めてみました。Scala 3で簡潔に書かれていて拡張も簡単にできそうな印象を受けました。これからもpure Scala 3なライブラリが出てくると思うので楽しみです。