HaskellのロガーKatipを試す

モチベ

Haskell の実行時ログ出力を行うライブラリは monad-logger が一番有名っぽい。 これは Yesod 陣営が開発しているから安心感があるし、バックエンドが fast-logger なので速度も信頼できる。 ただ (自分の調べ方が悪いのかもしれないが) ちょっと自分の用途には機能が足りなかった。 具体的には以下の機能:

  • ログにタイムスタンプを付記したい。
  • ロガーに名前をつけたい。
  • ファイルサイズか日付でログローテーションしたい。

Katip という別のロガーライブラリは機能が豊富のようなので今回はそれを試してみる。

(この記事のHaskell環境: lts-6.23)

Katipのおそらく最小の構成

とりあえず動かしてみる。

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}

module Main where

import System.IO (stdout)
import Control.Monad.Trans (lift)

import Katip

main :: IO ()
main = do
  le <- initLogEnv "MyApp" "prod"                      -- (1)
  hs <- mkHandleScribe ColorIfTerminal stdout InfoS V0 -- (2)
  let le' = registerScribe "stdout scribe" hs le       -- (3)
  runKatipContextT le' () "main" $ do                  -- (4)
    $logTM InfoS "ログ出力"                            -- (5)
    $logTM DebugS "これは出力されない"                 -- (6)
    lift $ putStrLn "ふつうの出力"
    return ()
  return ()
  
-- > [2016-10-30 08:00:31][MyApp.main][Info][kaz-mba-lb8o][23644][ThreadId 7][main:Main app/Main.hs:17:5] ログ出力
-- > ふつうの出力
  1. 新しい LogEnv を作る。ルートロガー名と実行環境名を指定している。
  2. 新しい Scribe を作る。 Scribe というのはログの出力先とログレベルなどの設定を組にしたもの。Katip はログレベルとして severity level (InfoとかErrorとか) だけでなくverbosity level (V0とかV1とか) も指定できる。
  3. LogEnvScribe を登録する。
  4. 作成した LogEnv でロガーのモナドを走らせる。
  5. Info レベルでログを出力する。 $ がついているのは TemplateHaskell でソースファイルの行番号などをログに出力するため。
  6. Debug レベルでログを出力する。しかし LogEnv が Info レベルの Scribe しか持っていないので出力されない。

ロガーのモナドの下でログ出力を行うという点以外はかなり普通のロガーという感じだ。 ファイルに出力したいときは stdout の代わりにファイルハンドルを渡せばいいし、 出力先を増やしたいときは Scribe を追加すればいい。

ログローテーション

Katip は組込みのログローテーションの仕組みは持っていないようだが、 Katip の開発元の Soostone 社は Katip と別に rotating-log というライブラリを公開している。 rotating-log の説明文によると、どうやらファイルサイズベースのローテーションを提供しているらしい。

それならログローテーションはこれでやればいいのかな……と思いきや、

This is so that it can be used to log non-textual streams such as binary serialized or compressed content.

とのことなのでテキストログのローテーションで使うものではないらしい。

一応 rotating-log を Scribe にしてテキストログのローテーションを試してみたが (以下のコード) 参考にしない方がいいかもしれない。

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}

module Main where

import System.IO ( hSetBuffering, hIsTerminalDevice, stdout
                 , BufferMode(..)
                 , Handle
                 )
import Control.Monad (when)
import Control.Monad.Trans (lift)
import Data.Monoid ((<>))
import qualified Data.Text.Encoding as T
import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.IO as LT
import qualified Data.Text.Lazy.Builder as LT
import qualified Data.Text.Lazy.Encoding as LT

import Katip
import Katip.Scribes.Handle (formatItem)
import System.RotatingLog ( RotatingLog
                          , mkRotatingLog, archiveFile
                          , rotatedWrite, rotatedClose
                          )

mkRotatingLogScribe :: RotatingLog -> Severity -> Verbosity -> IO Scribe
mkRotatingLogScribe rl sev verb =
  return $ Scribe $ \i -> do
    when (permitItem sev i) $
      rotatedWrite rl $ T.encodeUtf8 $ LT.toStrict $ LT.toLazyText $ (formatItem False verb i <> "\n")

main :: IO ()
main = do
  le <- initLogEnv "MyApp" "prod"
  rl <- mkRotatingLog "log/myapp" 256 LineBuffering (archiveFile "log/archive/")
  s <- mkRotatingLogScribe rl InfoS V0
  let le' = registerScribe "rotating log scribe" s le
  runKatipContextT le' () "main" $ do
    sequence_ $ replicate 10 $ $logTM InfoS "ログ出力"

-- > $ tree log
-- > log
-- > ├── archive
-- > │   ├── myapp_2016_10_30_17_07_51.852205.log
-- > │   ├── myapp_2016_10_30_17_07_51.852508.log
-- > │   ├── myapp_2016_10_30_17_07_51.85274.log
-- > │   └── myapp_2016_10_30_17_07_51.852956.log
-- > └── myapp.log

ちなみに fast-logger はサイズベースのローテーションの仕組みを持っている。 その一方で monad-logger は fast-logger を使って実装されているにも関わらずローテーションのインターフェースを持っていない。 推測だが、monad-logger を開発している Yesod 陣営が Keter を使っているためではないかと思う。 Keter についてはよく分かっていないのだが、Web アプリケーションのデプロイをサポートするものとのことで、 Yesod アプリのデプロイなどに使われているらしい。 ドキュメントによると Keter はログローテーションのAPIを持っているので、 Yesod 陣営にとっては monad-logger にローテーションを実装する必要性がないのではないだろうか。

まとめ

Katip はタイムスタンプや名前空間など、一般的なロガーライブラリが持っているような機能は十分備えている。 ログローテーションの仕組みはないが、これは logrotate コマンドなどを使えばなんとかなるので大きい問題ではないと思う。

なお Katip はこの記事で紹介した以外にも次のような機能がある。