module Data.Conduit.LogFile
( RotatingLog
, openRotatingLog
, addChunk
, close
, defaultMaxTotal
, dummy
) where
import Control.Concurrent (forkIO)
import Control.Concurrent.STM (atomically)
import Control.Concurrent.STM.TBQueue
import Control.Concurrent.STM.TVar
import Control.Exception (bracket, bracketOnError,
finally)
import Control.Monad (void, when)
import qualified Data.ByteString as S
import Data.Time (UTCTime, getCurrentTime)
import Data.Word (Word)
import System.Directory (createDirectoryIfMissing,
doesFileExist, renameFile)
import System.FilePath ((<.>), (</>))
import qualified System.IO as SIO
import System.IO.Unsafe (unsafePerformIO)
import System.Mem.Weak (addFinalizer)
data Command = AddChunk !S.ByteString
| Close
data RotatingLog = RotatingLog !(TVar State)
dummy :: RotatingLog
dummy = RotatingLog $! unsafePerformIO $! newTVarIO Closed
data State = Closed
| Running !SIO.Handle !(TBQueue Command)
queue :: Command -> RotatingLog -> IO ()
queue cmd (RotatingLog ts) = atomically $ do
s <- readTVar ts
case s of
Closed -> return ()
Running _ q -> writeTBQueue q cmd
addChunk :: RotatingLog -> S.ByteString -> IO ()
addChunk lf bs = queue (AddChunk bs) lf
close :: RotatingLog -> IO ()
close = queue Close
openRotatingLog :: FilePath
-> Word
-> IO RotatingLog
openRotatingLog dir maxTotal = do
createDirectoryIfMissing True dir
bracketOnError (moveCurrent dir) SIO.hClose $ \handle -> do
queue <- newTBQueueIO 5
let s = Running handle queue
ts <- newTVarIO s
void $ forkIO $ loop dir ts maxTotal
let rl = RotatingLog ts
addFinalizer rl (atomically (writeTBQueue queue Close))
return rl
current :: FilePath
-> FilePath
current = (</> "current.log")
moveCurrent :: FilePath
-> IO SIO.Handle
moveCurrent dir = do
let curr = current dir
x <- doesFileExist curr
when x $ do
now <- getCurrentTime
renameFile curr $ dir </> suffix now
SIO.openFile curr SIO.WriteMode
suffix :: UTCTime -> FilePath
suffix now =
(concatMap fix $ takeWhile (/= '.') $ show now) <.> "log"
where
fix ' ' = "_"
fix c | '0' <= c && c <= '9' = [c]
fix _ = ""
loop :: FilePath
-> TVar State
-> Word
-> IO ()
loop dir ts maxTotal =
go 0 `finally` (closeCurrentHandle `finally` atomically (writeTVar ts Closed))
where
closeCurrentHandle = bracket
(atomically $ do
s <- readTVar ts
case s of
Closed -> return Nothing
Running h _ -> return $! Just h)
(maybe (return ()) SIO.hClose)
(const $ return ())
go total = do
res <- atomically $ do
s <- readTVar ts
case s of
Closed -> return Nothing
Running handle queue -> do
cmd <- readTBQueue queue
case cmd of
Close -> return Nothing
AddChunk bs -> return $! Just (handle, queue, bs)
case res of
Nothing -> return ()
Just (handle, queue, bs) -> do
let total' = total + fromIntegral (S.length bs)
S.hPut handle bs
SIO.hFlush handle
if total' > maxTotal
then do
bracket
(SIO.hClose handle >> moveCurrent dir)
(\handle' -> atomically $ writeTVar ts $ Running handle' queue)
(const $ return ())
go 0
else go total'
defaultMaxTotal :: Word
defaultMaxTotal = 5 * 1024 * 1024