# Error logging with Sentry

In this recipe we will use [Sentry](https://sentry.io) to collect the runtime exceptions generated by our application. We will use the [raven-haskell](https://hackage.haskell.org/package/raven-haskell) package, which is a client for a Sentry event server. Mind that this package is not present on [Stackage](https://www.stackage.org/), so if we are using [Stack](https://docs.haskellstack.org) we’ll need to add it to our `extra-deps` section in the `stack.yaml` file.

To exemplify this we will need the following imports

```haskell
{-# LANGUAGE DataKinds     #-}
{-# LANGUAGE TypeOperators #-}

import           Control.Exception                      (Exception,
                                                         SomeException, throw)
import           Data.ByteString.Char8                  (unpack)
import           Network.Wai                            (Request, rawPathInfo,
                                                         requestHeaderHost)
import           Network.Wai.Handler.Warp               (defaultOnException,
                                                         defaultSettings,
                                                         runSettings,
                                                         setOnException,
                                                         setPort)
import           Servant
import           System.Log.Raven                       (initRaven, register,
                                                         silentFallback)
import           System.Log.Raven.Transport.HttpConduit (sendRecord)
import           System.Log.Raven.Types                 (SentryLevel (Error),
                                                         SentryRecord (..))
```

Just for the sake of the example we will use the following API which will throw an exception

```haskell
type API = "break" :> Get '[JSON] ()

data MyException = MyException deriving (Show)

instance Exception MyException

server = breakHandler
  where breakHandler :: Handler ()
        breakHandler = do
          throw MyException
          pure ()
```

First thing we need to do if we want to intercept and log this exception, we need to look in the section of our code where we run the `warp` application, and instead of using the simple `run` function from `warp`, we use the `runSettings` functions which allows to customise the handling of requests

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

The definition of the `sentryOnException` function could look as follows

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

It does three things. First it initializes the service which will communicate with Sentry. The parameters it receives are:

- the Sentry `DSN`, which is obtained when creating a new project on Sentry
- a default way to update sentry fields, where we use the identity function
- an event transport, which generally would be `sendRecord`, an HTTPS capable transport 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 it actually sends 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](https://hackage.haskell.org/package/raven-haskell/docs/System-Log-Raven-Types.html#t:SentryLevel) for the possible options)
- the message we want to send
- an update function to handle the specific `SentryRecord`

Eventually it just delegates the error handling to the default warp mechanism.

The function `formatMessage` simply uses the request and the exception to return a string with the error message.

```haskell
formatMessage :: Maybe Request -> SomeException -> String
formatMessage Nothing exception        = "Exception before request could be parsed: " ++ show exception
formatMessage (Just request) exception = "Exception " ++ show exception ++ " while handling request " ++ show request
```

The only piece left now is the `recordUpdate` function which allows to decorate with other [attributes](https://docs.sentry.io/clientdev/attributes/) the default `SentryRecord`.

```haskell
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
  }
```

In this examples we set the raw path as the culprit and we use the `Host` header to populate the server name field.

You can try to run this code using the `cookbook-sentry` executable. You should obtain a `MyException` error in the console and, if you provided a valid Sentry DSN, you should also find your error in the Sentry interface.
