{-# LANGUAGE CPP, FlexibleContexts #-} ----------------------------------------------------------------------------- -- Copyright 2019, Ideas project team. This file is distributed under the -- terms of the Apache License 2.0. For more information, see the files -- "LICENSE.txt" and "NOTICE.txt", which are included in the distribution. ----------------------------------------------------------------------------- -- | -- Maintainer : bastiaan.heeren@ou.nl -- Stability : provisional -- Portability : portable (depends on ghc) -- -- Facilities to create a log database -- ----------------------------------------------------------------------------- module Ideas.Encoding.Logging ( Record(..), addRequest, addState , LogRef, makeLogRef, defaultLogRef, enableLogging, disableLogging , changeLog, logEnabled, logRecord, logRecordWith, printLog , selectFrom , getRecord , getFilePath ) where import Control.Monad import Data.Char import Data.IORef import Data.Maybe import Data.Semigroup (Semigroup(..)) import Data.Time import Ideas.Encoding.Request (Request, Schema(..)) import Ideas.Service.State import qualified Ideas.Encoding.Request as R #ifdef DB import Data.List import Database.HDBC import Database.HDBC.Sqlite3 (connectSqlite3, setBusyTimeout) #endif type Diff = NominalDiffTime type Time = UTCTime -- | The Record datatype is based on the Ideas Request Logging Schema version 2. data Record = Record { useLogging :: Bool -- request attributes , service :: String -- name of feedback service , exerciseid :: String -- exercise identifier , source :: String -- tool/learning environment that makes request , script :: String -- name of feedback script (for textual feedback) , requestinfo :: String -- additional information from client (only for logging) -- request format , dataformat :: String -- xml, json , encoding :: String -- options for encoding (e.g. OpenMath, string) , -- grouping requests userid :: String -- user identifier (e.g. student number) , sessionid :: String -- session identifier (grouping requests for one task) , taskid :: String -- task identifier (default: start term) -- meta-information , time :: Time -- date and time of request , responsetime :: Diff -- time needed for processing request , ipaddress :: String -- IP address of client , binary :: String -- name of (cgi) binary that is being executed , version :: String -- version (and revision) information , errormsg :: String -- internal error message (default: empty string) -- service info , serviceinfo :: String -- summary of reply (customized for each service) , ruleid :: String -- rule identifier (customized for each service) -- raw data , input :: String -- raw input (request) , output :: String -- raw output (reply) } deriving Show record :: Record record = Record True "" "" "" "" "" "" "" "" "" "" t0 0 "" "" "" "" "" "" "" "" where t0 = UTCTime (toEnum 0) 0 makeRecord :: IO Record makeRecord = do now <- getCurrentTime return record { time = now } -- | Add record information from the Request datatype addRequest :: Request -> Record -> Record addRequest req r = r { service = maybe (service r) show (R.serviceId req) , exerciseid = maybe (exerciseid r) show (R.exerciseId req) , source = fromMaybe (source r) (R.source req) , script = fromMaybe (script r) (R.feedbackScript req) , requestinfo = fromMaybe (requestinfo r) (R.requestInfo req) , dataformat = show (R.dataformat req) , encoding = show (R.encoding req) , binary = fromMaybe (binary r) (R.cgiBinary req) } -- | Add record information from the state (userid, sessionid, taskid) addState :: State a -> Record -> Record addState st r = r { userid = fromMaybe (userid r) (stateUser st) , sessionid = fromMaybe (sessionid r) (stateSession st) , taskid = fromMaybe (taskid r) (stateStartTerm st) } --------------------------------------------------------------------- data LogRef = NoRef | LogRef FilePath Schema (IORef Record) instance Semigroup LogRef where NoRef <> r = r r <> _ = r instance Monoid LogRef where mempty = NoRef mappend = (<>) defaultLogRef :: IO LogRef defaultLogRef = makeLogRef "requests.db" V2 makeLogRef :: FilePath -> Schema -> IO LogRef makeLogRef file schema = do r <- makeRecord ref <- newIORef r return (LogRef file schema ref) enableLogging :: LogRef -> IO () enableLogging = flip changeLog (\r -> r {useLogging = True}) disableLogging :: LogRef -> IO () disableLogging = flip changeLog (\r -> r {useLogging = False}) whenLogging :: LogRef -> IO () -> IO () whenLogging logRef m = do r <- getRecord logRef when (useLogging r) m getRecord :: LogRef -> IO Record getRecord NoRef = return record getRecord (LogRef _ _ r) = readIORef r getFilePath :: LogRef -> Maybe FilePath getFilePath NoRef = Nothing getFilePath (LogRef fp _ _) = Just fp changeLog :: LogRef -> (Record -> Record) -> IO () changeLog NoRef _ = return () changeLog (LogRef _ _ r) f = modifyIORef r f printLog :: LogRef -> IO () printLog logRef = do putStrLn "-- log information" getRecord logRef >>= print -------------------------------------------------------------------------------- logEnabled :: Bool logRecord :: LogRef -> IO () selectFrom :: FilePath -> String -> [String] -> ([String] -> IO a) -> IO [a] #ifdef DB logEnabled = True #else -- without logging logEnabled = False logRecord _ = return () selectFrom _ _ _ _ = return [] logRecordWith :: LogRef -> c -> IO () logRecordWith _ _ = return () #endif -------------------------------------------------------------------------------- #ifdef DB nameOfTable :: Schema -> String nameOfTable V1 = "log" nameOfTable _ = "requests" columnsInTable :: Schema -> Record -> [SqlValue] columnsInTable V1 = values_v1 columnsInTable _ = values_v2 values_v1 :: Record -> [SqlValue] values_v1 r = let get f = toSql (f r) in [ get service, get exerciseid, get source, get dataformat, get encoding , get input, get output, get ipaddress, get time, get responsetime ] values_v2 :: Record -> [SqlValue] values_v2 r = let get f = toSql (f r) in [ get service, get exerciseid, get source, get script, get requestinfo , get dataformat, get encoding, get userid, get sessionid, get taskid , get time, get responsetime, get ipaddress, get binary, get version , get errormsg, get serviceinfo, get ruleid, get input, get output ] logRecord NoRef = return () logRecord logRef@(LogRef file _ _) = (whenLogging logRef) $ do -- connect to database conn <- connectSqlite3 file setBusyTimeout conn 200 -- milliseconds logRecordWith logRef conn -- close the connection to the database disconnect conn `catchSql` \_ -> return () logRecordWith :: IConnection c => LogRef -> c -> IO () logRecordWith NoRef _ = return () logRecordWith logRef@(LogRef _ schema _) conn = do -- calculate duration r <- getRecord logRef end <- getCurrentTime let diff = diffUTCTime end (time r) -- insert data into database insertRecord schema r {responsetime = diff} conn `catchSql` \_ -> return () insertRecord :: IConnection c => Schema -> Record -> c -> IO () insertRecord schema r conn = let cols = columnsInTable schema r pars = "(" ++ intercalate "," (replicate (length cols) "?") ++ ")" stm = "INSERT INTO " ++ nameOfTable schema ++ " VALUES " ++ pars in run conn stm cols >> commit conn selectFrom database table columns f = do let sql = "SELECT " ++ commas (map safe columns) ++ " from " ++ safe table commas = intercalate "," safe = filter isAlphaNum con <- connectSqlite3 database stat <- prepare con sql _ <- execute stat [] rows <- fetchAllRows stat xs <- mapM (f . map fromSql) rows disconnect con return xs #endif