@@ -0,0 +1,116 @@
1+# Error logging with Sentry
2+
3+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.
4+
5+To exemplify this we will need the following imports
6+
7+```haskell
8+{-# LANGUAGE DataKinds #-}
9+{-# LANGUAGE TypeOperators #-}
10+
11+import Control.Exception (Exception,
12+ SomeException, throw)
13+import Data.ByteString.Char8 (unpack)
14+import Network.Wai (Request, rawPathInfo,
15+ requestHeaderHost)
16+import Network.Wai.Handler.Warp (defaultOnException,
17+ defaultSettings,
18+ runSettings,
19+ setOnException,
20+ setPort)
21+import Servant
22+import System.Log.Raven (initRaven, register,
23+ silentFallback)
24+import System.Log.Raven.Transport.HttpConduit (sendRecord)
25+import System.Log.Raven.Types (SentryLevel (Error),
26+ SentryRecord (..))
27+```
28+
29+Just for the sake of the example we will use the following API which will throw an exception
30+
31+```haskell
32+type API = "break" :> Get '[JSON] ()
33+
34+data MyException = MyException deriving (Show)
35+
36+instance Exception MyException
37+
38+server = breakHandler
39+ where breakHandler :: Handler ()
40+ breakHandler = do
41+ throw MyException
42+ return ()
43+```
44+
45+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
46+
47+```haskell
48+main :: IO ()
49+main =
50+ let
51+ settings =
52+ setPort 8080 $
53+ setOnException sentryOnException $
54+ defaultSettings
55+ in
56+ runSettings settings $ serve (Proxy :: Proxy API) server
57+```
58+
59+The definition of the `sentryOnException` function could look as follows
60+
61+```haskell
62+sentryOnException :: Maybe Request -> SomeException -> IO ()
63+sentryOnException mRequest exception = do
64+ sentryService <- initRaven
65+ "https://username:password@senty.host/id"
66+ id
67+ sendRecord
68+ silentFallback
69+ register
70+ sentryService
71+ "myLogger"
72+ Error
73+ (formatMessage mRequest exception)
74+ (recordUpdate mRequest exception)
75+ defaultOnException mRequest exception
76+```
77+
78+It does three things. First it initializes the service which will communicate with Sentry. The parameters it receives are:
79+
80+- the Sentry `DSN`, which is obtained when creating a new project on Sentry
81+- a default way to update sentry fields, where we use the identity function
82+- an event trasport, which generally would be `sendRecord`, an HTTPS capable trasport which uses http-conduit
83+- a fallback handler, which we choose to be `silentFallback` since later we are logging to the console anyway.
84+
85+In the second step it actually sends our message to Sentry with the `register` function. Its arguments are:
86+
87+- the configured Sentry service which we just created
88+- the name of the logger
89+- the error level (see [SentryLevel](https://hackage.haskell.org/package/raven-haskell-0.1.2.0/docs/System-Log-Raven-Types.html#t:SentryLevel) for the possible options)
90+- the message we want to send
91+- an update function to handle the specific `SentryRecord`
92+
93+Eventually it just delegates the error handling to the default warp mechanism.
94+
95+The function `formatMessage` simply uses the request and the exception to return a string with the error message.
96+
97+```haskell
98+formatMessage :: Maybe Request -> SomeException -> String
99+formatMessage Nothing exception = "Exception before request could be parsed: " ++ show exception
100+formatMessage (Just request) exception = "Exception " ++ show exception ++ " while handling request " ++ show request
101+```
102+
103+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`.
104+
105+```haskell
106+recordUpdate :: Maybe Request -> SomeException -> SentryRecord -> SentryRecord
107+recordUpdate Nothing exception record = record
108+recordUpdate (Just request) exception record = record
109+ { srCulprit = Just $ unpack $ rawPathInfo request
110+ , srServerName = fmap unpack $ requestHeaderHost request
111+ }
112+```
113+
114+In this examples we set the raw path as the culprit and we use the `Host` header to populate the server name field.
115+
116+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.