Search code examples
haskellloggingservantrio

How to define a simple RIO LogFunc without bracketing execution


I'm trying to set up logging in a RIO application; yet, I don't seem to understand the logging interface.

RIO documentation encourages to define a logger and run the application as follows:

withLogFunc logOptions $ \lf -> do
  let env = Env -- application specific environment
        { appLogFunc = lf
        , appOtherStuff = ...
        }
  runRIO env $ do
    logInfo "Starting app"
    myApp ...

That is, withLogFunc brackets execution of the actual application - why?

I want to hoist my RIO monad into Servant, so this bracketing approach gets in the way. What I'd like to do is to define an environment for use with RIO, say:

data Env = Env
  { config :: ...
  , logger :: !LogFunc
  }

make the environment an instance of the HasLogFunc typeclass:

instance HasLogFunc Env where
  logFuncL = lens logger (\x y -> x { logger = y })

and then create a value of the Env type before passing it to runRIO, instead of passing the entire application execution as a function parameter to withLogFunc. That is, I would like something along the lines

let env = Env {
   config = ...
   logger = mkLogFunc ...
}
in runRIO env $ do 
   logInfo "Starting app"
   ...

However, I do not understand how to create a LogFunc as part of the environment separately. In this way, I could hoist runRIO into a Servant server (server :: S.ServerT UserApi (RIO Env)) and then execute the latter. It appears that the RIO logging interface discourages this. Why? What am I missing?

Thanks for any insights!


Solution

  • The creation of a LogFunc (other than trivial ones which discard all messages) will require performing effects for the initial setup. Effects like opening the log file, or allocating some other resource. This, in addition to the effects of logging each particular message.

    That's why LogFunc-creating functions will either have bracket-like shapes, like withLogFunc does, or else return the log function inside a monad, like newLogFunc does.

    I think the solution is simply to pull the withLogFunc outward, so that it also wraps the point in the code at which you create the Servant server. That way, you'll have the LogFunc at hand when you need it, and you'll be able to construct the environment and "hoist" runRIO.