module System.Logger.Logger.Internal
(
LoggerConfig(..)
, loggerConfigQueueSize
, loggerConfigThreshold
, loggerConfigScope
, loggerConfigPolicy
, loggerConfigExceptionLimit
, loggerConfigExceptionWait
, loggerConfigExitTimeout
, defaultLoggerConfig
, validateLoggerConfig
, pLoggerConfig
, pLoggerConfig_
, Logger
, loggerScope
, loggerThreshold
, createLogger
, createLogger_
, releaseLogger
, withLogger
, withLogger_
, loggCtx
, withLogFunction
, withLogFunction_
, LoggerT
, runLoggerT
, runLogT
) where
#ifndef MIN_VERSION_base
#define MIN_VERSION_base(a,b,c) 1
#endif
#ifndef MIN_VERSION_deepseq
#define MIN_VERSION_deepseq(a,b,c) 1
#endif
#ifndef MIN_VERSION_aeson
#define MIN_VERSION_aeson(a,b,c) 1
#endif
import Configuration.Utils hiding (Lens', Error)
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async
import Control.Monad.STM
import Control.Concurrent.STM.TVar
import Control.DeepSeq
import Control.Exception.Lifted
import Control.Exception.Enclosed
import Control.Lens hiding ((.=))
import Control.Monad.Except
import Control.Monad.Trans.Control
import Control.Monad.Unicode
import Data.Monoid.Unicode
import qualified Data.Text as T
import Data.Typeable
import qualified Data.Text.IO as T (hPutStrLn)
import Data.Void
import GHC.Generics
import Numeric.Natural
import Prelude.Unicode
import System.Clock
import System.IO (stderr)
import System.Timeout
import System.Logger.Internal
import System.Logger.Internal.Queue
import System.Logger.Types
#if ! MIN_VERSION_aeson(0,11,1) && ! ( MIN_VERSION_base(4,8,0) && MIN_VERSION_aeson(0,11,0) )
instance ToJSON Natural where
toJSON = Number ∘ fromIntegral
instance FromJSON Natural where
parseJSON = withScientific "Natural" $ \n →
if n < 0
then fail $ "expected a natural number but got " ⊕ show n
else pure $ floor n
#endif
data LoggerConfig = LoggerConfig
{ _loggerConfigQueueSize ∷ !Natural
, _loggerConfigThreshold ∷ !LogLevel
, _loggerConfigScope ∷ !LogScope
, _loggerConfigPolicy ∷ !LogPolicy
, _loggerConfigExceptionLimit ∷ !(Maybe Natural)
, _loggerConfigExceptionWait ∷ !(Maybe Natural)
, _loggerConfigExitTimeout ∷ !(Maybe Natural)
}
deriving (Show, Read, Eq, Ord, Typeable, Generic)
loggerConfigQueueSize ∷ Lens' LoggerConfig Natural
loggerConfigQueueSize = lens _loggerConfigQueueSize $ \a b → a { _loggerConfigQueueSize = b }
loggerConfigThreshold ∷ Lens' LoggerConfig LogLevel
loggerConfigThreshold = lens _loggerConfigThreshold $ \a b → a { _loggerConfigThreshold = b }
loggerConfigScope ∷ Lens' LoggerConfig LogScope
loggerConfigScope = lens _loggerConfigScope $ \a b → a { _loggerConfigScope = b }
loggerConfigPolicy ∷ Lens' LoggerConfig LogPolicy
loggerConfigPolicy = lens _loggerConfigPolicy $ \a b → a { _loggerConfigPolicy = b }
loggerConfigExceptionLimit ∷ Lens' LoggerConfig (Maybe Natural)
loggerConfigExceptionLimit = lens _loggerConfigExceptionLimit $ \a b → a { _loggerConfigExceptionLimit = b }
loggerConfigExceptionWait ∷ Lens' LoggerConfig (Maybe Natural)
loggerConfigExceptionWait = lens _loggerConfigExceptionWait $ \a b → a { _loggerConfigExceptionWait = b }
loggerConfigExitTimeout ∷ Lens' LoggerConfig (Maybe Natural)
loggerConfigExitTimeout = lens _loggerConfigExitTimeout $ \a b → a { _loggerConfigExitTimeout = b }
#if ! MIN_VERSION_deepseq(1,4,0) || ! MIN_VERSION_base(4,8,0)
instance NFData Natural where
rnf a = a `seq` ()
#endif
#if MIN_VERSION_deepseq(1,4,0)
instance NFData LoggerConfig
#else
instance NFData LoggerConfig where
rnf (LoggerConfig a0 a1 a2 a3 a4 a5 a6) =
rnf a0 `seq` rnf a1 `seq` rnf a2 `seq` rnf a3 `seq` rnf a4 `seq` rnf a5 `seq` rnf a6
#endif
defaultLoggerConfig ∷ LoggerConfig
defaultLoggerConfig = LoggerConfig
{ _loggerConfigQueueSize = 1000
, _loggerConfigThreshold = Warn
, _loggerConfigScope = []
, _loggerConfigPolicy = LogPolicyDiscard
, _loggerConfigExceptionLimit = Just 10
, _loggerConfigExceptionWait = Just 1000
, _loggerConfigExitTimeout = Just 1000000
}
validateLoggerConfig ∷ ConfigValidation LoggerConfig λ
validateLoggerConfig _ = return ()
instance ToJSON LoggerConfig where
toJSON LoggerConfig{..} = object
[ "queue_size" .= _loggerConfigQueueSize
, "log_level" .= _loggerConfigThreshold
, "scope" .= _loggerConfigScope
, "policy" .= _loggerConfigPolicy
, "exception_limit" .= _loggerConfigExceptionLimit
, "exception_wait" .= _loggerConfigExceptionWait
, "exit_timeout" .= _loggerConfigExitTimeout
]
instance FromJSON (LoggerConfig → LoggerConfig) where
parseJSON = withObject "LoggerConfig" $ \o → id
<$< loggerConfigQueueSize ..: "queue_size" × o
<*< loggerConfigThreshold ..: "log_level" × o
<*< loggerConfigScope ..: "scope" × o
<*< loggerConfigPolicy ..: "policy" × o
<*< loggerConfigExceptionLimit ..: "exception_limit" × o
<*< loggerConfigExceptionWait ..: "exception_wait" × o
<*< loggerConfigExitTimeout ..: "exit_timeout" × o
pLoggerConfig ∷ MParser LoggerConfig
pLoggerConfig = pLoggerConfig_ ""
pLoggerConfig_
∷ T.Text
→ MParser LoggerConfig
pLoggerConfig_ prefix = id
<$< loggerConfigQueueSize .:: option auto
× long (T.unpack prefix ⊕ "queue-size")
⊕ metavar "INT"
⊕ help "size of the internal logger queue"
<*< loggerConfigThreshold .:: pLogLevel_ prefix
<*< loggerConfigPolicy .:: pLogPolicy_ prefix
<*< loggerConfigExceptionLimit .:: fmap Just × option auto
× long (T.unpack prefix ⊕ "exception-limit")
⊕ metavar "INT"
⊕ help "maximal number of backend failures before and exception is raised"
<*< loggerConfigExceptionWait .:: fmap Just × option auto
× long (T.unpack prefix ⊕ "exception-wait")
⊕ metavar "INT"
⊕ help "time to wait after an backend failure occured"
<*< loggerConfigExitTimeout .:: fmap Just × option auto
× long (T.unpack prefix ⊕ "exit-timeout")
⊕ metavar "INT"
⊕ help "timeout for flushing the log message queue on exit"
type LoggerQueue a = TBMChan (LogMessage a)
data Logger a = Logger
{ _loggerQueue ∷ !(LoggerQueue a)
, _loggerWorker ∷ !(Async ())
, _loggerThreshold ∷ !LogLevel
, _loggerScope ∷ !LogScope
, _loggerPolicy ∷ !LogPolicy
, _loggerMissed ∷ !(TVar Natural)
, _loggerExitTimeout ∷ !(Maybe Natural)
, _loggerErrLogFunction ∷ !(T.Text → IO ())
}
deriving (Typeable, Generic)
loggerQueue ∷ Lens' (Logger a) (LoggerQueue a)
loggerQueue = lens _loggerQueue $ \a b → a { _loggerQueue = b }
loggerWorker ∷ Lens' (Logger a) (Async ())
loggerWorker = lens _loggerWorker $ \a b → a { _loggerWorker = b }
loggerThreshold ∷ Lens' (Logger a) LogLevel
loggerThreshold = lens _loggerThreshold $ \a b → a { _loggerThreshold = b }
loggerScope ∷ Lens' (Logger a) LogScope
loggerScope = lens _loggerScope $ \a b → a { _loggerScope = b }
loggerPolicy ∷ Lens' (Logger a) LogPolicy
loggerPolicy = lens _loggerPolicy $ \a b → a { _loggerPolicy = b }
loggerMissed ∷ Lens' (Logger a) (TVar Natural)
loggerMissed = lens _loggerMissed $ \a b → a { _loggerMissed = b }
loggerExitTimeout ∷ Lens' (Logger a) (Maybe Natural)
loggerExitTimeout = lens _loggerExitTimeout $ \a b → a { _loggerExitTimeout = b }
loggerErrLogFunction ∷ Lens' (Logger a) (T.Text → IO ())
loggerErrLogFunction = lens _loggerErrLogFunction $ \a b → a { _loggerErrLogFunction = b }
createLogger
∷ MonadIO μ
⇒ LoggerConfig
→ LoggerBackend a
→ μ (Logger a)
createLogger = createLogger_ (T.hPutStrLn stderr)
createLogger_
∷ MonadIO μ
⇒ (T.Text → IO ())
→ LoggerConfig
→ LoggerBackend a
→ μ (Logger a)
createLogger_ errLogFun LoggerConfig{..} backend = liftIO $ do
queue ← newQueue (fromIntegral _loggerConfigQueueSize)
missed ← newTVarIO 0
worker ← backendWorker errLogFun _loggerConfigExceptionLimit _loggerConfigExceptionWait backend queue missed
link worker
return $ Logger
{ _loggerQueue = queue
, _loggerWorker = worker
, _loggerThreshold = _loggerConfigThreshold
, _loggerScope = _loggerConfigScope
, _loggerPolicy = _loggerConfigPolicy
, _loggerMissed = missed
, _loggerExitTimeout = _loggerConfigExitTimeout
, _loggerErrLogFunction = errLogFun
}
backendWorker
∷ (T.Text → IO ())
→ Maybe Natural
→ Maybe Natural
→ LoggerBackend a
→ LoggerQueue a
→ TVar Natural
→ IO (Async ())
backendWorker errLogFun errLimit errWait backend queue missed = mask_ $
asyncWithUnmask $ \umask → umask (go []) `catch` \(_ ∷ LoggerKilled) → return ()
where
go errList = do
t ← getTime Realtime
readMsg t ≫= \case
Nothing → return ()
Just msg → runBackend errList msg ≫= go
runBackend errList msg = (backend msg ≫ return []) `catchAny` \e → do
t ← getTime Realtime
let errMsg = backendErrorMsg t (sshow e)
backend (Left errMsg) `catchAny` \_ →
errLogFun (errLogMsg errMsg) `catchAny` \_ →
return ()
case fromException e of
Just (BackendTerminatedException _ ∷ LoggerException Void) → throwIO e
_ → do
maybe (return ()) (threadDelay ∘ fromIntegral) errWait
let errList' = e:errList
case errLimit of
Nothing → return []
Just n
| fromIntegral (length errList') > n → throwIO $ BackendTooManyExceptions (reverse errList')
| otherwise → return errList'
readMsg t = do
n ← atomically $ swapTVar missed 0
if n > 0
then do
return ∘ Just ∘ Left $ discardMsg t n
else
fmap Right <$> readQueue queue
discardMsg t n = LogMessage
{ _logMsg = "discarded " ⊕ sshow n ⊕ " log messages"
, _logMsgLevel = Warn
, _logMsgScope = [("system", "logger")]
, _logMsgTime = t
}
backendErrorMsg t e = LogMessage
{ _logMsg = e
, _logMsgLevel = Error
, _logMsgScope = [("system", "logger"), ("component", "backend")]
, _logMsgTime = t
}
errLogMsg LogMessage{..} = T.unwords
[ formatIso8601Milli _logMsgTime
, "[" ⊕ logLevelText _logMsgLevel ⊕ "]"
, formatScope _logMsgScope
, _logMsg
]
formatScope scope = "[" ⊕ T.intercalate "," (map formatLabel scope) ⊕ "]"
formatLabel (k,v) = "(" ⊕ k ⊕ "," ⊕ v ⊕ ")"
data LoggerKilled = LoggerKilled deriving (Show, Typeable)
instance Exception LoggerKilled
releaseLogger
∷ MonadIO μ
⇒ Logger a
→ μ ()
releaseLogger Logger{..} = liftIO $ do
closeQueue _loggerQueue
complete ← maybe (fmap Just) (timeout ∘ fromIntegral) _loggerExitTimeout $ wait _loggerWorker
case complete of
Nothing → _loggerErrLogFunction "logger: timeout while flushing queue; remaining messages are discarded"
Just _ → return ()
cancelWith _loggerWorker LoggerKilled
withLogger
∷ (MonadIO μ, MonadBaseControl IO μ)
⇒ LoggerConfig
→ LoggerBackend a
→ (Logger a → μ α)
→ μ α
withLogger = withLogger_ (T.hPutStrLn stderr)
withLogger_
∷ (MonadIO μ, MonadBaseControl IO μ)
⇒ (T.Text → IO ())
→ LoggerConfig
→ LoggerBackend a
→ (Logger a → μ α)
→ μ α
withLogger_ errLogFun config backend =
bracket (createLogger_ errLogFun config backend) releaseLogger
withLogFunction
∷ (Show a, Typeable a, NFData a, MonadIO μ, MonadBaseControl IO μ)
⇒ LoggerConfig
→ LoggerBackend a
→ (LogFunctionIO a → μ α)
→ μ α
withLogFunction = withLogFunction_ (T.hPutStrLn stderr)
withLogFunction_
∷ (Show a, Typeable a, NFData a, MonadIO μ, MonadBaseControl IO μ)
⇒ (T.Text → IO ())
→ LoggerConfig
→ LoggerBackend a
→ (LogFunctionIO a → μ α)
→ μ α
withLogFunction_ errLogFun config backend f =
withLogger_ errLogFun config backend $ f ∘ loggCtx
loggCtx
∷ (Show a, Typeable a, NFData a)
⇒ Logger a
→ LogFunctionIO a
loggCtx Logger{..} level msg = do
case _loggerThreshold of
Quiet → return ()
threshold
| level ≤ threshold → liftIO $ do
t ← getTime Realtime
writeWithLogPolicy $!! LogMessage
{ _logMsg = msg
, _logMsgLevel = level
, _logMsgScope = _loggerScope
, _logMsgTime = t
}
| otherwise → return ()
where
writeWithLogPolicy lmsg
| _loggerPolicy ≡ LogPolicyBlock = void $ writeQueue _loggerQueue lmsg
| otherwise = tryWriteQueue _loggerQueue lmsg ≫= \case
Just True → return ()
Just False → return ()
Nothing
| _loggerPolicy ≡ LogPolicyDiscard → atomically $ modifyTVar' _loggerMissed succ
| _loggerPolicy ≡ LogPolicyRaise → throwIO $ QueueFullException lmsg
| otherwise → return ()
instance LoggerCtx (Logger a) a where
loggerFunIO = loggCtx
setLoggerLevel = loggerThreshold
setLoggerScope = loggerScope
setLoggerPolicy = loggerPolicy
type LoggerT a = LoggerCtxT (Logger a)
runLoggerT ∷ LoggerT a m α → Logger a → m α
runLoggerT = runLoggerCtxT
runLogT
∷ (MonadBaseControl IO m, MonadIO m)
⇒ LoggerConfig
→ LoggerBackend msg
→ LoggerT msg m α
→ m α
runLogT config backend = withLogger config backend ∘ runLoggerT