{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
module System.FastLogger
( Logger
, timestampedLogEntry
, combinedLogEntry
, newLogger
, newLoggerWithCustomErrorFunction
, withLogger
, withLoggerWithCustomErrorFunction
, stopLogger
, logMsg
) where
import Control.Concurrent (MVar, ThreadId, killThread, newEmptyMVar, putMVar, takeMVar, threadDelay, tryPutMVar, withMVar)
import Control.Concurrent.Extended (forkIOLabeledWithUnmaskBs)
import Control.Exception (AsyncException, Handler (..), IOException, SomeException, bracket, catch, catches, mask_)
import Control.Monad (unless, void, when)
import Data.ByteString.Builder (Builder, byteString, char8, stringUtf8, toLazyByteString, toLazyByteString)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as S
import qualified Data.ByteString.Lazy.Char8 as L
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.Monoid (mappend, mconcat, mempty)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Word (Word64)
import Prelude (Eq (..), FilePath, IO, Int, Maybe, Monad (..), Num (..), Ord (..), Show (..), mapM_, maybe, ($), ($!), (++), (.), (||))
import System.IO (IOMode (AppendMode), hClose, hFlush, openFile, stderr, stdout)
import System.PosixCompat.Time (epochTime)
import Snap.Internal.Http.Server.Common (atomicModifyIORef')
import Snap.Internal.Http.Server.Date (getLogDateString)
data Logger = Logger
{ _queuedMessages :: !(IORef Builder)
, _dataWaiting :: !(MVar ())
, _loggerPath :: !(FilePath)
, _loggingThread :: !(MVar ThreadId)
, _errAction :: ByteString -> IO ()
}
newLogger :: FilePath
-> IO Logger
newLogger = newLoggerWithCustomErrorFunction
(\s -> S.hPutStr stderr s >> hFlush stderr)
newLoggerWithCustomErrorFunction :: (ByteString -> IO ())
-> FilePath
-> IO Logger
newLoggerWithCustomErrorFunction errAction fp = do
q <- newIORef mempty
dw <- newEmptyMVar
th <- newEmptyMVar
let lg = Logger q dw fp th errAction
mask_ $ do
tid <- forkIOLabeledWithUnmaskBs "snap-server: logging" $
loggingThread lg
putMVar th tid
return lg
withLogger :: FilePath
-> (Logger -> IO a)
-> IO a
withLogger f = bracket (newLogger f) stopLogger
withLoggerWithCustomErrorFunction :: (ByteString -> IO ())
-> FilePath
-> (Logger -> IO a)
-> IO a
withLoggerWithCustomErrorFunction e f =
bracket (newLoggerWithCustomErrorFunction e f) stopLogger
timestampedLogEntry :: ByteString -> IO ByteString
timestampedLogEntry msg = do
timeStr <- getLogDateString
return $! S.concat
$ L.toChunks
$ toLazyByteString
$ mconcat [ char8 '['
, byteString timeStr
, byteString "] "
, byteString msg ]
combinedLogEntry :: ByteString
-> Maybe ByteString
-> ByteString
-> Int
-> Word64
-> Maybe ByteString
-> ByteString
-> IO ByteString
combinedLogEntry !host !mbUser !req !status !numBytes !mbReferer !ua = do
timeStr <- getLogDateString
let !l = [ byteString host
, byteString " - "
, user
, byteString " ["
, byteString timeStr
, byteString "] \""
, byteString req
, byteString "\" "
, fromShow status
, space
, fromShow numBytes
, space
, referer
, byteString " \""
, byteString ua
, quote ]
return $! S.concat . L.toChunks $ toLazyByteString $ mconcat l
where
dash = char8 '-'
quote = char8 '\"'
space = char8 ' '
user = maybe dash byteString mbUser
referer = maybe dash
(\s -> mconcat [ quote
, byteString s
, quote ])
mbReferer
logMsg :: Logger -> ByteString -> IO ()
logMsg !lg !s = do
let !s' = byteString s `mappend` char8 '\n'
atomicModifyIORef' (_queuedMessages lg) $ \d -> (d `mappend` s',())
void $ tryPutMVar (_dataWaiting lg) ()
loggingThread :: Logger -> (forall a. IO a -> IO a) -> IO ()
loggingThread (Logger queue notifier filePath _ errAct) unmask = do
initialize >>= go
where
openIt =
if filePath == "-"
then return stdout
else
if filePath == "stderr"
then return stderr
else openFile filePath AppendMode `catch`
\(e::IOException) -> do
logInternalError $ "Can't open log file \"" ++
filePath ++ "\".\n"
logInternalError $ "Exception: " ++ show e ++ "\n"
logInternalError $ "Logging to stderr instead. " ++
"**THIS IS BAD, YOU OUGHT TO " ++
"FIX THIS**\n\n"
return stderr
closeIt h = unless (h == stdout || h == stderr) $
hClose h
logInternalError = errAct . T.encodeUtf8 . T.pack
go (href, lastOpened) = unmask loop `catches`
[ Handler $ \(_::AsyncException) -> killit (href, lastOpened)
, Handler $ \(e::SomeException) -> do
logInternalError $ "logger got exception: "
++ Prelude.show e ++ "\n"
threadDelay 20000000
go (href, lastOpened) ]
where
loop = waitFlushDelay (href, lastOpened) >> loop
initialize = do
lh <- openIt
href <- newIORef lh
t <- epochTime
tref <- newIORef t
return (href, tref)
killit (href, lastOpened) = do
flushIt (href, lastOpened)
h <- readIORef href
closeIt h
flushIt (!href, !lastOpened) = do
dl <- atomicModifyIORef' queue $ \x -> (mempty,x)
let !msgs = toLazyByteString dl
h <- readIORef href
(do L.hPut h msgs
hFlush h) `catch` \(e::IOException) -> do
logInternalError $ "got exception writing to log " ++
filePath ++ ": " ++ show e ++ "\n"
logInternalError "writing log entries to stderr.\n"
mapM_ errAct $ L.toChunks msgs
t <- epochTime
old <- readIORef lastOpened
when (t-old > 900) $ do
closeIt h
mask_ $ openIt >>= writeIORef href
writeIORef lastOpened t
waitFlushDelay !d = do
_ <- takeMVar notifier
flushIt d
threadDelay 5000000
stopLogger :: Logger -> IO ()
stopLogger lg = withMVar (_loggingThread lg) killThread
fromShow :: Show a => a -> Builder
fromShow = stringUtf8 . show