2017-03-10 00:52:30 +03:00
-- | We use BangPatterns to force evaluation of git operations to preserve accuracy in measuring system stats (particularly disk read bytes)
2017-03-07 01:19:48 +03:00
{- # LANGUAGE RecordWildCards, BangPatterns, DeriveGeneric, RankNTypes # -}
2017-03-02 21:27:16 +03:00
{- # OPTIONS_GHC - fno - warn - unused - do - bind # -}
2017-02-15 04:42:35 +03:00
module GitmonClient where
2017-03-08 21:30:32 +03:00
import Control.Exception ( throw )
2017-02-15 04:42:35 +03:00
import Data.Aeson
import Data.Aeson.Types
2017-02-24 01:03:26 +03:00
import Data.ByteString.Lazy ( toStrict )
2017-03-03 00:16:38 +03:00
import Data.Text ( pack , unpack , toLower , isInfixOf )
2017-03-01 02:27:33 +03:00
import qualified Data.Yaml as Y
2017-02-24 01:03:26 +03:00
import GHC.Generics
2017-02-22 22:15:58 +03:00
import Git.Libgit2
2017-03-03 00:16:38 +03:00
import Network.Socket hiding ( recv )
import Network.Socket.ByteString ( sendAll , recv )
2017-02-24 01:03:26 +03:00
import Prelude
2017-03-11 02:30:43 +03:00
import Prologue hiding ( toStrict )
2017-02-24 01:01:53 +03:00
import System.Clock
2017-03-01 02:27:33 +03:00
import System.Directory ( getCurrentDirectory )
import System.Environment
2017-03-03 00:30:35 +03:00
import System.Timeout
2017-03-21 04:12:13 +03:00
import Text.Regex
2017-02-15 04:42:35 +03:00
2017-03-08 21:30:32 +03:00
newtype GitmonException = GitmonException String deriving ( Show , Typeable )
instance Exception GitmonException
2017-03-03 00:35:51 +03:00
data ProcIO = ProcIO { read_bytes :: Integer
, write_bytes :: Integer } deriving ( Show , Generic )
2017-02-24 01:03:01 +03:00
instance FromJSON ProcIO
2017-03-03 00:35:51 +03:00
data ProcessData = ProcessUpdateData { gitDir :: String
, program :: String
, realIP :: Maybe String
, repoName :: Maybe String
2017-03-21 04:10:22 +03:00
, repoID :: Maybe Int
, userID :: Maybe Int
2017-03-03 00:35:51 +03:00
, via :: String }
| ProcessScheduleData
| ProcessFinishData { cpu :: Integer
, diskReadBytes :: Integer
, diskWriteBytes :: Integer
, resultCode :: Integer } deriving ( Generic , Show )
2017-02-23 00:34:28 +03:00
2017-03-01 02:46:29 +03:00
instance ToJSON ProcessData where
2017-02-15 04:42:35 +03:00
toJSON = genericToJSON defaultOptions { fieldLabelModifier = camelTo2 '_' }
2017-02-24 01:04:24 +03:00
2017-02-15 04:42:35 +03:00
data GitmonCommand = Update
| Finish
2017-03-01 02:28:05 +03:00
| Schedule deriving ( Generic , Show )
2017-02-15 04:42:35 +03:00
instance ToJSON GitmonCommand where
2017-03-01 02:28:20 +03:00
toJSON = genericToJSON defaultOptions { constructorTagModifier = unpack . toLower . pack }
2017-02-15 04:42:35 +03:00
2017-02-24 01:04:24 +03:00
2017-03-03 00:35:51 +03:00
data GitmonMsg = GitmonMsg { command :: GitmonCommand
, processData :: ProcessData } deriving ( Show )
2017-02-15 04:42:35 +03:00
instance ToJSON GitmonMsg where
2017-03-02 21:22:02 +03:00
toJSON GitmonMsg { .. } = object [ " command " .= command , " data " .= processData ]
2017-02-24 01:01:53 +03:00
2017-03-02 01:06:58 +03:00
2017-03-03 01:05:50 +03:00
type ProcInfo = Either Y . ParseException ( Maybe ProcIO )
2017-03-07 01:19:48 +03:00
newtype SocketFactory = SocketFactory { withSocket :: forall a . ( Socket -> IO a ) -> IO a }
2017-03-07 00:51:38 +03:00
2017-03-07 19:46:44 +03:00
reportGitmon :: String -> ReaderT LgRepo IO a -> ReaderT LgRepo IO a
reportGitmon = reportGitmon' SocketFactory { withSocket = withGitmonSocket }
2017-03-07 01:19:48 +03:00
2017-03-07 19:46:44 +03:00
reportGitmon' :: SocketFactory -> String -> ReaderT LgRepo IO a -> ReaderT LgRepo IO a
reportGitmon' SocketFactory { .. } program gitCommand = do
2017-03-21 04:12:59 +03:00
( gitDir , realIP , repoName , repoID , userID ) <- liftIO loadEnvVars
2017-03-07 00:51:38 +03:00
( startTime , beforeProcIOContents ) <- liftIO collectStats
2017-03-08 21:49:44 +03:00
gitmonStatus <- safeIO . timeout gitmonTimeout . withSocket $ \ s -> do
2017-03-21 04:12:59 +03:00
sendAll s $ processJSON Update ( ProcessUpdateData gitDir program realIP repoName repoID userID " semantic-diff " )
2017-03-07 19:46:44 +03:00
sendAll s $ processJSON Schedule ProcessScheduleData
2017-03-07 00:51:38 +03:00
recv s 1024
2017-03-10 00:52:30 +03:00
-- | We are eagerly evaluating the gitCommand with BangPatterns. This is to preserve accuracy in measuring the process stats calculated, in particular disk read bytes.
2017-03-08 21:49:44 +03:00
! result <- withGitmonStatus ( join gitmonStatus ) gitCommand
2017-03-07 00:51:38 +03:00
( afterTime , afterProcIOContents ) <- liftIO collectStats
let ( cpuTime , diskReadBytes , diskWriteBytes , resultCode ) = procStats startTime afterTime beforeProcIOContents afterProcIOContents
2017-03-08 22:36:44 +03:00
safeIO . timeout gitmonTimeout . withSocket . flip sendAll $ processJSON Finish ( ProcessFinishData cpuTime diskReadBytes diskWriteBytes resultCode )
2017-03-07 00:51:38 +03:00
2017-03-07 19:46:44 +03:00
pure result
2017-03-07 00:51:38 +03:00
where
2017-03-08 21:49:44 +03:00
withGitmonStatus :: Maybe ByteString -> ReaderT LgRepo IO a -> ReaderT LgRepo IO a
withGitmonStatus maybeGitmonStatus gitCommand = case maybeGitmonStatus of
Just gitmonStatus | " fail " ` isInfixOf ` decodeUtf8 gitmonStatus -> throwGitmonException gitmonStatus
_ -> gitCommand
throwGitmonException :: ByteString -> e
2017-03-08 22:10:57 +03:00
throwGitmonException command = throw . GitmonException . unpack $ " Received: ' " <> decodeUtf8 command <> " ' from Gitmon "
2017-03-08 21:49:44 +03:00
2017-03-07 00:51:38 +03:00
collectStats :: IO ( TimeSpec , ProcInfo )
collectStats = do
time <- getTime clock
procIOContents <- Y . decodeFileEither procFileAddr :: IO ProcInfo
pure ( time , procIOContents )
procStats :: TimeSpec -> TimeSpec -> ProcInfo -> ProcInfo -> ( Integer , Integer , Integer , Integer )
procStats beforeTime afterTime beforeProcIOContents afterProcIOContents = ( cpuTime , diskReadBytes , diskWriteBytes , resultCode )
where
2017-03-21 04:13:11 +03:00
cpuTime = toNanoSecs $ afterTime - beforeTime
2017-03-07 00:51:38 +03:00
beforeDiskReadBytes = either ( const 0 ) ( maybe 0 read_bytes ) beforeProcIOContents
afterDiskReadBytes = either ( const 0 ) ( maybe 0 read_bytes ) afterProcIOContents
beforeDiskWriteBytes = either ( const 0 ) ( maybe 0 write_bytes ) beforeProcIOContents
afterDiskWriteBytes = either ( const 0 ) ( maybe 0 write_bytes ) afterProcIOContents
diskReadBytes = afterDiskReadBytes - beforeDiskReadBytes
diskWriteBytes = afterDiskWriteBytes - beforeDiskWriteBytes
resultCode = 0
2017-03-21 04:12:59 +03:00
loadEnvVars :: IO ( String , Maybe String , Maybe String , Maybe Int , Maybe Int )
2017-03-07 00:51:38 +03:00
loadEnvVars = do
2017-03-08 21:19:52 +03:00
pwd <- safeGetCurrentDirectory
2017-03-07 00:51:38 +03:00
gitDir <- fromMaybe pwd <$> lookupEnv " GIT_DIR "
realIP <- lookupEnv " GIT_SOCKSTAT_VAR_real_ip "
repoName <- lookupEnv " GIT_SOCKSTAT_VAR_repo_name "
2017-03-21 04:12:59 +03:00
repoID <- lookupEnv " GIT_SOCKSTAT_VAR_repo_id "
userID <- lookupEnv " GIT_SOCKSTAT_VAR_user_id "
pure ( gitDir , realIP , repoName , readIntFromEnv repoID , readIntFromEnv userID )
2017-03-08 21:19:52 +03:00
where
safeGetCurrentDirectory :: IO String
safeGetCurrentDirectory = getCurrentDirectory ` catch ` handleIOException
handleIOException :: IOException -> IO String
handleIOException _ = pure " "
2017-03-07 00:51:38 +03:00
2017-03-21 04:12:13 +03:00
readIntFromEnv :: Maybe String -> Maybe Int
readIntFromEnv Nothing = Nothing
readIntFromEnv ( Just s ) = Just ( readInt $ subRegex regex s " " )
where regex :: Regex
regex = mkRegexWithOpts " uint: " True False
readInt :: String -> Int
readInt s = read s :: Int
2017-03-07 01:19:48 +03:00
withGitmonSocket :: ( Socket -> IO c ) -> IO c
withGitmonSocket = bracket connectSocket close
2017-03-07 00:51:38 +03:00
where
connectSocket = do
s <- socket AF_UNIX Stream defaultProtocol
2017-03-08 22:35:06 +03:00
timeout gitmonTimeout $ connect s ( SockAddrUnix gitmonSocketAddr )
2017-03-07 00:51:38 +03:00
pure s
2017-03-03 01:05:50 +03:00
2017-03-03 00:35:51 +03:00
-- Timeout in nanoseconds to wait before giving up on Gitmon response to schedule.
gitmonTimeout :: Int
gitmonTimeout = 1 * 1000 * 1000
gitmonSocketAddr :: String
gitmonSocketAddr = " /tmp/gitstats.sock "
procFileAddr :: String
procFileAddr = " /proc/self/io "
clock :: Clock
clock = Realtime
processJSON :: GitmonCommand -> ProcessData -> ByteString
processJSON command processData = ( toStrict . encode $ GitmonMsg command processData ) <> " \ n "
safeIO :: MonadIO m => IO a -> m ( Maybe a )
safeIO command = liftIO $ ( Just <$> command ) ` catch ` noop
noop :: IOException -> IO ( Maybe a )
noop _ = pure Nothing