{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE GADTs #-}
module System.Log.FastLogger (
LoggerSet
, newFileLoggerSet
, newStdoutLoggerSet
, newStderrLoggerSet
, newLoggerSet
, BufSize
, defaultBufSize
, renewLoggerSet
, rmLoggerSet
, LogStr
, ToLogStr(..)
, fromLogStr
, logStrLength
, pushLogStr
, pushLogStrLn
, flushLogStr
, FastLogger
, TimedFastLogger
, LogType'(..), LogType
, newFastLogger
, withFastLogger
, newTimedFastLogger
, withTimedFastLogger
, module System.Log.FastLogger.Date
, module System.Log.FastLogger.File
, module System.Log.FastLogger.Types
) where
import Control.Debounce (mkDebounce, defaultDebounceSettings, debounceAction)
import Control.Concurrent (getNumCapabilities, myThreadId, threadCapability, takeMVar, MVar, newMVar, tryTakeMVar, putMVar)
import Control.Exception (handle, SomeException(..), bracket)
import Data.Array (Array, listArray, (!), bounds)
import System.EasyFile (getFileSize)
import System.Log.FastLogger.Date
import System.Log.FastLogger.File
import System.Log.FastLogger.FileIO
import System.Log.FastLogger.IO
import System.Log.FastLogger.Imports
import System.Log.FastLogger.LogStr
import System.Log.FastLogger.Logger
import System.Log.FastLogger.Types
data LoggerSet = LoggerSet (Maybe FilePath) (IORef FD) (Array Int Logger) (IO ())
newFileLoggerSet :: BufSize -> FilePath -> IO LoggerSet
newFileLoggerSet size file = openFileFD file >>= newFDLoggerSet size (Just file)
newStdoutLoggerSet :: BufSize -> IO LoggerSet
newStdoutLoggerSet size = getStdoutFD >>= newFDLoggerSet size Nothing
newStderrLoggerSet :: BufSize -> IO LoggerSet
newStderrLoggerSet size = getStderrFD >>= newFDLoggerSet size Nothing
{-# DEPRECATED newLoggerSet "Use newFileLoggerSet etc instead" #-}
newLoggerSet :: BufSize -> Maybe FilePath -> IO LoggerSet
newLoggerSet size = maybe (newStdoutLoggerSet size) (newFileLoggerSet size)
newFDLoggerSet :: BufSize -> Maybe FilePath -> FD -> IO LoggerSet
newFDLoggerSet size mfile fd = do
n <- getNumCapabilities
loggers <- replicateM n $ newLogger (max 1 size)
let arr = listArray (0,n-1) loggers
fref <- newIORef fd
flush <- mkDebounce defaultDebounceSettings
{ debounceAction = flushLogStrRaw fref arr
}
return $ LoggerSet mfile fref arr flush
pushLogStr :: LoggerSet -> LogStr -> IO ()
pushLogStr (LoggerSet _ fdref arr flush) logmsg = do
(i, _) <- myThreadId >>= threadCapability
let u = snd $ bounds arr
lim = u + 1
j | i < lim = i
| otherwise = i `mod` lim
let logger = arr ! j
pushLog fdref logger logmsg
flush
pushLogStrLn :: LoggerSet -> LogStr -> IO ()
pushLogStrLn loggerSet logStr = pushLogStr loggerSet (logStr <> "\n")
flushLogStr :: LoggerSet -> IO ()
flushLogStr (LoggerSet _ fref arr _) = flushLogStrRaw fref arr
flushLogStrRaw :: IORef FD -> Array Int Logger -> IO ()
flushLogStrRaw fdref arr = do
let (l,u) = bounds arr
mapM_ flushIt [l .. u]
where
flushIt i = flushLog fdref (arr ! i)
renewLoggerSet :: LoggerSet -> IO ()
renewLoggerSet (LoggerSet Nothing _ _ _) = return ()
renewLoggerSet (LoggerSet (Just file) fref _ _) = do
newfd <- openFileFD file
oldfd <- atomicModifyIORef' fref (\fd -> (newfd, fd))
closeFD oldfd
rmLoggerSet :: LoggerSet -> IO ()
rmLoggerSet (LoggerSet mfile fdref arr _) = do
let (l,u) = bounds arr
let nums = [l .. u]
mapM_ flushIt nums
mapM_ freeIt nums
fd <- readIORef fdref
when (isJust mfile) $ closeFD fd
where
flushIt i = flushLog fdref (arr ! i)
freeIt i = do
let (Logger _ mbuf _) = arr ! i
takeMVar mbuf >>= freeBuffer
type FastLogger = LogStr -> IO ()
type TimedFastLogger = (FormattedTime -> LogStr) -> IO ()
type LogType = LogType' LogStr
data LogType' a where
LogNone :: LogType' LogStr
LogStdout :: BufSize -> LogType' LogStr
LogStderr :: BufSize -> LogType' LogStr
LogFileNoRotate :: FilePath -> BufSize -> LogType' LogStr
LogFile :: FileLogSpec -> BufSize -> LogType' LogStr
LogFileTimedRotate :: TimedFileLogSpec -> BufSize -> LogType' LogStr
LogCallback :: (v -> IO ()) -> IO () -> LogType' v
newFastLogger :: LogType' v -> IO (v -> IO (), IO ())
newFastLogger typ = case typ of
LogNone -> return (const noOp, noOp)
LogStdout bsize -> newStdoutLoggerSet bsize >>= stdLoggerInit
LogStderr bsize -> newStderrLoggerSet bsize >>= stdLoggerInit
LogFileNoRotate fp bsize -> newFileLoggerSet bsize fp >>= fileLoggerInit
LogFile fspec bsize -> rotateLoggerInit fspec bsize
LogFileTimedRotate fspec bsize -> timedRotateLoggerInit fspec bsize
LogCallback cb flush -> return (\str -> cb str >> flush, noOp)
where
stdLoggerInit lgrset = return (pushLogStr lgrset, rmLoggerSet lgrset)
fileLoggerInit lgrset = return (pushLogStr lgrset, rmLoggerSet lgrset)
rotateLoggerInit fspec bsize = do
lgrset <- newFileLoggerSet bsize $ log_file fspec
ref <- newIORef (0 :: Int)
mvar <- newMVar ()
let logger str = do
cnt <- decrease ref
pushLogStr lgrset str
when (cnt <= 0) $ tryRotate lgrset fspec ref mvar
return (logger, rmLoggerSet lgrset)
timedRotateLoggerInit fspec bsize = do
cache <- newTimeCache $ timed_timefmt fspec
now <- cache
lgrset <- newFileLoggerSet bsize $ prefixTime now $ timed_log_file fspec
ref <- newIORef now
mvar <- newMVar lgrset
let logger str = do
ct <- cache
updated <- updateTime (timed_same_timeframe fspec) ref ct
when updated $ tryTimedRotate fspec ct mvar
pushLogStr lgrset str
return (logger, rmLoggerSet lgrset)
withFastLogger :: LogType -> (FastLogger -> IO a) -> IO a
withFastLogger typ log' = bracket (newFastLogger typ) snd (log' . fst)
newTimedFastLogger ::
IO FormattedTime
-> LogType -> IO (TimedFastLogger, IO ())
newTimedFastLogger tgetter typ = case typ of
LogNone -> return (const noOp, noOp)
LogStdout bsize -> newStdoutLoggerSet bsize >>= stdLoggerInit
LogStderr bsize -> newStderrLoggerSet bsize >>= stdLoggerInit
LogFileNoRotate fp bsize -> newFileLoggerSet bsize fp >>= fileLoggerInit
LogFile fspec bsize -> rotateLoggerInit fspec bsize
LogFileTimedRotate fspec bsize -> timedRotateLoggerInit fspec bsize
LogCallback cb flush -> return (\f -> tgetter >>= cb . f >> flush, noOp)
where
stdLoggerInit lgrset = return ( \f -> tgetter >>= pushLogStr lgrset . f, rmLoggerSet lgrset)
fileLoggerInit lgrset = return (\f -> tgetter >>= pushLogStr lgrset . f, rmLoggerSet lgrset)
rotateLoggerInit fspec bsize = do
lgrset <- newFileLoggerSet bsize $ log_file fspec
ref <- newIORef (0 :: Int)
mvar <- newMVar ()
let logger f = do
cnt <- decrease ref
t <- tgetter
pushLogStr lgrset (f t)
when (cnt <= 0) $ tryRotate lgrset fspec ref mvar
return (logger, rmLoggerSet lgrset)
timedRotateLoggerInit fspec bsize = do
cache <- newTimeCache $ timed_timefmt fspec
now <- cache
lgrset <- newFileLoggerSet bsize $ prefixTime now $ timed_log_file fspec
ref <- newIORef now
mvar <- newMVar lgrset
let logger f = do
ct <- cache
updated <- updateTime (timed_same_timeframe fspec) ref ct
when updated $ tryTimedRotate fspec ct mvar
t <- tgetter
pushLogStr lgrset (f t)
return (logger, rmLoggerSet lgrset)
withTimedFastLogger :: IO FormattedTime -> LogType -> (TimedFastLogger -> IO a) -> IO a
withTimedFastLogger tgetter typ log' = bracket (newTimedFastLogger tgetter typ) snd (log' . fst)
noOp :: IO ()
noOp = return ()
decrease :: IORef Int -> IO Int
decrease ref = atomicModifyIORef' ref (\x -> (x - 1, x - 1))
updateTime :: (FormattedTime -> FormattedTime -> Bool) -> IORef FormattedTime -> FormattedTime -> IO Bool
updateTime cmp ref newTime = atomicModifyIORef' ref (\x -> (newTime, not $ cmp x newTime))
tryRotate :: LoggerSet -> FileLogSpec -> IORef Int -> MVar () -> IO ()
tryRotate lgrset spec ref mvar = bracket lock unlock rotateFiles
where
lock = tryTakeMVar mvar
unlock Nothing = return ()
unlock _ = putMVar mvar ()
rotateFiles Nothing = return ()
rotateFiles _ = do
msiz <- getSize
case msiz of
Nothing -> writeIORef ref 1000000
Just siz
| siz > limit -> do
rotate spec
renewLoggerSet lgrset
writeIORef ref $ estimate limit
| otherwise ->
writeIORef ref $ estimate (limit - siz)
file = log_file spec
limit = log_file_size spec
getSize = handle (\(SomeException _) -> return Nothing) $
Just . fromIntegral <$> getFileSize file
estimate x = fromInteger (x `div` 200)
tryTimedRotate :: TimedFileLogSpec -> FormattedTime -> MVar LoggerSet -> IO ()
tryTimedRotate spec now mvar = bracket lock unlock rotateFiles
where
lock = tryTakeMVar mvar
unlock Nothing = return ()
unlock (Just (LoggerSet current_path a b c)) = do
putMVar mvar $ LoggerSet (Just new_file_path) a b c
case current_path of
Nothing -> return ()
Just path -> timed_post_process spec path
rotateFiles Nothing = return ()
rotateFiles (Just (LoggerSet _ a b c)) = renewLoggerSet $ LoggerSet (Just new_file_path) a b c
new_file_path = prefixTime now $ timed_log_file spec