{-# 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 -- Helper function splitBy ∷ Char → Text → [Text] splitBy _ "" = [] splitBy ch t = T.split (≡ ch) t -- | Level of message 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"] -- | Component — each one have separate log scopes and can have different politics -- Child component's root politics inherits its parent root politics -- Component name parts stored in reverse order 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 -- | Log scope, also stored in reverse order 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 -- | Parse level 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]] -- | Parse level, failing on invalid input level_ ∷ Text → Level level_ t = fromMaybe (error errMsg) ∘ level $ t where errMsg = "invalid level: " ++ T.unpack t -- | Log message 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 -- | Returns function which accepts consumed value type Consumer a = ContT () IO (a → IO ()) -- | Make consumer consumer ∷ (((a → IO ()) → IO ()) → IO ()) → Consumer a consumer = ContT -- | Message handler 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 -- | Default log config — info level defCfg ∷ LogConfig defCfg = def -- | Make log config by list of components and levels logCfg ∷ [(Component, Level)] → LogConfig logCfg = LogConfig ∘ M.fromList -- | Component config level lens componentCfg ∷ Component → Lens' LogConfig (Maybe Level) componentCfg comp = logConfigMap . at comp -- | Get politics for specified component componentLevel ∷ LogConfig → Component → Level componentLevel cfg comp = fromMaybe def $ (cfg ^. componentCfg comp) <|> (componentLevel cfg <$> getParent comp) -- | Log data Log = Log { -- | Current log component logComponent ∷ Component, -- | Current log scope logScope ∷ Scope, -- | Log message, it is low-level function, i.e. it doesn't take into account current component and scope and writes message as is logPost ∷ Message → IO (), -- | Stop log and wait until it writes all logStop ∷ IO (), -- | Log config logConfig ∷ MVar LogConfig, -- | Handlers list logHandlers ∷ MVar [LogHandler], -- | Restart all handlers 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 -- | Create log, returns root logger for root component -- -- Messages from distinct threads and components are splitted in several chans, where they are processed, and then messages combined back and sent to log-thread -- newLog ∷ LogConfig → [LogHandler] → IO Log newLog cfg handlers = do ch ← newChan ∷ IO (FChan Message) cfgVar ← newMVar cfg handlersVar ← newMVar handlers handlersThread ← newEmptyMVar let -- | Pass message firther if it passes config 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 -- | Perform log 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 () -- | Consume messages and send to handlers 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 () -- | Start handlers thread startHandlers ∷ IO (A.Async ()) startHandlers = readMVar handlersVar >>= A.async ∘ flip runContT return ∘ runHandlers ch -- | Restart handlers thread restartHandlers ∷ IO () restartHandlers = modifyMVar_ handlersThread $ \th → A.cancel th >> startHandlers -- | Write message with myThreadId 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 } -- | Get root log, i.e. just drop current component and scope rootLog ∷ Log → Log rootLog l = l { logComponent = mempty, logScope = mempty } -- | Get log for specified component and scope getLog ∷ Component → Scope → Log → Log getLog comp scope l = l { logComponent = comp, logScope = scope } -- | Get sub-log subLog ∷ Component → Scope → Log → Log subLog comp scope l = l { logComponent = logComponent l `mappend` comp, logScope = logScope l `mappend` scope } -- | Read log config getLogConfig ∷ MonadIO m ⇒ Log → m LogConfig getLogConfig l = liftIO $ readMVar (logConfig l) -- | Modify log config updateLogConfig ∷ MonadIO m ⇒ Log → (LogConfig → LogConfig) → m LogConfig updateLogConfig l update = liftIO $ modifyMVar (logConfig l) (return ∘ (update &&& id)) -- | Update log handlers, this restarts handlers thread updateLogHandlers ∷ MonadIO m ⇒ Log → ([LogHandler] → [LogHandler]) → m () updateLogHandlers l update = liftIO $ modifyMVar_ (logHandlers l) (return ∘ update) >> logRestartHandlers l -- | Write message to log for current component and scope 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 -- | Wait log messages and stop log stopLog ∷ MonadIO m ⇒ Log → m () stopLog = liftIO ∘ logStop