Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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もくもく会@朝日ネットでの成果を元に書かれました。

lotz
実用関数型プログラミング言語 Haskell の情報を発信しています
http://lotz84.github.io/
folio-sec
誰もがかんたんに資産運用することができるサービス「フォリオ」を作っているFinTech系スタートアップ
https://corp.folio-sec.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away