Error logging with Sentry¶
In this recipe we will use Sentry to collect the runtime exceptions generated by our application. We will use the raven-haskell package, which is a client for a Sentry event server. Mind that this package is not present on Stackage, so if we are using Stack 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
{-# 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
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
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
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 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.
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 the default SentryRecord
.
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.