3

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!

Ulrich Schuster
  • 1,670
  • 15
  • 24

2 Answers2

1

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.

danidiaz
  • 26,936
  • 4
  • 45
  • 95
  • From the source code, LogFunc is ```data LogFunc = LogFunc { unLogFunc :: !(CallStack -> LogSource -> LogLevel -> Utf8Builder -> IO ()) , lfOptions :: !(Maybe LogOptions) }``` Hence, it is already defined as a function in the IO monad. To my understanding, it should be possible to define this function beforehand and store it in the environment. – Ulrich Schuster Apr 19 '21 at 17:00
  • 1
    @UlrichSchuster Imagine that you had a function `loggerFromHandle :: Handle -> CallStack -> LogSource -> LogLevel -> Utf8Builder -> IO ()` directly available at the top level, and you wanted to construct a `LogFunc` out of it. You would need to open a file with something like `withFile` in order to obtain a `Handle`, and then partially apply `loggerFromHandle`. But at that point you have already performed some effects! What you *could* have is a `LogFunc` available at the top level which logged everything to `stdout`. Why? Because that `Handle` is already "open" by default, so to speak. – danidiaz Apr 19 '21 at 18:38
  • Got it! The important clue was stdout already being open. This is why logging to the console does actually work without the bracket. Thanks for the insight! – Ulrich Schuster Apr 19 '21 at 19:57
0

I think newLogFunc is what you want.

Hogeyama
  • 748
  • 4
  • 10