{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE NoImplicitPrelude #-} {- | Module : System.Log.Logger Copyright : Copyright (C) 2004-2011 John Goerzen License : BSD3 Maintainer : John Goerzen Stability : provisional Portability: portable Haskell Logging Framework, Primary Interface Written by John Goerzen, jgoerzen\@complete.org This module is a modification of "System.Log.Logger" of 'hslogger' library. Unless proper description is written here, please use the original documentation available on hackage/hslogger. -} module System.Wlog.Logger ( -- * Basic Types Logger -- ** Re-Exported from System.Wlog , Severity(..) -- * Logging Messages -- ** Basic , logM , logMCond -- ** Utility Functions , debugM, infoM, noticeM, warningM, errorM , removeAllHandlers , traplogging -- ** Logging to a particular Logger by object , logL , logLCond -- * Logger Manipulation -- ** Finding ∨ Creating Loggers , getLogger, getRootLogger, rootLoggerName -- ** Modifying Loggers , addHandler, removeHandler, setHandlers , getLevel, setLevel, clearLevel -- ** Saving Your Changes , saveGlobalLogger , updateGlobalLogger ) where import Control.Concurrent.MVar (modifyMVar, modifyMVar_) import Data.List (isPrefixOf) import qualified Data.Map as Map import Data.Maybe (fromJust) --import System.IO import System.IO.Unsafe (unsafePerformIO) import System.Wlog.Handler (LogHandler (getTag), close) import qualified System.Wlog.Handler (handle) import System.Wlog.Handler.Simple (streamHandler) import System.Wlog.Severity (LogRecord, Severity (..)) import Universum --------------------------------------------------------------------------- -- Basic logger types --------------------------------------------------------------------------- data HandlerT = forall a. LogHandler a => HandlerT a data Logger = Logger { level :: Maybe Severity , handlers :: [HandlerT] , name :: String } type LogTree = Map.Map String Logger --------------------------------------------------------------------------- -- Utilities --------------------------------------------------------------------------- -- | The name of the root logger, which is always defined and present -- on the system. rootLoggerName :: String rootLoggerName = "" --------------------------------------------------------------------------- -- Logger Tree Storage --------------------------------------------------------------------------- -- | The log tree. Initialize it with a default root logger -- and (FIXME) a logger for MissingH itself. {-# NOINLINE logTree #-} logTree :: MVar LogTree -- note: only kick up tree if handled locally logTree = unsafePerformIO $ do h <- streamHandler stderr Debug newMVar $ Map.singleton rootLoggerName $ Logger {level = Just Warning, name = "", handlers = [HandlerT h]} {- | Given a name, return all components of it, starting from the root. Example return value: >["", "MissingH", "System.Cmd.Utils", "System.Cmd.Utils.pOpen"] -} componentsOfName :: String -> [String] componentsOfName name = let joinComp [] _ = [] joinComp (x:xs) [] = x : joinComp xs x joinComp (x:xs) accum = let newlevel = accum ++ "." ++ x in newlevel : joinComp xs newlevel in rootLoggerName : joinComp (split "." name) [] --------------------------------------------------------------------------- -- Logging With Location --------------------------------------------------------------------------- -- | Log a message using the given logger at a given priority. logM :: String -- ^ Name of the logger to use -> Severity -- ^ Severity of this message -> Text -- ^ The log text itself -> IO () logM logname pri msg = do l <- getLogger logname logL l pri msg logMCond :: String -> Severity -> Text -> (String -> Bool) -> IO () logMCond logname sev msg cond = do l <- getLogger logname logLCond l sev msg cond --------------------------------------------------------------------------- -- Utility functions --------------------------------------------------------------------------- {- | Log a message at 'Debug' priority -} debugM :: String -- ^ Logger name -> Text -- ^ Log message -> IO () debugM s = logM s Debug {- | Log a message at 'Info' priority -} infoM :: String -- ^ Logger name -> Text -- ^ Log message -> IO () infoM s = logM s Info {- | Log a message at 'Notice' priority -} noticeM :: String -- ^ Logger name -> Text -- ^ Log message -> IO () noticeM s = logM s Notice {- | Log a message at 'Warning' priority -} warningM :: String -- ^ Logger name -> Text -- ^ Log message -> IO () warningM s = logM s Warning {- | Log a message at 'ERROR' priority -} errorM :: String -- ^ Logger name -> Text -- ^ Log message -> IO () errorM s = logM s Error --------------------------------------------------------------------------- -- Public Logger Interaction Support --------------------------------------------------------------------------- -- | Returns the logger for the given name. If no logger with that name -- exists, creates new loggers and any necessary parent loggers, with -- no connected handlers. getLogger :: String -> IO Logger getLogger lname = modifyMVar logTree $ \lt -> case Map.lookup lname lt of Just x -> return (lt, x) -- A logger exists; return it and leave tree Nothing -> do -- Add logger(s). Then call myself to retrieve it. let newlt = createLoggers (componentsOfName lname) lt let result = fromJust $ Map.lookup lname newlt return (newlt, result) where createLoggers :: [String] -> LogTree -> LogTree createLoggers [] lt = lt -- No names to add; return tree unmodified createLoggers (x:xs) lt = -- Add logger to tree if Map.member x lt then createLoggers xs lt else createLoggers xs (Map.insert x (defaultLogger {name=x}) lt) defaultLogger = Logger Nothing [] undefined -- ???!??! -- | Returns the root logger. getRootLogger :: IO Logger getRootLogger = getLogger rootLoggerName -- | Log a message, assuming the current logger's level permits it. logL :: Logger -> Severity -> Text -> IO () logL l pri msg = handle l (pri, msg) (const True) -- | Logs a message with condition. logLCond :: Logger -> Severity -> Text -> (String -> Bool) -> IO () logLCond l pri msg = handle l (pri, msg) -- | Handle a log request. handle :: Logger -> LogRecord -> (String -> Bool) -> IO () handle l lrecord@(sev, _) handlerFilter = do lp <- getLoggerSeverity nm if sev >= lp then do ph <- concatMap handlers <$> parentLoggers nm forM_ ph $ callHandler lrecord nm else return () where nm = name l parentLoggers :: String -> IO [Logger] parentLoggers = mapM getLogger . componentsOfName -- Get the severity we should use. Find the first logger in the -- tree, starting here, with a set severity. If even root doesn't -- have one, assume "Debug". getLoggerSeverity :: String -> IO Severity getLoggerSeverity name = do pl <- parentLoggers name case catMaybes . map level $ (l : pl) of [] -> pure Debug (x:_) -> pure x callHandler :: LogRecord -> String -> HandlerT -> IO () callHandler lr loggername (HandlerT x) = when (handlerFilter $ getTag x) $ System.Wlog.Handler.handle x lr loggername -- | Add handler to 'Logger'. Returns a new 'Logger'. addHandler :: LogHandler a => a -> Logger -> Logger addHandler h l= l{handlers = (HandlerT h) : (handlers l)} -- | Remove a handler from the 'Logger'. Handlers are removed in the reverse -- order they were added, so the following property holds for any 'LogHandler' -- @h@: -- -- > removeHandler . addHandler h = id -- -- If no handlers are associated with the 'Logger', it is returned unchanged. -- -- The root logger's default handler that writes every message to stderr can -- be removed by using this function before any handlers have been added -- to the root logger: -- -- > updateGlobalLogger rootLoggerName removeHandler removeHandler :: Logger -> Logger removeHandler l = l { handlers = drop 1 $ handlers l } -- | Set the 'Logger'\'s list of handlers to the list supplied. -- All existing handlers are removed first. setHandlers :: LogHandler a => [a] -> Logger -> Logger setHandlers hl l = l{handlers = map (\h -> HandlerT h) hl} -- | Returns the "level" of the logger. Items beneath this -- level will be ignored. getLevel :: Logger -> Maybe Severity getLevel = level -- | Sets the "level" of the 'Logger'. Returns a new -- 'Logger' object with the new level. setLevel :: Severity -> Logger -> Logger setLevel p l = l {level = Just p} -- | Clears the "level" of the 'Logger'. It will now inherit the level of -- | its parent. clearLevel :: Logger -> Logger clearLevel l = l {level = Nothing} -- | Updates the global record for the given logger to take into -- account any changes you may have made. saveGlobalLogger :: Logger -> IO () saveGlobalLogger l = modifyMVar_ logTree (\lt -> return $ Map.insert (name l) l lt) -- | Helps you make changes on the given logger. Takes a function -- that makes changes and writes those changes back to the global -- database. Here's an example from above (\"s\" is a 'LogHandler'): -- -- > updateGlobalLogger "MyApp.BuggyComponent" -- > (setLevel DEBUG . setHandlers [s]) updateGlobalLogger :: String -- ^ Logger name -> (Logger -> Logger) -- ^ Function to call -> IO () updateGlobalLogger ln func = do l <- getLogger ln saveGlobalLogger (func l) -- | Allow graceful shutdown. Release all opened files/handlers/etc. removeAllHandlers :: IO () removeAllHandlers = modifyMVar_ logTree $ \lt -> do let allHandlers = Map.fold (\l r -> concat [r, handlers l]) [] lt mapM_ (\(HandlerT h) -> close h) allHandlers return $ Map.map (\l -> l {handlers = []}) lt -- | Traps exceptions that may occur, logging them, then passing them on. -- -- Takes a logger name, priority, leading description text (you can set it to -- @\"\"@ if you don't want any), and action to run. traplogging :: String -- ^ Logger name -> Severity -- ^ Logging priority -> Text -- ^ Descriptive text to prepend to logged messages -> IO a -- ^ Action to run -> IO a -- ^ Return value traplogging logger priority desc action = action `catch` handler where realdesc = case desc of "" -> "" x -> x <> ": " handler :: SomeException -> IO a handler e = do logM logger priority (realdesc <> show e) throwM e -- Re-raise it -- | This function pulled in from MissingH to avoid a dep on it split :: Eq a => [a] -> [a] -> [[a]] split _ [] = [] split delim str = let (firstline, remainder) = breakList (isPrefixOf delim) str in firstline : case remainder of [] -> [] x | x == delim -> [] : [] | otherwise -> split delim (drop (length delim) x) -- This function also pulled from MissingH breakList :: ([a] -> Bool) -> [a] -> ([a], [a]) breakList func = spanList (not . func) -- This function also pulled from MissingH spanList :: ([a] -> Bool) -> [a] -> ([a], [a]) spanList _ [] = ([],[]) spanList func l@(x:xs) = let (ys, zs) = spanList func xs in if func l then (x : ys, zs) else ([], l)