packages feed

log-utils-0.2.2: log-fetcher/LogFetcher.hs

module Main where

import Control.Applicative
import Control.Concurrent
import Control.Exception (ErrorCall(..))
import Control.Monad
import Control.Monad.Base
import Control.Monad.Catch
import Control.Monad.Trans.Reader
import Data.Aeson
import Data.Default
import Data.Function
import Data.IORef
import Data.Maybe
import Data.Time
import Database.PostgreSQL.PQTypes
import Log.Data
import Prelude
import System.Console.CmdArgs.Implicit hiding (def)
import System.Environment
import qualified Data.Foldable as F
import qualified Data.Text as T
import qualified Data.Text.IO as T
import qualified Data.Traversable as T

import SQL

data CmdArgument = Logs {
  database  :: String
, component :: Maybe String
, from      :: Maybe String
, to        :: Maybe String
, where_    :: Maybe String
, limit     :: Maybe Int
, last_     :: Bool
, follow    :: Bool
} | Components {
  database :: String
} deriving (Data, Typeable)

cmdLogs :: CmdArgument
cmdLogs = Logs {
  database  = defDatabase
, component = Nothing
           &= name "c"
           &= help "system component (optional)"
           &= typ "COMPONENT"
, from      = Nothing
           &= name "f"
           &= help ("fetch logs since (optional, format: " ++ timeFormat ++ ")")
           &= typ "TIMESTAMP"
, to        = Nothing
           &= name "t"
           &= help ("fetch logs until (optional, format: " ++ timeFormat ++ ")")
           &= typ "TIMESTAMP"
, where_    = Nothing
           &= name "w"
           &= help "WHERE clause to further filter logs (optional)"
           &= typ "SQL"
, limit     = Nothing
           &= name "l"
           &= help ("limit of fetched logs (optional, default: " ++ show defLogLimit ++ ")")
, last_     = False
           &= help "fetch <LIMIT> last logs instead of first ones (optional)"
, follow    = False
           &= help "output logs as they are recorded (with 5 seconds delay). If set, implicitly enables 'last' and overwrites 'to' (optional)"
} &= help "Fetch the list of log messages fulfilling set criteria"
  where
    timeFormat = "'YYYY-MM-DD hh:mm:ss'"

cmdComponents :: CmdArgument
cmdComponents = Components {
  database = defDatabase
} &= help "Fetch the list of available components"

defDatabase :: String
defDatabase = ""
           &= name "d"
           &= help "database connection info (required)"
           &= typ "CONNINFO"

----------------------------------------

data LogsSt = LogsSt {
  lsLogNumber :: !(IORef Int)
}

type LogsM = ReaderT LogsSt (DBT IO)

main :: IO ()
main = do
  progName <- getProgName
  cmd <- cmdArgs $ modes [cmdComponents, cmdLogs] &= program progName
  let cs = simpleSource $ def { csConnInfo = T.pack $ database cmd }
  case cmd of
    Components{..} -> runDB (unConnectionSource cs) $ fetchComponents >>= mapM_ (liftBase . T.putStrLn)
    Logs{..} -> runDB (unConnectionSource cs) $ do
      utcFrom <- T.mapM parseTime_ from
      utcTo <- T.mapM parseTime_ to
      logRq <- parseLogRequest . encode . object $ catMaybes [
          fmap ("component" .=) component
        , fmap ("from"      .=) utcFrom
        , fmap ("to"        .=) utcTo
        , fmap ("where"     .=) where_
        , fmap ("limit"     .=) limit
        , Just $ "last" .= last_
        ]
      initSt <- LogsSt <$> liftBase (newIORef 0)
      (`runReaderT` initSt) . (`finally` printSummary) $ if not follow
        then printLogs logRq
        else do
          -- When following logs, start by fetching all logs recorded
          -- until 5 seconds ago and then each second fetch logs recorded
          -- between now - 6s and now - 5s. The delay is needed so that
          -- we don't "lose" logs that might have been recorded with a
          -- slight delay (5 seconds should be plenty of time to archive
          -- that).
          initRq <- liftBase $ getCurrentTime
            >>= \now -> return logRq {
              lrTo   = Just $ fiveSecondsBefore now
            , lrLast = True
            }
          (`fix` initRq) $ \loop rq -> do
            printLogs rq
            liftBase $ threadDelay 1000000
            now <- liftBase getCurrentTime
            loop $ rq {
              lrFrom  = lrTo rq
            , lrTo    = Just $ fiveSecondsBefore now
            , lrLimit = Just maxBound
            }
  where
    fiveSecondsBefore :: UTCTime -> UTCTime
    fiveSecondsBefore t = (-5) `addUTCTime` t

    printSummary :: LogsM ()
    printSummary = do
      n <- liftBase . readIORef =<< asks lsLogNumber
      liftBase $ putStrLn $ show n ++ " log messages fetched."

    printLogs :: LogRequest -> LogsM ()
    printLogs logRq = foldChunkedLogs logRq return () $ \() qr -> do
      liftBase . (`modifyIORef'` (+ ntuples qr)) =<< asks lsLogNumber
      liftBase . F.forM_ qr $ \(t, lm) -> T.putStrLn $ showLogMessage (Just t) lm

    parseTime_ :: MonadThrow m => String -> m UTCTime
    parseTime_ s = case mtime of
      Just time -> return time
      Nothing   -> throwM . ErrorCall $ "parseTime_: invalid value: " ++ s
      where
        mtime = msum [
            parse "%Y-%m-%d" s
          , parse "%Y-%m-%d %H:%M" s
          , parse "%Y-%m-%d %H:%M:%S%Q" s
          ]
        parse = parseTimeM True defaultTimeLocale