Merge pull request #987 from marcosh/sentry-cookbook
cookbook sentry integration page
This commit is contained in:
commit
79f8858dfc
6 changed files with 150 additions and 4 deletions
12
.travis.yml
12
.travis.yml
|
@ -66,7 +66,7 @@ install:
|
|||
- rm -fv cabal.project cabal.project.local
|
||||
- "if [ $HCNUMVER -ge 70800 ]; then sed -i.bak 's/-- ghc-options:.*/ghc-options: -j2/' ${HOME}/.cabal/config; fi"
|
||||
- grep -Ev -- '^\s*--' ${HOME}/.cabal/config | grep -Ev '^\s*$'
|
||||
- "printf 'packages: \"servant\" \"servant-client\" \"servant-client-core\" \"servant-docs\" \"servant-foreign\" \"servant-server\" \"doc/tutorial\" \"doc/cookbook/basic-auth\" \"doc/cookbook/curl-mock\" \"doc/cookbook/db-postgres-pool\" \"doc/cookbook/db-sqlite-simple\" \"doc/cookbook/file-upload\" \"doc/cookbook/generic\" \"doc/cookbook/https\" \"doc/cookbook/jwt-and-basic-auth\" \"doc/cookbook/pagination\" \"doc/cookbook/structuring-apis\" \"doc/cookbook/using-custom-monad\" \"doc/cookbook/using-free-client\"\\n' > cabal.project"
|
||||
- "printf 'packages: \"servant\" \"servant-client\" \"servant-client-core\" \"servant-docs\" \"servant-foreign\" \"servant-server\" \"doc/tutorial\" \"doc/cookbook/basic-auth\" \"doc/cookbook/curl-mock\" \"doc/cookbook/db-postgres-pool\" \"doc/cookbook/db-sqlite-simple\" \"doc/cookbook/file-upload\" \"doc/cookbook/generic\" \"doc/cookbook/https\" \"doc/cookbook/jwt-and-basic-auth\" \"doc/cookbook/pagination\" \"doc/cookbook/structuring-apis\" \"doc/cookbook/using-custom-monad\" \"doc/cookbook/using-free-client\" \"doc/cookbook/sentry\"\\n' > 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"
|
||||
- touch cabal.project.local
|
||||
|
@ -130,8 +130,11 @@ install:
|
|||
- if [ -f "doc/cookbook/using-free-client/configure.ac" ]; then
|
||||
(cd "doc/cookbook/using-free-client" && autoreconf -i);
|
||||
fi
|
||||
- if [ -f "doc/cookbook/sentry/configure.ac" ]; then
|
||||
(cd "doc/cookbook/sentry" && autoreconf -i);
|
||||
fi
|
||||
- rm -f cabal.project.freeze
|
||||
- rm -rf .ghc.environment.* "servant"/dist "servant-client"/dist "servant-client-core"/dist "servant-docs"/dist "servant-foreign"/dist "servant-server"/dist "doc/tutorial"/dist "doc/cookbook/curl-mock"/dist "doc/cookbook/basic-auth"/dist "doc/cookbook/curl-mock"/dist "doc/cookbook/db-postgres-pool"/dist "doc/cookbook/db-sqlite-simple"/dist "doc/cookbook/file-upload"/dist "doc/cookbook/generic"/dist "doc/cookbook/https"/dist "doc/cookbook/jwt-and-basic-auth"/dist "doc/cookbook/pagination"/dist "doc/cookbook/structuring-apis"/dist "doc/cookbook/using-custom-monad"/dist "doc/cookbook/using-free-client"/dist
|
||||
- rm -rf .ghc.environment.* "servant"/dist "servant-client"/dist "servant-client-core"/dist "servant-docs"/dist "servant-foreign"/dist "servant-server"/dist "doc/tutorial"/dist "doc/cookbook/curl-mock"/dist "doc/cookbook/basic-auth"/dist "doc/cookbook/curl-mock"/dist "doc/cookbook/db-postgres-pool"/dist "doc/cookbook/db-sqlite-simple"/dist "doc/cookbook/file-upload"/dist "doc/cookbook/generic"/dist "doc/cookbook/https"/dist "doc/cookbook/jwt-and-basic-auth"/dist "doc/cookbook/pagination"/dist "doc/cookbook/structuring-apis"/dist "doc/cookbook/using-custom-monad"/dist "doc/cookbook/using-free-client"/dist "doc/cookbook/sentry"/dist
|
||||
- DISTDIR=$(mktemp -d /tmp/dist-test.XXXX)
|
||||
|
||||
# Here starts the actual work to be performed for the package under test;
|
||||
|
@ -158,12 +161,13 @@ script:
|
|||
- (cd "doc/cookbook/structuring-apis" && cabal sdist)
|
||||
- (cd "doc/cookbook/using-custom-monad" && cabal sdist)
|
||||
- (cd "doc/cookbook/using-free-client" && cabal sdist)
|
||||
- (cd "doc/cookbook/sentry" && cabal sdist)
|
||||
- echo -en 'travis_fold:end:sdist\\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
|
||||
- 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 '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
|
||||
|
|
|
@ -20,6 +20,7 @@ packages: servant/
|
|||
doc/cookbook/structuring-apis
|
||||
doc/cookbook/using-custom-monad
|
||||
doc/cookbook/using-free-client
|
||||
doc/cookbook/sentry
|
||||
|
||||
allow-newer:
|
||||
servant-auth-server:http-types,
|
||||
|
|
|
@ -9,6 +9,7 @@ packages:
|
|||
structuring-apis/
|
||||
https/
|
||||
pagination/
|
||||
sentry/
|
||||
../../servant
|
||||
../../servant-server
|
||||
../../servant-client-core
|
||||
|
|
|
@ -29,3 +29,4 @@ you name it!
|
|||
file-upload/FileUpload.lhs
|
||||
pagination/Pagination.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 " ++ 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.
|
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