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

-- servant-server
import Servant                  (Proxy (..), Server, serve)

-- warp
import Network.Wai.Handler.Warp (run)

type API = "users" :> Get '[JSON] [USER]

server :: Server API
server = usersHandler

main :: IO ()
main = run 8080 $ serve (Proxy :: Proxy API) server

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

-- warp
import Nework.Wai.Handler.Warp (defaultSettings, runSettings,
                                setPort, setOnException)

main :: IO ()
main = 
    let
        settings =
            setPort 8080 $
            setOnException sentryOnException $
            defaultSettings
    in    
        runSettings settings $ serve (Proxy :: Proxy API) server

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

-- base
import Control.Exception                      (SomeException)

-- wai
import Network.Wai                            (Request)

-- raven-haskell
import System.Log.Raven                       (initRaven, register,
                                               silentFallback)
import System.Log.Raven.Transport.HttpConduit (sendRecord)
import System.Log.Raven.Types                 (SentryLevel (Error))

sentryOnException :: Maybe Request -> SomeException -> IO ()
sentryOnException mRequest exception = do
    sentryService <- initRaven
        "https://username:password@senty.host/port"
        id
        sendRecord
        silentFallback
    register
        sentryService
        "myLogger"
        Error
        (formatMessage mRequest exception)
        (recordUpdate mRequest exception)
    defaultOnException mRequest exception

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

-- base
import Control.Exception      (SomeException)

-- wai
import Network.Wai            (Request, rawPathInfo, requestHeaderHost)

-- raven-haskell
import System.Log.Raven.Types (SentryRecord (..))

-- bytestring
import Data.ByteString.Char8  (unpack)

recordUpdate :: Maybe Request -> SomeException -> SentryRecord -> SentryRecord
recordUpdate Nothing exception record        = record
recordUpdate (Just request) exception record = record
    { srCulprit = Just $ unpack $ rawPathInfo request
    , srServerName = fmap unpack $ requestHeaderHost request
    }

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.