Integration using transformers
You might be wondering: how can I integrate my favorite logging library with
mu-grpc-server? Our explanation of services introduced
MonadServer as the simplest set of capabilities required for a server:
- Finish successfully by
- Finish with an error code via
- Executing arbitrary
But you are not tied to that simple set! You can create servers which need more capabilities if you later define how to run those.
One simple example of a capability is having one single piece of information you can access. This is useful to thread configuration data, or if you use a transactional variable as information, as a way to share data between concurrent threads. This is traditionally done using a
Let us extend our
sayHello example with a piece of configuration which states the word to use when greeting:
import Control.Monad.Reader sayHello :: (MonadServer m, MonadReader T.Text m) => HelloRequest -> m HelloResponse sayHello (HelloRequest nm) = do greeting <- ask pure $ HelloResponse (greeting <> ", " <> nm)
Unfortunately, the simple way to run a gRPC application no longer works:
main = runGRpcApp 8080 "helloworld" quickstartServer
Furthermore, how does the server know which is the actual value? In other words, how do we inject the value for
greeting? We need to declare how to handle that capability. This is traditionally done with a
run function; this additional argument is used by
main = runGRpcAppTrans 8080 (flip runReaderT "hi") quickstartServer
There are quite a number of libraries which provide logging support. Let’s begin with
monad-logger. In this case, an additional set of functions is available when you implement the
MonadLogger class. For example, we could log a message every time we say hi:
import Control.Monad.Logger sayHello :: (MonadServer m, MonadLogger m) => HelloRequest -> m HelloResponse sayHello (HelloRequest nm) = do logInfoN "running hi" pure $ HelloResponse ("hi, " <> nm)
The most important addition with respect to the original code is in the signature. Before we only had
MonadServer m, now we have an additional
MonadLogger m there.
As we have done with the Reader example, we need to define how to handle
monad-logger provides three different monad transformers, so you can choose whether your logging will be completely ignored, will become a Haskell value, or would fire some
IO action like printing in the console. Each of these monad transformers comes with a
run action which declares how to handle it; the extended function
runGRpcAppTrans takes that handler as argument.
main = runGRpcAppTrans msgSerializer 8080 runStderrLoggingT quickstartServer
If you prefer other logging library, this is fine with us! Replacing
co-log means asking for a different capability in the server. In this case we have to declare the type of the log items as part of the
import Colog.Monad sayHello :: (MonadServer m, WithLog env String m) => HelloRequest -> m HelloResponse sayHello (HelloRequest nm) = do logInfoN "running hi" pure $ HelloResponse ("hi, " <> nm)
In this case, the top-level handler is called
usingLoggerT. Its definition is slightly more involved because
co-log gives you maximum customization power on your logging, instead of defining a set of predefined logging mechanisms.
main = runGRpcAppTrans msgSerializer 8080 logger quickstartServer where logger = usingLoggerT (LogAction $ liftIO putStrLn)
run function you provide to
runGRpcAppTrans may be called more than once! This is fine for readers and logging, but not for
StateT, for example. In particular, you must ensure that your
run function is idempotent, that is, that the result of calling it more than once is the same as calling it just once.
In the particular case of
StateT, we suggest using a transactional variable, passed as either an argument or using
ReaderT. This has the additional benefit that concurrent access to the variable - which is fairly possible in a gRPC server – are automatically protected for data races and deadlocks.