{-# LANGUAGE OverloadedStrings, TemplateHaskell, RankNTypes, TypeFamilies #-}
module System.Log.Simple.Base (
Level(..), level, level_,
Component(..), Scope(..),
Message(..),
Converter, Consumer, consumer,
LogHandler, handler,
LogConfig(..), defCfg, logCfg, componentCfg, componentLevel,
Log(..),
newLog, rootLog, getLog, subLog, getLogConfig, updateLogConfig, updateLogHandlers, writeLog, stopLog,
) where
import Prelude.Unicode
import Control.Applicative
import Control.Arrow
import qualified Control.Exception as E
import Control.Concurrent
import qualified Control.Concurrent.Async as A
import Control.DeepSeq
import Control.Monad
import Control.Monad.Cont
import Data.Default
import Data.Function (fix)
import Data.Map (Map)
import qualified Data.Map as M
import Data.Maybe (fromMaybe)
import Data.Semigroup (Semigroup(..))
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time
import Data.String
import Lens.Micro.Platform
import Lens.Micro.Internal
import Text.Format
splitBy ∷ Char → Text → [Text]
splitBy _ "" = []
splitBy ch t = T.split (≡ ch) t
data Level = Trace | Debug | Info | Warning | Error | Fatal
deriving (Eq, Ord, Read, Show, Enum, Bounded)
instance Default Level where
def = Trace
instance Formattable Level where
formattable Trace _ = "TRACE" `withFlags` ["gray"]
formattable Debug _ = "DEBUG" `withFlags` ["yellow"]
formattable Info _ = "INFO" `withFlags` ["blue"]
formattable Warning _ = "WARN" `withFlags` ["darkyellow"]
formattable Error _ = "ERROR" `withFlags` ["red"]
formattable Fatal _ = "FATAL" `withFlags` ["bg=red"]
newtype Component = Component { componentPath ∷ [Text] } deriving (Eq, Ord)
instance Show Component where
show = T.unpack ∘ T.intercalate "." ∘ reverse ∘ componentPath
instance Formattable Component
instance Read Component where
readsPrec _ = return ∘ flip (,) "" ∘ Component ∘ reverse ∘ splitBy '.' ∘ T.pack
instance IsString Component where
fromString = read
instance Semigroup Component where
Component l <> Component r = Component $ r ++ l
instance Monoid Component where
mempty = Component []
Component l `mappend` Component r = Component $ r ++ l
instance NFData Component where
rnf (Component cs) = rnf cs
newtype Scope = Scope { scopePath ∷ [Text] } deriving (Eq, Ord)
instance Show Scope where
show = T.unpack ∘ T.intercalate "/" ∘ reverse ∘ scopePath
instance Formattable Scope
instance Read Scope where
readsPrec _ = return ∘ flip (,) "" ∘ Scope ∘ reverse ∘ splitBy '/' ∘ T.pack
instance IsString Scope where
fromString = read
instance Semigroup Scope where
Scope l <> Scope r = Scope $ r ++ l
instance Monoid Scope where
mempty = Scope []
Scope l `mappend` Scope r = Scope $ r ++ l
instance NFData Scope where
rnf (Scope s) = rnf s
class HasParent a where
getParent ∷ a → Maybe a
instance HasParent Component where
getParent (Component []) = Nothing
getParent (Component (_:cs)) = Just $ Component cs
instance HasParent Scope where
getParent (Scope []) = Nothing
getParent (Scope (_:ps)) = Just $ Scope ps
level ∷ Text → Maybe Level
level = flip M.lookup levels ∘ T.toLower where
levels = M.fromList [(T.toLower ∘ T.pack ∘ show $ l', l') | l' ← [minBound .. maxBound]]
level_ ∷ Text → Level
level_ t = fromMaybe (error errMsg) ∘ level $ t where
errMsg = "invalid level: " ++ T.unpack t
data Message = Message {
messageTime ∷ ZonedTime,
messageLevel ∷ Level,
messageComponent ∷ Component,
messageScope ∷ Scope,
messageText ∷ Text }
deriving (Read, Show)
instance NFData Message where
rnf (Message t l c s m) = t `seq` l `seq` rnf c `seq` rnf s `seq` rnf m
type Converter a = Message → a
type Consumer a = ContT () IO (a → IO ())
consumer ∷ (((a → IO ()) → IO ()) → IO ()) → Consumer a
consumer = ContT
type LogHandler = Consumer Message
handler ∷ Converter a → Consumer a → Consumer Message
handler conv = fmap (∘ conv)
data LogConfig = LogConfig {
_logConfigMap ∷ Map Component Level }
makeLenses ''LogConfig
instance Default LogConfig where
def = LogConfig mempty
instance Show LogConfig where
show (LogConfig cfg) = unlines [show comp ++ ":" ++ show lev | (comp, lev) ← M.toList cfg]
type instance Index LogConfig = Component
type instance IxValue LogConfig = Level
instance Ixed LogConfig where
ix n = logConfigMap ∘ ix n
instance At LogConfig where
at n = logConfigMap ∘ at n
defCfg ∷ LogConfig
defCfg = def
logCfg ∷ [(Component, Level)] → LogConfig
logCfg = LogConfig ∘ M.fromList
componentCfg ∷ Component → Lens' LogConfig (Maybe Level)
componentCfg comp = logConfigMap . at comp
componentLevel ∷ LogConfig → Component → Level
componentLevel cfg comp = fromMaybe def $ (cfg ^. componentCfg comp) <|> (componentLevel cfg <$> getParent comp)
data Log = Log {
logComponent ∷ Component,
logScope ∷ Scope,
logPost ∷ Message → IO (),
logStop ∷ IO (),
logConfig ∷ MVar LogConfig,
logHandlers ∷ MVar [LogHandler],
logRestartHandlers ∷ IO () }
type FChan a = Chan (Maybe a)
writeFChan ∷ FChan a → a → IO ()
writeFChan ch = writeChan ch ∘ Just
stopFChan ∷ FChan a → IO ()
stopFChan ch = writeChan ch Nothing
newLog ∷ LogConfig → [LogHandler] → IO Log
newLog cfg handlers = do
ch ← newChan ∷ IO (FChan Message)
cfgVar ← newMVar cfg
handlersVar ← newMVar handlers
handlersThread ← newEmptyMVar
let
passMessage ∷ (Message → IO ()) → Message → IO ()
passMessage fn msg = do
cfg' ← readMVar cfgVar
when (componentLevel cfg' (messageComponent msg) ≤ messageLevel msg) $ fn msg
fatalMsg ∷ String → IO Message
fatalMsg s = do
tm ← getZonedTime
return $ Message tm Fatal "*" "" $ fromString s
tryLog ∷ (Message → IO ()) → Message → IO ()
tryLog logMsg m = E.handle onError (m `deepseq` logMsg m) where
onError ∷ E.SomeException → IO ()
onError e = E.handle ignoreError $ fatalMsg ("Exception during logging message: " ++ show e) >>= logMsg
ignoreError ∷ E.SomeException → IO ()
ignoreError _ = return ()
runHandlers ∷ FChan Message → [LogHandler] → ContT () IO ()
runHandlers inCh hs = do
hs' ← sequence $ map (fmap tryLog) hs
fix $ \loop' → do
msg ← liftIO $ readChan inCh
case msg of
Just msg' → liftIO (mapM_ ($ msg') hs') >> loop'
Nothing → return ()
startHandlers ∷ IO (A.Async ())
startHandlers = readMVar handlersVar >>= A.async ∘ flip runContT return ∘ runHandlers ch
restartHandlers ∷ IO ()
restartHandlers = modifyMVar_ handlersThread $ \th → A.cancel th >> startHandlers
writeMessage ∷ Message → IO ()
writeMessage = passMessage (writeFChan ch)
waitHandlers ∷ IO ()
waitHandlers = readMVar handlersThread >>= A.wait
startHandlers >>= putMVar handlersThread
return $ Log {
logComponent = mempty,
logScope = mempty,
logPost = writeMessage,
logStop = stopFChan ch >> waitHandlers,
logConfig = cfgVar,
logHandlers = handlersVar,
logRestartHandlers = restartHandlers }
rootLog ∷ Log → Log
rootLog l = l { logComponent = mempty, logScope = mempty }
getLog ∷ Component → Scope → Log → Log
getLog comp scope l = l { logComponent = comp, logScope = scope }
subLog ∷ Component → Scope → Log → Log
subLog comp scope l = l {
logComponent = logComponent l `mappend` comp,
logScope = logScope l `mappend` scope }
getLogConfig ∷ MonadIO m ⇒ Log → m LogConfig
getLogConfig l = liftIO $ readMVar (logConfig l)
updateLogConfig ∷ MonadIO m ⇒ Log → (LogConfig → LogConfig) → m LogConfig
updateLogConfig l update = liftIO $ modifyMVar (logConfig l) (return ∘ (update &&& id))
updateLogHandlers ∷ MonadIO m ⇒ Log → ([LogHandler] → [LogHandler]) → m ()
updateLogHandlers l update = liftIO $ modifyMVar_ (logHandlers l) (return ∘ update) >> logRestartHandlers l
writeLog ∷ MonadIO m ⇒ Log → Level → Text → m ()
writeLog l lev msg = liftIO $ do
tm ← getZonedTime
logPost l $ Message tm lev (logComponent l) (logScope l) msg
stopLog ∷ MonadIO m ⇒ Log → m ()
stopLog = liftIO ∘ logStop