git.iko.soy/typeable/servant
on master help sign in
cookbook sentry integration page
7a9504eMarco Perone
.travis.yml+2−2
@@ -160,10 +160,10 @@ script:
160 - (cd "doc/cookbook/using-free-client" && cabal sdist)
160 - (cd "doc/cookbook/using-free-client" && cabal sdist)
161 - echo -en 'travis_fold:end:sdist\\r'
161 - echo -en 'travis_fold:end:sdist\\r'
162 - echo Unpacking... && echo -en 'travis_fold:start:unpack\\r'
162 - echo Unpacking... && echo -en 'travis_fold:start:unpack\\r'
163− - mv "servant"/dist/servant-*.tar.gz "servant-client"/dist/servant-client-*.tar.gz "servant-client-core"/dist/servant-client-core-*.tar.gz "servant-docs"/dist/servant-docs-*.tar.gz "servant-foreign"/dist/servant-foreign-*.tar.gz "servant-server"/dist/servant-server-*.tar.gz "doc/tutorial"/dist/tutorial-*.tar.gz "doc/cookbook/basic-auth"/dist/cookbook-basic-auth-*.tar.gz "doc/cookbook/curl-mock"/dist/cookbook-curl-mock-*.tar.gz "doc/cookbook/db-postgres-pool"/dist/cookbook-db-postgres-pool-*.tar.gz "doc/cookbook/db-sqlite-simple"/dist/cookbook-db-sqlite-simple-*.tar.gz "doc/cookbook/file-upload"/dist/cookbook-file-upload-*.tar.gz "doc/cookbook/generic"/dist/cookbook-generic-*.tar.gz "doc/cookbook/https"/dist/cookbook-https-*.tar.gz "doc/cookbook/jwt-and-basic-auth"/dist/cookbook-jwt-and-basic-auth-*.tar.gz "doc/cookbook/pagination"/dist/cookbook-pagination-*.tar.gz "doc/cookbook/structuring-apis"/dist/cookbook-structuring-apis-*.tar.gz "doc/cookbook/using-custom-monad"/dist/cookbook-using-custom-monad-*.tar.gz "doc/cookbook/using-free-client"/dist/cookbook-using-free-client-*.tar.gz ${DISTDIR}/
163+ - mv "servant"/dist/servant-*.tar.gz "servant-client"/dist/servant-client-*.tar.gz "servant-client-core"/dist/servant-client-core-*.tar.gz "servant-docs"/dist/servant-docs-*.tar.gz "servant-foreign"/dist/servant-foreign-*.tar.gz "servant-server"/dist/servant-server-*.tar.gz "doc/tutorial"/dist/tutorial-*.tar.gz "doc/cookbook/basic-auth"/dist/cookbook-basic-auth-*.tar.gz "doc/cookbook/db-postgres-pool"/dist/cookbook-db-postgres-pool-*.tar.gz "doc/cookbook/db-sqlite-simple"/dist/cookbook-db-sqlite-simple-*.tar.gz "doc/cookbook/file-upload"/dist/cookbook-file-upload-*.tar.gz "doc/cookbook/generic"/dist/cookbook-generic-*.tar.gz "doc/cookbook/https"/dist/cookbook-https-*.tar.gz "doc/cookbook/jwt-and-basic-auth"/dist/cookbook-jwt-and-basic-auth-*.tar.gz "doc/cookbook/pagination"/dist/cookbook-pagination-*.tar.gz "doc/cookbook/structuring-apis"/dist/cookbook-structuring-apis-*.tar.gz "doc/cookbook/using-custom-monad"/dist/cookbook-using-custom-monad-*.tar.gz "doc/cookbook/using-free-client"/dist/cookbook-using-free-client-*.tar.gz "doc/cookbook/sentry"/dist/cookbook-sentry-*.tar.gz "doc/cookbook/curl-mock"/dist/cookbook-curl-mock-*.tar.gz ${DISTDIR}/
164 - cd ${DISTDIR} || false
164 - cd ${DISTDIR} || false
165 - find . -maxdepth 1 -name '*.tar.gz' -exec tar -xvf '{}' \;
165 - find . -maxdepth 1 -name '*.tar.gz' -exec tar -xvf '{}' \;
166− - "printf 'packages: servant-*/*.cabal servant-client-*/*.cabal servant-client-core-*/*.cabal servant-docs-*/*.cabal servant-foreign-*/*.cabal servant-server-*/*.cabal tutorial-*/*.cabal cookbook-basic-auth-*/*.cabal cookbook-curl-mock-*/*.cabal cookbook-db-postgres-pool-*/*.cabal cookbook-db-sqlite-simple-*/*.cabal cookbook-file-upload-*/*.cabal cookbook-generic-*/*.cabal cookbook-https-*/*.cabal cookbook-jwt-and-basic-auth-*/*.cabal cookbook-pagination-*/*.cabal cookbook-structuring-apis-*/*.cabal cookbook-using-custom-monad-*/*.cabal cookbook-using-free-client-*/*.cabal\\n' > cabal.project"
166+ - "printf 'packages: servant-*/*.cabal servant-client-*/*.cabal servant-client-core-*/*.cabal servant-docs-*/*.cabal servant-foreign-*/*.cabal servant-server-*/*.cabal tutorial-*/*.cabal cookbook-basic-auth-*/*.cabal cookbook-curl-mock-*/*.cabal cookbook-db-postgres-pool-*/*.cabal cookbook-db-sqlite-simple-*/*.cabal cookbook-file-upload-*/*.cabal cookbook-generic-*/*.cabal cookbook-https-*/*.cabal cookbook-jwt-and-basic-auth-*/*.cabal cookbook-pagination-*/*.cabal cookbook-structuring-apis-*/*.cabal cookbook-using-custom-monad-*/*.cabal cookbook-using-free-client-*/*.cabal cookbook-sentry-*/*.cabal\\n' > cabal.project"
167 - "echo 'constraints: foundation >=0.0.14,memory <0.14.12 || >0.14.12' >> cabal.project"
167 - "echo 'constraints: foundation >=0.0.14,memory <0.14.12 || >0.14.12' >> cabal.project"
168 - "echo 'allow-newer: servant-auth-server:http-types,servant-auth-server:servant-server, servant-pagination:servant,servant-pagination:servant-server' >> cabal.project"
168 - "echo 'allow-newer: servant-auth-server:http-types,servant-auth-server:servant-server, servant-pagination:servant,servant-pagination:servant-server' >> cabal.project"
169 - touch cabal.project.local
169 - touch cabal.project.local
doc/cookbook/cabal.project+1
@@ -9,6 +9,7 @@ packages:
9 structuring-apis/
9 structuring-apis/
10 https/
10 https/
11 pagination/
11 pagination/
12+ sentry/
12 ../../servant
13 ../../servant
13 ../../servant-server
14 ../../servant-server
14 ../../servant-client-core
15 ../../servant-client-core
doc/cookbook/index.rst+1
@@ -29,3 +29,4 @@ you name it!
29 file-upload/FileUpload.lhs
29 file-upload/FileUpload.lhs
30 pagination/Pagination.lhs
30 pagination/Pagination.lhs
31 curl-mock/CurlMock.lhs
31 curl-mock/CurlMock.lhs
32+ sentry/Sentry.lhs
doc/cookbook/sentry/Sentry.lhsnew file+116
@@ -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 " ++ 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.
doc/cookbook/sentry/sentry.cabalnew file+23
@@ -0,0 +1,23 @@
1+name: cookbook-sentry
2+version: 0.1
3+synopsis: Collecting runtime exceptions using Sentry
4+homepage: http://haskell-servant.readthedocs.org/
5+license: BSD3
6+license-file: ../../../servant/LICENSE
7+author: Servant Contributors
8+maintainer: haskell-servant-maintainers@googlegroups.com
9+build-type: Simple
10+cabal-version: >=1.10
11+
12+executable cookbook-sentry
13+ main-is: Sentry.lhs
14+ build-depends: base == 4.*
15+ , bytestring
16+ , markdown-unlit >= 0.4
17+ , raven-haskell >= 0.1.2
18+ , servant-server
19+ , warp >= 3.2
20+ , wai >= 3.2
21+ default-language: Haskell2010
22+ ghc-options: -Wall -pgmL markdown-unlit
23+ build-tool-depends: markdown-unlit:markdown-unlit