cookbook sentry integration page
This commit is contained in:
parent
2c70a2b3f5
commit
7a9504e046
5 changed files with 143 additions and 2 deletions
|
@ -160,10 +160,10 @@ script:
|
||||||
- (cd "doc/cookbook/using-free-client" && cabal sdist)
|
- (cd "doc/cookbook/using-free-client" && cabal sdist)
|
||||||
- echo -en 'travis_fold:end:sdist\\r'
|
- echo -en 'travis_fold:end:sdist\\r'
|
||||||
- echo Unpacking... && echo -en 'travis_fold:start:unpack\\r'
|
- echo Unpacking... && echo -en 'travis_fold:start:unpack\\r'
|
||||||
- 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}/
|
- 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}/
|
||||||
- cd ${DISTDIR} || false
|
- cd ${DISTDIR} || false
|
||||||
- find . -maxdepth 1 -name '*.tar.gz' -exec tar -xvf '{}' \;
|
- find . -maxdepth 1 -name '*.tar.gz' -exec tar -xvf '{}' \;
|
||||||
- "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"
|
- "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"
|
||||||
- "echo 'constraints: foundation >=0.0.14,memory <0.14.12 || >0.14.12' >> cabal.project"
|
- "echo 'constraints: foundation >=0.0.14,memory <0.14.12 || >0.14.12' >> cabal.project"
|
||||||
- "echo 'allow-newer: servant-auth-server:http-types,servant-auth-server:servant-server, servant-pagination:servant,servant-pagination:servant-server' >> cabal.project"
|
- "echo 'allow-newer: servant-auth-server:http-types,servant-auth-server:servant-server, servant-pagination:servant,servant-pagination:servant-server' >> cabal.project"
|
||||||
- touch cabal.project.local
|
- touch cabal.project.local
|
||||||
|
|
|
@ -9,6 +9,7 @@ packages:
|
||||||
structuring-apis/
|
structuring-apis/
|
||||||
https/
|
https/
|
||||||
pagination/
|
pagination/
|
||||||
|
sentry/
|
||||||
../../servant
|
../../servant
|
||||||
../../servant-server
|
../../servant-server
|
||||||
../../servant-client-core
|
../../servant-client-core
|
||||||
|
|
|
@ -29,3 +29,4 @@ you name it!
|
||||||
file-upload/FileUpload.lhs
|
file-upload/FileUpload.lhs
|
||||||
pagination/Pagination.lhs
|
pagination/Pagination.lhs
|
||||||
curl-mock/CurlMock.lhs
|
curl-mock/CurlMock.lhs
|
||||||
|
sentry/Sentry.lhs
|
||||||
|
|
116
doc/cookbook/sentry/Sentry.lhs
Normal file
116
doc/cookbook/sentry/Sentry.lhs
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
# 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 trasport, which generally would be `sendRecord`, an HTTPS capable trasport 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-0.1.2.0/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 " ++ 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.
|
23
doc/cookbook/sentry/sentry.cabal
Normal file
23
doc/cookbook/sentry/sentry.cabal
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
name: cookbook-sentry
|
||||||
|
version: 0.1
|
||||||
|
synopsis: Collecting runtime exceptions using Sentry
|
||||||
|
homepage: http://haskell-servant.readthedocs.org/
|
||||||
|
license: BSD3
|
||||||
|
license-file: ../../../servant/LICENSE
|
||||||
|
author: Servant Contributors
|
||||||
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
|
|
||||||
|
executable cookbook-sentry
|
||||||
|
main-is: Sentry.lhs
|
||||||
|
build-depends: base == 4.*
|
||||||
|
, bytestring
|
||||||
|
, markdown-unlit >= 0.4
|
||||||
|
, raven-haskell >= 0.1.2
|
||||||
|
, servant-server
|
||||||
|
, warp >= 3.2
|
||||||
|
, wai >= 3.2
|
||||||
|
default-language: Haskell2010
|
||||||
|
ghc-options: -Wall -pgmL markdown-unlit
|
||||||
|
build-tool-depends: markdown-unlit:markdown-unlit
|
Loading…
Reference in a new issue