A take on logging
In my previous post I described a type, with instances and a couple of useful
functions for composing log messages. To actually make use of that there's a bit
more needed, i.e. the actual logging. In this post I'll share that part of the
logging setup I've been using in the Haskell services at $DAYJOB
.
The logger type
The logger will be a wrapper around fast-logger's FastLogger
, even though
that's not really visible.
newtype Logger = Logger (LogMsg -> IO ())
It's nature as a wrapper makes it natural to follow the API of fast-logger, with
some calls to liftIO
added.
newLogger :: MonadIO m => m (Logger, m ()) newLogger = liftIO $ do (fastLogger, cleanUp) <- newFastLogger $ LogStdout defaultBufSize pure (Logger (fastLogger . toLogStr @LogMsg), liftIO cleanUp)
The implementation of withLogger
is pretty much a copy of what I found in
fast-logger, just adapted to the newLogger
above.
withLogger :: (MonadMask m, MonadIO m) => (Logger -> m ()) -> m () withLogger go = bracket newLogger snd (go . fst)
Logging functions
All logging functions will follow the same pattern so it's easy to break out the common parts.
logIO :: MonadIO m => Text -> Logger -> LogMsg -> m () logIO lvl (Logger ls) msg = do t <- formatTime defaultTimeLocale "%y-%m-%dT%H:%M:%S%03QZ" <$> liftIO getCurrentTime let bmsg = "" :# [ "correlation-id" .= ("no-correlation-id" :: Text) , "timestamp" .= t , "level" .= lvl ] liftIO $ ls $ bmsg <> msg
With that in place the logging functions become very short and sweet.
debugIO, infoIO, warnIO, errIO, fatalIO :: MonadIO m => Logger -> LogMsg -> m () debugIO = logIO "debug" infoIO = logIO "info" warnIO = logIO "warn" errIO = logIO "error" fatalIO = logIO "fatal"
Simple example of usage
A very simple example showing how it could be used would be something like this
main :: IO () main = withLogger $ \logger -> do debugIO logger "a log line" infoIO logger $ "another log line" #+ ["extras" .= (42 :: Int)]