diff --git a/.travis.yml b/.travis.yml index 06136b09..c70b97d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -160,10 +160,10 @@ script: - (cd "doc/cookbook/using-free-client" && 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 diff --git a/doc/cookbook/cabal.project b/doc/cookbook/cabal.project index 910db22c..71b97b89 100644 --- a/doc/cookbook/cabal.project +++ b/doc/cookbook/cabal.project @@ -9,6 +9,7 @@ packages: structuring-apis/ https/ pagination/ + sentry/ ../../servant ../../servant-server ../../servant-client-core diff --git a/doc/cookbook/index.rst b/doc/cookbook/index.rst index ac20d758..99d905c7 100644 --- a/doc/cookbook/index.rst +++ b/doc/cookbook/index.rst @@ -29,3 +29,4 @@ you name it! file-upload/FileUpload.lhs pagination/Pagination.lhs curl-mock/CurlMock.lhs + sentry/Sentry.lhs diff --git a/doc/cookbook/sentry/Sentry.lhs b/doc/cookbook/sentry/Sentry.lhs new file mode 100644 index 00000000..6e234f4f --- /dev/null +++ b/doc/cookbook/sentry/Sentry.lhs @@ -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. diff --git a/doc/cookbook/sentry/sentry.cabal b/doc/cookbook/sentry/sentry.cabal new file mode 100644 index 00000000..70a4c97c --- /dev/null +++ b/doc/cookbook/sentry/sentry.cabal @@ -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 \ No newline at end of file