Edited at

Servant + monad-logger でログを吐いてみる

ServantはHaskellのWebフレームワークです。型でAPIを記述するのが特徴でAPIの型さえ書いてしまえばサーバーの実装は最小限の関数を書くだけで良くなります。(参考: 【型レベルWeb DSL】 Servantの紹介)

アプリケーションを作る時にログを吐くようにしておくことは大事です。monad-loggerはDebugやWarnなどのログレベルに応じて出力を制御できたりTemplate Haskellを使ってログが出力されているコードの位置を出力したり出来てとても便利です。

Servant 0.12 以降を対象としています。0.11 以前のバージョンについては以前の版を参照してください。

今回はServantとmonad-loggerを組み合わせて使う方法を紹介します。Servantはサーバーを実装するときにHandler a(ExceptT ServantErr IO aのnewtype)の値を返すように作る必要がありますがこれだとLoggingTが入っていないのでそのままではログを吐くことが出来ません。そのためにまずは別のモナドトランスフォーマーで実装した後にモナド準同型と呼ばれる自然変換を利用して型を合わせるという技を使います。文字だけだとわけがわからないと思うので順を追って説明していきます。

今回作るWebサーバーは以下の様なものです

type API = Capture "anypath" Text :> Get '[PlainText] Text

/:anypath にアクセスすると何かのplain textが返って来ます。この時アクセスされた:anypathを標準出力にログとして出していくことにします。

loggingServer :: ServerT API (LoggingT Handler)

loggingServer anypath = do
$(logInfo) anypath
pure "success"

これがサーバーの実装です。$(logInfo) anypathでログを吐き、pure "success""success"という文字列を返しています。大事なのは型です。通常はServer API(ServerT API Handlerのエイリアス)のような型を使うと思いますが今回はLoggingTを使いたかったのでServerT Api (LoggingT Handler)のようにしています。これでdo構文の中はLoggingT Handler aと思って実装すれば良くなります。

実際にサーバーを動かすためにはServer APIという型が必要ですが手元にはまだServerT API (LoggingT Handler)しかありません。そこで自然変換を用います。自然変換は圏論に出てくる概念で関手(Functor)から関手への射になります(参考: 自然変換)。これをHaskellで表すとforall a. f a -> g aとなります。

ServantではhoistServer :: HasServer api '[] => Proxy api -> (forall x. m x -> n x) -> ServerT api m -> ServerT api nが用意されています。この第2引数が自然変換になっているのでここに適切な関数を渡せばよいことになります。例えばrunStdoutLoggingT :: MonadIO m => LoggingT m a -> m aがちょうどその型に一致します。 :clap:

main :: IO ()

main = do
putStrLn "Listening on port 8080"
let server = hoistServer api runStdoutLoggingT loggingServer
Warp.run 8080 $ serve api server

これがmain関数になります。

実際に動かしてみましょう!

スクリーンショット 2016-02-21 14.37.07.png

Google Chrome が favicon.ico を読みに行ってるとこまでバッチリわかりますね!

以下に今回作ったコードの全体を載せておきます。


app/Main.hs

{-# LANGUAGE DataKinds #-}

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

module Main where

import Control.Monad.Logger
import Data.Text (Text)
import qualified Network.Wai.Handler.Warp as Warp
import Servant

type API = Capture "anypath" Text :> Get '[PlainText] Text

api :: Proxy API
api = Proxy

loggingServer :: ServerT API (LoggingT Handler)
loggingServer anypath = do
$(logInfo) anypath
pure "success"

main :: IO ()
main = do
putStrLn "Listening on port 8080"
let server = hoistServer api runStdoutLoggingT loggingServer
Warp.run 8080 $ serve api server



servant-logger-example.cabal

name:                servant-logger-example

version: 0.1.0.0
build-type: Simple
cabal-version: >=1.10

executable app
hs-source-dirs: app
main-is: Main.hs
ghc-options: -threaded -rtsopts -with-rtsopts=-N
build-depends: base
, text
, monad-logger
, warp
, servant-server >= 0.12
default-language: Haskell2010


ログを取るためにモナド準同型や自然変換といった概念が登場してきましたがモナドを乗り越えた人なら分かるとおり数学はプログラマの味方です。使いこなせればよりシンプルに強力なコードを書けるようになるでしょう。気になる人はmmorphというライブラリもチェックしてみて下さい!

この記事は第29回Haskellもくもく会@朝日ネットでの成果を元に書かれました。