Logging with class
In two previous posts I've described how I currently compose log messages and how I do the actual logging. This post wraps up this particular topic for now with a couple of typeclasses, a default implementation, and an example showing how I use them.
The typeclasses
First off I want a monad for the logging itself. It's just a collection of
functions taking a LogMsg
and returning unit (in a monad).
class Monad m => LoggerActions m where debug :: LogMsg -> m () info :: LogMsg -> m () warn :: LogMsg -> m () err :: LogMsg -> m () fatal :: LogMsg -> m ()
In order to provide a default implementation I also need a way to extract the logger itself.
class Monad m => HasLogger m where getLogger :: m Logger
Default implementation
Using the two typeclasses above it's now possible to define a type with an
implementation of LoggerActions
that is usable with deriving
via
.
newtype StdLoggerActions m a = MkStdZLA (m a) deriving (Functor, Applicative, Monad, MonadIO, HasLogger)
And its implementattion of LoggerActions
looks like this:
instance (HasLogger m, MonadIO m) => LoggerActions (StdLoggerActions m) where debug msg = getLogger >>= flip debugIO msg info msg = getLogger >>= flip infoIO msg warn msg = getLogger >>= flip warnIO msg err msg = getLogger >>= flip errIO msg fatal msg = getLogger >>= flip fatalIO msg
An example
Using the definitions above is fairly straight forward. First a type the derives
its implementaiton of LoggerActions
from StdLoggerActions
.
newtype EnvT a = EnvT {runEnvT :: ReaderT Logger IO a} deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader Logger) deriving (LoggerActions) via (StdLoggerActions EnvT)
In order for it to work, and compile, it needs an implementation of HasLogger
too.
instance HasLogger EnvT where getLogger = ask
All that's left is a function using a constraint on LoggerActions
(doStuff
)
and a main
function creating a logger, constructing an EnvT
, and then
running doStuff
in it.
doStuff :: LoggerActions m => m () doStuff = do debug "a log line" info $ "another log line" #+ ["extras" .= (42 :: Int)] main :: IO () main = withLogger $ \logger -> runReaderT (runEnvT doStuff) logger