Bending Warp
In the past I've noticed that Warp both writes to stdout
at times and produces
some default HTTP responses, but I've never bothered taking the time to look up
what possibilities it offers to changes this behaviour. I've also always thought
that I ought to find out how Warp handles signals.
If you wonder why this would be interesting to know there are three main points:
- The environments where the services run are set up to handle structured
logging. In our case it should be JSONL written to
stdout
, i.e. one JSON object per line. - We've decided that the error responses we produce in our code should be JSON, so it's irritating to have to document some special cases where this isn't true just because Warp has a few default error responses.
- Signal handling is, IMHO, a very important part of writing a service that runs well in k8s as it uses signals to handle the lifetime of pods.
Looking through the Warp API
Browsing through the API documentation for Warp it wasn't too difficult to find the interesting pieces, and that Warp follows a fairly common pattern in Haskell libraries
- There's a function called
runSettings
that takes an argument of typeSettings
. - The default settings are available in a variable called
defaultSettings
(not very surprising). There are several functions for modifying the settings and they all have the same shape
setX :: X -> Settings -> Settings.
which makes it easy to chain them together.
- The functions I'm interested in now are
setOnException
- the default handler,
defaultOnException
, prints the exception tostdout
using itsShow
instance setOnExceptionResponse
- the default responses are produced by
defaultOnExceptionResponse
and contain plain text response bodies setInstallShutdownHandler
- the default behaviour is to wait for all ongoing requests and then shut done
setGracefulShutdownTimeout
- sets the number of seconds to wait for ongoing requests to finnish, the default is to wait indefinitely
Some experimenting
In order to experiment with these I put together a small API using servant,
app
, with a main
function using runSettings
and stringing together a bunch
of modifications to defaultSettings
.
main :: IO () main = Log.withLogger $ \logger -> do Log.infoIO logger "starting the server" runSettings (mySettings logger defaultSettings) (app logger) Log.infoIO logger "stopped the server" where mySettings logger = myShutdownHandler logger . myOnException logger . myOnExceptionResponse
myOnException
logs JSON objects (using the logging I've written about before,
here and here). It decides wether to log or not using
defaultShouldDisplayException
, something I copied from defaultOnException
.
myOnException :: Log.Logger -> Settings -> Settings myOnException logger = setOnException handler where handler mr e = when (defaultShouldDisplayException e) $ case mr of Nothing -> Log.warnIO logger $ lm $ "exception: " <> T.pack (show e) Just _ -> do Log.warnIO logger $ lm $ "exception with request: " <> T.pack (show e)
myExceptionResponse
responds with JSON objects. It's simpler than
defaultOnExceptionResponse
, but it suffices for my learning.
myOnExceptionResponse :: Settings -> Settings myOnExceptionResponse = setOnExceptionResponse handler where handler _ = responseLBS H.internalServerError500 [(H.hContentType, "application/json; charset=utf-8")] (encode $ object ["error" .= ("Something went wrong" :: String)])
Finally, myShutdownHandler
installs a handler for SIGTERM
that logs and then
shuts down.
myShutdownHandler :: Log.Logger -> Settings -> Settings myShutdownHandler logger = setInstallShutdownHandler shutdownHandler where shutdownAction = Log.infoIO logger "closing down" shutdownHandler closeSocket = void $ installHandler sigTERM (Catch $ shutdownAction >> closeSocket) Nothing
Conclusion
I really ought to have looked into this sooner, especially as it turns out that
Warp offers all the knobs and dials I could wish for to control these aspects of
its behaviour. The next step is to take this and put it to use in one of the
services at $DAYJOB