Using a configuration in Scotty
At work we're only now getting around to put correlation IDs into use. We write most our code in Clojure but since I'd really like to use more Haskell at work I thought I'd dive into Scotty and see how to deal with logging and then especially how to get correlation IDs into the logs.
The types
For configuration it decided to use the reader monad inside ActionT
from
Scotty. Enter Chell:
type ChellM c = ScottyT Text (ReaderT c IO) type ChellActionM c = ActionT Text (ReaderT c IO)
In order to run it I wrote a function corresponding to scotty
:
chell :: c -> Port -> ChellM () -> IO () chell cfg port a = scottyOptsT opts (flip runReaderT cfg) a where opts = def { verbose = 0 , settings = (settings def) { settingsPort = port } }
Correlation ID
To deal with the correlation ID each incoming request should be checked for the
HTTP header X-Correlation-Id
and if present it should be used during logging.
If no such header is present then a new correlation ID should be created. Since
it's per request it feels natural to create a WAI middleware for this.
The easiest way I could come up with was to push the correlation ID into the request's headers before it's passed on:
requestHeaderCorrelationId :: Request -> Maybe ByteString requestHeaderCorrelationId = lookup "X-Correlation-Id" . requestHeaders correlationId :: Middleware correlationId app req sendResponse = do u <- (randomIO :: IO UUID) let corrId = maybe (toASCIIBytes u) id (requestHeaderCorrelationId req) newHeaders = ("X-Correlation-Id", corrId) : (requestHeaders req) app (req { requestHeaders = newHeaders }) $ \ res -> sendResponse res
It also turns out to be useful to have both a default correlation ID and a function for pulling it out of the headers:
defaultCorrelationString :: ByteString defaultCorrelationString = "no-correlation-id" getCorrelationId :: Request -> ByteString getCorrelationId r = maybe defaultCorrelationString id (requestHeaderCorrelationId r)
Getting the correlation ID into the configuration
Since the correlation ID should be picked out of the request on handling of
every request it's useful to have it the configuration when running the
ChellActionM
actions. However, since the correlation ID isn't available when
running the reader (the call to runReaderT
in chell
) something else is
called for. When looking around I found local
(and later I was pointed to the
more general withReaderT
) but it doesn't have a suitable type. After some help
on Twitter I arrived at withConfig
which allows me to run an action in a
modified configuration:
withConfig :: (c -> c') -> ChellActionM c' () -> ChellActionM c () withConfig = mapActionT . withReaderT where mapActionT f (ActionT a) = ActionT $ (mapExceptT . mapReaderT . mapStateT) f a
Making it handy to use
Armed with this I can put together some functions to replace Scotty's get
,
post
, etc. With a configuration type like this:
data Config = Cfg LoggerSet ByteString
The modified get
looks like this (Scotty's original is S.get
)
get :: RoutePattern -> ChellActionM Config () -> ChellM Config () get p a = S.get p $ do r <- request let corrId = getCorrelationId r withConfig (\ (Cfg l _) -> Cfg l corrId) a
With this in place I can use the simpler ReaderT Config IO
for inner functions
that need to log.