{-# LANGUAGE RecordWildCards, ScopedTypeVariables #-} {-# LANGUAGE GeneralizedNewtypeDeriving, DeriveGeneric #-} {-# LANGUAGE BangPatterns #-} module Wrecker.Recorder where import Control.Concurrent.STM import Control.Concurrent.STM.TBMQueue import Control.Exception import qualified Network.HTTP.Client as HTTP import qualified Network.HTTP.Types as HTTP import System.Clock import System.Clock.TimeIt data RunResult = Success { resultTime :: !Double , name :: !String } | ErrorStatus { resultTime :: !Double , errorCode :: !Int , name :: !String } | Error { resultTime :: !Double , exception :: !SomeException , name :: !String } | RuntimeError | End deriving (Show) data Event = Event { eRunIndex :: !Int , eResult :: !RunResult } deriving (Show) -- | An opaque type for recording actions for profiling. -- To obtain a 'Recorder' use either 'run', 'defaultMain', 'runOne' or -- 'newStandaloneRecorder'. data Recorder = Recorder { rWithQuery :: !Bool , rRunIndex :: !Int , rQueue :: !(TBMQueue Event) } newtype LogicError = LogicError String instance Show LogicError where show (LogicError err) = err instance Exception LogicError newtype HandledError = HandledError SomeException deriving (Show) instance Exception HandledError -- The bound here should be configurable split :: Recorder -> Recorder split Recorder {..} = Recorder rWithQuery (rRunIndex + 1) rQueue newRecorder :: Bool -> Int -> IO Recorder newRecorder withQuery maxSize = Recorder withQuery 0 <$> newTBMQueueIO maxSize stopRecorder :: Recorder -> IO () stopRecorder = atomically . closeTBMQueue . rQueue addEvent :: Recorder -> RunResult -> IO () addEvent (Recorder _ runIndex queue) runResult = atomically $ writeTBMQueue queue $ Event runIndex runResult readEvent :: Recorder -> IO (Maybe Event) readEvent = atomically . readTBMQueue . rQueue {- | 'record' is a low level function for collecting timing information. Wrap each action of interest in a call to record. > record recorder $ threadDelay 1000000 'record' measures the elapsed time of the call, and catches 'HttpException' in the case of failure. This means failures must be thrown if they are to be properly recorded. -} record :: forall a. Recorder -> String -> IO a -> IO a record recorder key action = do !startTime <- getTime Monotonic handle (recordException startTime) (recordAction startTime) where cleanKey = if rWithQuery recorder then key else takeWhile (/= '?') key -- Remove the query string recordAction :: TimeSpec -> IO a recordAction startTime = do r <- action endTime <- getTime Monotonic let !elapsedTime' = diffSeconds endTime startTime addEvent recorder $ Success {resultTime = elapsedTime', name = cleanKey} return r recordException :: TimeSpec -> SomeException -> IO a recordException startTime ex = do endTime <- getTime Monotonic handleException endTime ex throwIO (HandledError ex) where handleException endTime e | Just ((HTTP.HttpExceptionRequest _ (HTTP.StatusCodeException resp _))) <- fromException e = do let code = HTTP.statusCode $ HTTP.responseStatus resp addEvent recorder $ ErrorStatus {resultTime = diffSeconds endTime startTime, errorCode = code, name = cleanKey} | otherwise = addEvent recorder $ Error { resultTime = diffSeconds endTime startTime , exception = toException e , name = cleanKey }