Servant ♥ Sentry
It’s quite a long time I’m not posting something on this blog, so it’s time to release some new content!
Today I had time to work on something which is not really difficult but which I think is still worth sharing, so I decided to wrote this post. Other than giving you the possibility to read this post, I wrote it mainly for three reasons:
- fix and refine the concepts involved
- be able to find quickly these things if I’ll need them in the future
- hope that somebody tells me that there’s a better way ☺
SERVANT AND SENTRY
In the last months I had the possibility to work on some Haskell projects and it has been really fun and instructive. In particular I am using Servant, a fantastic web framework which allows the developer to describe a web API using a type level DSL. As almost every other Haskell web framework it is based on wai, a de-facto standard web application interface, and warp, a lightweight web server for wai applications.
What I wanted to do today was integrate Sentry in my projects. Sentry is an open-source error tracking platform that we are constantly using here in MVLabs to monitor the health of our applications.
A COMMON SERVANT
If you follow the tutorial on the Servant website, you will obtain something that resembles the following
Let me try to explain some things, so it could be clearer what is happening.
The type API
is using the type-level DSL defined by Servant to define the endpoints of the web application. For example, in this example, we have a single /users
endpoint which will respond to GET
requests with an array of users in Json format. The beauty of using this DSL is that Servant is able to generate automatically client code which calls the endpoints, or the Swagger documentation of the API.
The server
function instead is just the implementation of the endpoints defined in the API
. Generally this function will be composed of many pieces, one for every endpoint.
Once we have defined the type signature and the implementation of our API, we need to expose it on a port using a web server. We do this in two steps. First, using the Servant serve
function, we convert our server
into an Application
compatible with wai
. The Proxy
that is used there is just some type level magic which is needed by Servant to pass the API
definition at the type level. The second step is just using the run
function imported from warp
to expose an Application
on the provided port.
ERROR HANDLING
Now, how can we intercept all the exceptions generated by our web application?
The run
function that sets in motion all the application lives in the IO monad, where runtime exceptions can happen (Haskell generally really helps in avoiding runtime exceptions, but sometimes they are inevitable: a required file is missing, an external system in currently down, …). We need to be able to intercept these exceptions, log them and then let them free again to follow their course.
If we look at the documentation of warp
, we will find the setOnException function which does exactly what we need. To use we need to slightly modify the code we had above
What remains to be done now is to define the sentryOnException
function, which will have the signature Maybe Request -> SomeException -> IO ()
. This means that we will have access to the request, if it is present (the request could be missing if an exception is thrown before the request parsing is complete), and the actual exception; SomeException
is a generic exception type in which all the exceptions will be wrapped.
SENTRY INTEGRATION
Let’s see the code and comment it afterwards
Here we do three things. First we initialize the service which will communicate with Sentry. The parameters it receives are:
- the Sentry
DSN
, which you obtain when creating a new project on sentry - a default way to update sentry fields, where we use the identity function
- an event trasport, which generally would be
sendRecord
, an HTTPS capable trasport which uses http-conduit - a fallback handler, which we choose to be
silentFallback
since later we are logging to the console anyway.
In the second step we actually send our message to Sentry with the register
function. Its arguments are:
- the configured Sentry service which we just created
- the name of the logger
- the error level (see SentryLevel for the possible options)
- the message we want to send
- an update function to handle the specific
SentryRecord
Eventually we just delegate the error handling to the default warp
mechanism.
We left out the implementations of formatMessage
and recordUpdate
. I’m not going to delve into the first one, which is just a function which uses the request and the exception to return a string message. The second is actually more interesting, so let’s have a look at it
This function allows us to embellish the skinny SentryRecord
record with some more metadata retrieved from the request
. For example here I’m setting the raw path of the request as culprit of the exception; the server name is retrieved from the Host
header of the request. There are many more fields of the SentryRecord
, corresponding to the several attributes of a Sentry event, and you could update them as needed to obtain nice looking errors in the Sentry web interface.
CONCLUSION
This was not really hard, wasn’t it? Still I think it could be helpful to have it written down so that next time I (or anybody else) need to integrate Sentry, I could just copy/paste from here.
Two notes to conclude. First I’d like to notice that this error handling solution is in no way strictly related to Servant
, but could actually be used in any web application based on wai
and warp
. Second, I need to mention that the raven-haskell
package I’m using is not present on Stackage
, so you’ll need to add it to your extra-deps
section in the stack.yaml
file.