{-# LANGUAGE RecordWildCards #-} ----------------------------------------------------------------------------- -- | -- Module : System.RotatingLog -- Copyright : Soostone Inc -- License : BSD3 -- -- Maintainer : admin@soostone.com -- Stability : experimental -- -- Convenient logging to a disk-based log file with automatic file -- rotation based on size. ---------------------------------------------------------------------------- module System.RotatingLog ( -- * Core API RotatingLog , mkRotatingLog , rotatedWrite , rotatedWrite' -- * Built-In Post-Rotate Actions , archiveFile ) where ------------------------------------------------------------------------------- import Control.Concurrent.MVar import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Time import Data.Word import System.Directory import System.FilePath.Posix import System.IO import System.Locale ------------------------------------------------------------------------------- ------------------------------------------------------------------------------ -- | A size-limited rotating log. Log filenames are of the format -- prefix_timestamp.log. data RotatingLog = RotatingLog { logInfo :: MVar LogInfo , namePrefix :: String , sizeLimit :: Word64 , buffering :: BufferMode , postAction :: FilePath -> IO () } data LogInfo = LogInfo { curHandle :: Handle , bytesWritten :: !Word64 } curLogFileName :: String -> FilePath curLogFileName = (++".log") logFileName :: String -> UTCTime -> FilePath logFileName pre t = concat [pre, "_", formatTime defaultTimeLocale "%Y_%m_%d_%H_%M_%S%Q" t, ".log"] ------------------------------------------------------------------------------ -- | Creates a rotating log given a prefix and size limit in bytes. mkRotatingLog :: String -- ^ A prefix for the written log files. -> Word64 -- ^ A size limit in bytes. -> BufferMode -- ^ A buffering mode for output; we leave it to you to decide how -- often the file should be flushed. -> (FilePath -> IO ()) -- ^ An action to be performed on the finished file following -- rotation. For example, you could give a callback that moves or -- ships the files somewhere else. -> IO RotatingLog mkRotatingLog pre limit buf pa = do mvar <- newEmptyMVar let rl = RotatingLog mvar pre limit buf pa h <- openLogFile rl len <- hFileSize h putMVar mvar $ LogInfo h (fromIntegral len) return rl ------------------------------------------------------------------------------- openLogFile RotatingLog{..} = do let fp = curLogFileName namePrefix h <- openFile fp AppendMode hSetBuffering h buffering return h ------------------------------------------------------------------------------ -- | Like "rotatedWrite'", but doesn't need a UTCTime and obtains it -- with a syscall. rotatedWrite :: RotatingLog -> ByteString -> IO () rotatedWrite rlog bs = do t <- getCurrentTime rotatedWrite' rlog t bs ------------------------------------------------------------------------------ -- | Writes ByteString to a rotating log file. If this write would exceed the -- size limit, then the file is closed and a new file opened. This function -- takes a UTCTime to allow a cached time to be used to avoid a system call. -- -- Please note this function does NOT implicitly insert a newline at -- the end of the string you provide. This is so that it can be used -- to log non-textual streams such as binary serialized or compressed -- content. rotatedWrite' :: RotatingLog -> UTCTime -> ByteString -> IO () rotatedWrite' rl@RotatingLog{..} t bs = do modifyMVar_ logInfo $ \LogInfo{..} -> do (h,b) <- if bytesWritten + len > sizeLimit then do hClose curHandle let newFile = logFileName namePrefix t renameFile curFile newFile postAction newFile h <- openLogFile rl return (h, 0) else return (curHandle, bytesWritten) B.hPutStr h bs return $! LogInfo h (len + b) where len = fromIntegral $ B.length bs curFile = curLogFileName namePrefix ------------------------------------------------------------------------------- -- | A built-in post-rotate action that moves the finished file to a -- given archive location. archiveFile :: FilePath -- ^ A target archive directory -> (FilePath -> IO ()) archiveFile archive fp = let (_, fn) = splitFileName fp target = archive </> fn in do createDirectoryIfMissing True archive renameFile fp target