wai-middleware-rollbar-0.1.0.0: src/Network/Wai/Middleware/Rollbar.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-|
Module : Network.Wai.Middleware.Rollbar
Description : WAI middleware for interfacing with Rollbar
Copyright : (c) Hardy Jones, 2017
License : BSD3
Maintainer : jones3.hardy@gmail.com
Stability : experimental
Provides middleware for communicating with Rollbar.
Currently has middleware for sending all server errors to Rollbar.
More to come shortly.
-}
module Network.Wai.Middleware.Rollbar (requests) where
import Control.Concurrent (forkIO)
import Control.Exception (Handler(Handler), catches, displayException)
import Control.Monad (when)
import Data.Aeson (ToJSON, defaultOptions, genericToEncoding, toEncoding)
import Data.Functor (void)
import Data.Maybe (fromMaybe)
import Data.Time (getCurrentTime)
import Data.UUID.V4 (nextRandom)
import GHC.Generics (Generic)
import Network.HostName (getHostName)
import Network.HTTP.Client (HttpException)
import Network.HTTP.Simple
( JSONException
, Request
, defaultRequest
, httpNoBody
, setRequestBodyJSON
, setRequestHost
, setRequestIgnoreStatus
, setRequestMethod
, setRequestPath
, setRequestPort
, setRequestSecure
)
import Network.HTTP.Types.Status
(Status(Status), statusCode, statusIsServerError, statusMessage)
import Network.Wai (Middleware, ResponseReceived)
import System.Environment (getExecutablePath)
import System.IO (hPutStrLn, stderr)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Network.Wai as NW
import qualified Rollbar.Item as RI
-- | Middleware that watches responses
-- and sends an item to Rollbar if it is a server error (5xx).
--
-- Sends additional metadata including the request information.
requests :: RI.AccessToken -> RI.Environment -> Middleware
requests accessToken environment app req handler' = app req handler
where
handler :: NW.Response -> IO ResponseReceived
handler res = do
_ <- forkIO $ handle500 accessToken environment req res
handler' res
handle500 :: RI.AccessToken -> RI.Environment -> NW.Request -> NW.Response -> IO ()
handle500 accessToken environment req res =
when (statusIsServerError $ NW.responseStatus res) (send accessToken environment req res)
`catches`
[ Handler handleHttpException
, Handler handleJSONException
]
send :: RI.AccessToken -> RI.Environment -> NW.Request -> NW.Response -> IO ()
send accessToken environment req res = do
uuid <- Just . RI.UUID4 <$> nextRandom
timestamp <- Just <$> getCurrentTime
host <- Just <$> getHostName
root <- Just . RI.Root . T.pack <$> getExecutablePath
let request = Just RI.Request {..}
let server = Just RI.Server { RI.branch = Nothing, RI.serverCodeVersion = Nothing, .. }
let itemData = (RI.error environment messageBody payload)
{ RI.request, RI.server, RI.timestamp, RI.uuid }
let rReq = rollbarRequest RI.Item{..}
void $ httpNoBody rReq
where
Status{..} = NW.responseStatus res
headers = RI.Headers $ NW.requestHeaders req
messageBody = RI.MessageBody <$> myDecodeUtf8 statusMessage
rawBody = ""
get = RI.Get $ NW.queryString req
method = RI.Method $ NW.requestMethod req
queryString = RI.QueryString $ NW.rawQueryString req
url = RI.URL (NW.requestHeaderHost req, NW.pathInfo req)
userIP = RI.IP $ NW.remoteHost req
referer = myDecodeUtf8 =<< NW.requestHeaderReferer req
range = myDecodeUtf8 =<< NW.requestHeaderRange req
userAgent = myDecodeUtf8 =<< NW.requestHeaderUserAgent req
payload = Payload
{ statusMessage = myDecodeUtf8' statusMessage
, ..
}
myDecodeUtf8 = either (const Nothing) Just . TE.decodeUtf8'
myDecodeUtf8' = fromMaybe "" . myDecodeUtf8
handleHttpException :: HttpException -> IO ()
handleHttpException e = do
hPutStrLn stderr "Ran into an exception while sending a request to Rollbar:"
hPutStrLn stderr $ displayException e
handleJSONException :: JSONException -> IO ()
handleJSONException e = do
hPutStrLn stderr "Ran into an exception while parsing JSON response from Rollbar:"
hPutStrLn stderr $ displayException e
rollbarRequest :: ToJSON a => RI.Item a -> Network.HTTP.Simple.Request
rollbarRequest payload =
setRequestMethod "POST"
. setRequestSecure True
. setRequestHost "api.rollbar.com"
. setRequestPort 443
. setRequestPath "api/1/item/"
. setRequestBodyJSON payload
. setRequestIgnoreStatus
$ defaultRequest
data Payload
= Payload
{ statusCode :: Int
, statusMessage :: T.Text
, userAgent :: Maybe T.Text
, range :: Maybe T.Text
, referer :: Maybe T.Text
}
deriving (Generic, Show)
instance ToJSON Payload where
toEncoding = genericToEncoding defaultOptions