8e7b538921
* Simplify code in CurlMock cookbook recipe * Link to latest versions of packages on hackage * Fix grammar in the OpenIdConnect recipe * HasForeignType -> HasForeign
116 lines
5.2 KiB
Text
116 lines
5.2 KiB
Text
# 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
|
||
return ()
|
||
```
|
||
|
||
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.
|