heavy-logger 0.3.0.0 → 0.3.1.0
raw patch · 9 files changed
+284/−90 lines, 9 filesdep +stm
Dependencies added: stm
Files
- ChangeLog.md +5/−0
- README.md +3/−0
- heavy-logger.cabal +5/−2
- src/System/Log/Heavy.hs +12/−5
- src/System/Log/Heavy/Backends.hs +12/−79
- src/System/Log/Heavy/Backends/Dynamic.hs +156/−0
- src/System/Log/Heavy/Shortcuts.hs +2/−2
- src/System/Log/Heavy/Types.hs +2/−2
- src/System/Log/Heavy/Util.hs +87/−0
ChangeLog.md view
@@ -23,3 +23,8 @@ * Added the IO module. * Introduced null logging backend. +## 0.3.1.0 -- 2017-10-30++* Introduced Dynamic backend, which allows to change backend or its settings in runtime.+* Introduced FilteringM backend, which allows to change filtering settings in runtime.+
README.md view
@@ -19,6 +19,9 @@ * Possiblity to write messages to several backends in parallel. * Logging backend settings can be defined dynamically; it is not necessary to hardcode which backend you will use, you can load settings in runtime.+* It is possible to change backend or it's settings in runtime (more precisely,+ you can change underlying backend if you use `DynamicBackend` as a backend, + or you can change backend's filter if you use `FilteringM` as a backend). * Sane default set of logging message severity levels and possibility to define custom severity levels. * Logging context stacks support (aka mapped diagnostic contexts, MDC). Each
heavy-logger.cabal view
@@ -1,5 +1,5 @@ name: heavy-logger-version: 0.3.0.0+version: 0.3.1.0 synopsis: Full-weight logging based on fast-logger description: This is Haskell logging library, which prefers functionality and extendability over light weight and simplicity. It can use fast-logger as backend, and is compatible @@ -20,9 +20,11 @@ exposed-modules: System.Log.Heavy System.Log.Heavy.Level System.Log.Heavy.Types+ System.Log.Heavy.Util System.Log.Heavy.LoggingT System.Log.Heavy.Format System.Log.Heavy.Backends+ System.Log.Heavy.Backends.Dynamic System.Log.Heavy.Shortcuts System.Log.Heavy.TH System.Log.Heavy.IO@@ -45,7 +47,8 @@ monad-logger >= 0.3.22, hsyslog >= 5, text-format-heavy >= 0.1.5.0,- thread-local-storage >= 0.1.1+ thread-local-storage >= 0.1.1,+ stm >= 2.4 hs-source-dirs: src default-language: Haskell2010 ghc-options: -fwarn-unused-imports
src/System/Log/Heavy.hs view
@@ -64,8 +64,13 @@ module System.Log.Heavy.Level, module System.Log.Heavy.LoggingT, module System.Log.Heavy.Backends,+ module System.Log.Heavy.Backends.Dynamic,+ -- * Logging functions+ logMessage,+ -- * Run actions with logging withLogging, withLoggingF, withLoggingT,- isLevelEnabledByBackend, isLevelEnabled,+ -- * Check current settings+ isLevelEnabledByBackend, isLevelEnabled ) where import Control.Monad.Trans@@ -75,8 +80,10 @@ import System.Log.Heavy.Types import System.Log.Heavy.Level+import System.Log.Heavy.Util import System.Log.Heavy.LoggingT import System.Log.Heavy.Backends+import System.Log.Heavy.Backends.Dynamic -- | Execute actions with logging backend. -- This is mostly an utility function to be used to construct custom @@ -119,11 +126,11 @@ -- This function assumes that if some events filtering is enabled by the -- backend, it does not depend on message text, only on source and -- severity level.-isLevelEnabledByBackend :: forall m. (Monad m, HasLogBackend AnyLogBackend m) => LogSource -> Level -> m Bool+isLevelEnabledByBackend :: forall m. (Monad m, MonadIO m, HasLogBackend AnyLogBackend m) => LogSource -> Level -> m Bool isLevelEnabledByBackend src level = do backend <- getLogBackend :: m AnyLogBackend let msg = LogMessage level src undefined TL.empty () []- return $ wouldWriteMessage backend msg+ liftIO $ wouldWriteMessage backend msg -- | Check if logging of events of specified level from specified source -- is enabled by both context and backend filter.@@ -131,11 +138,11 @@ -- This function assumes that if some events filtering is enabled by the -- backend, it does not depend on message text, only on source and -- severity level.-isLevelEnabled :: forall m. (Monad m, HasLogBackend AnyLogBackend m, HasLogContext m) => LogSource -> Level -> m Bool+isLevelEnabled :: forall m. (Monad m, MonadIO m, HasLogBackend AnyLogBackend m, HasLogContext m) => LogSource -> Level -> m Bool isLevelEnabled src level = do let msg = LogMessage level src undefined TL.empty () [] backend <- getLogBackend :: m AnyLogBackend- let isEnabledByBackend = wouldWriteMessage backend msg+ isEnabledByBackend <- liftIO $ wouldWriteMessage backend msg isEnabledByContext <- checkContextFilterM msg return $ isEnabledByContext && isEnabledByBackend
src/System/Log/Heavy/Backends.hs view
@@ -18,18 +18,12 @@ defStderrSettings, defFileSettings, defaultSyslogSettings,- defaultSyslogFormat,- -- * Utilities for other backends implementation- checkLogLevel, checkLogLevel',- checkContextFilter, checkContextFilter', checkContextFilterM,- logMessage+ defaultSyslogFormat ) where import Control.Monad import Control.Monad.Trans (liftIO)-import Control.Monad.Reader import Control.Concurrent-import Data.List (isPrefixOf) import qualified Data.ByteString.Unsafe as BSU import qualified Data.Text.Format.Heavy as F import qualified System.Posix.Syslog as Syslog@@ -40,6 +34,7 @@ import System.Log.Heavy.Types import System.Log.Heavy.Level import System.Log.Heavy.Format+import System.Log.Heavy.Util -- $description --@@ -62,8 +57,12 @@ -- * Filtering - passes messages, that match specified filter, -- to underlying backend. --+-- * FilteringM - similar to Filtering, but allows to change the filter in runtime.+-- -- * Parallel - writes messages to several backends in parallel. --+-- * Dynamic - allows to change underlying backend or its settings in runtime.+-- -- | Default settings for fast-logger stdout output defStdoutSettings :: LogBackendSettings FastLoggerBackend@@ -177,8 +176,9 @@ instance IsLogBackend ParallelBackend where data LogBackendSettings ParallelBackend = ParallelLogSettings [LoggingSettings] - wouldWriteMessage (ParallelBackend list) msg =- or [wouldWriteMessage backend msg | backend <- list]+ wouldWriteMessage (ParallelBackend list) msg = do+ results <- sequence [wouldWriteMessage backend msg | backend <- list]+ return $ or results makeLogger (ParallelBackend list) msg = forM_ list $ \(AnyLogBackend backend) -> makeLogger backend msg@@ -210,7 +210,8 @@ instance IsLogBackend b => IsLogBackend (Filtering b) where data LogBackendSettings (Filtering b) = Filtering (LogMessage -> Bool) (LogBackendSettings b) - wouldWriteMessage (FilteringBackend fltr _) msg = fltr msg+ wouldWriteMessage (FilteringBackend fltr _) msg = do+ return $ fltr msg makeLogger (FilteringBackend fltr backend) msg = do when (fltr msg) $ do@@ -230,79 +231,11 @@ instance IsLogBackend NullBackend where data LogBackendSettings NullBackend = NullLogSettings - wouldWriteMessage _ _ = False+ wouldWriteMessage _ _ = return False makeLogger _ _ = return () initLogBackend _ = return NullBackend cleanupLogBackend _ = return ()---- | Check if message level matches given filter.-checkLogLevel :: LogFilter -> LogMessage -> Bool-checkLogLevel fltr m =- checkLogLevel' fltr (lmSource m) (lmLevel m)---- | Check if message level matches given filter.-checkLogLevel' :: LogFilter -> LogSource -> Level -> Bool-checkLogLevel' fltr source level =- case lookup (bestMatch source (map fst fltr)) fltr of- Nothing -> False- Just min -> level <= min- where- bestMatch :: LogSource -> [LogSource] -> LogSource- bestMatch src list = go [] src list-- go best src [] = best- go best src (x:xs)- | src == x = x- | x `isPrefixOf` src && length x > length best = go x src xs- | otherwise = go best src xs---- | Check if message source and level passes specified filters.------ The message is passed if:------ * No @include@ filters are defined in context stack, OR the message conforms to ANY of @include@ filters;------ * AND the message does not conform to any of @exclude@ filters in the stack.----checkContextFilter' :: [LogContextFilter] -> LogSource -> Level -> Bool-checkContextFilter' filters source level =- let includeFilters = [fltr | LogContextFilter (Just fltr) _ <- filters]- excludeFilters = [fltr | LogContextFilter _ (Just fltr) <- filters]- includeOk = null includeFilters || or [checkLogLevel' fltr source level | fltr <- includeFilters]- excludeOk = or [checkLogLevel' fltr source level | fltr <- excludeFilters]- in includeOk && not excludeOk---- | Check if message matches filters from logging context.------ The message is passed if:------ * No @include@ filters are defined in context stack, OR the message conforms to ANY of @include@ filters;------ * AND the message does not conform to any of @exclude@ filters in the stack.----checkContextFilter :: LogContext -> LogMessage -> Bool-checkContextFilter context msg =- checkContextFilter' (map lcfFilter context) (lmSource msg) (lmLevel msg)---- | Check if message matches filters from logging context.--- This function is similar to @checkContextFilter@, but uses current context--- from monadic state.-checkContextFilterM :: HasLogContext m => LogMessage -> m Bool-checkContextFilterM msg = do- context <- getLogContext- return $ checkContextFilter context msg---- | Log a message. This will add current context to context specified--- in the message.--- This function checks current context filter.-logMessage :: forall m. (HasLogging m, MonadIO m) => LogMessage -> m ()-logMessage msg = do- ok <- checkContextFilterM msg- when ok $ do- context <- getLogContext- logger <- getLogger- liftIO $ logger $ msg {lmContext = context ++ lmContext msg}
+ src/System/Log/Heavy/Backends/Dynamic.hs view
@@ -0,0 +1,156 @@+{-# LANGUAGE OverloadedStrings, TypeSynonymInstances, FlexibleInstances, ExistentialQuantification, TypeFamilies, GeneralizedNewtypeDeriving, StandaloneDeriving, MultiParamTypeClasses, UndecidableInstances #-}+-- | This module incldues logging backend combinators that allow to change+-- underlying backend or it's settings in runtime.+module System.Log.Heavy.Backends.Dynamic+ (+ -- * Dynamic backend+ DynamicBackend,+ DynamicBackendHandle,+ newDynamicBackendHandle,+ updateDynamicBackendSettings,+ LogBackendSettings (..),+ -- * Dynamic filtering+ FilteringM, filteringM, excludingM+ ) where ++import Control.Monad (when)+import Control.Concurrent+import Control.Concurrent.STM++import System.Log.Heavy.Types+import System.Log.Heavy.Util++-- | Abstract handle, that is used to control @DynamicBackend@.+data DynamicBackendHandle = DynamicBackendHandle {+ dbhBroadcast :: TChan LoggingSettings -- ^ Broadcast TChan. It is write-only.+ , dbhDefault :: LoggingSettings -- ^ Initial logging settings. Used only before first update in TChan.+ -- We cannot put this into TChan at beginning, because after dupTChan+ -- newly created TChan's will be empty anyway.+ }+-- I put these data into special Handle type, to hide the implementation detail+-- that it uses TChan inside. This detail can be changed in later versions.++-- | Create an instance of @DynamicBackendHandle@.+newDynamicBackendHandle :: LoggingSettings -- ^ Initial logging settings. This can be changed later with @updateDynamicBackendSettings@.+ -> IO DynamicBackendHandle+newDynamicBackendHandle settings = do+ broadcast <- newBroadcastTChanIO+ return $ DynamicBackendHandle broadcast settings++-- | Update settings of @DynamicBackend@, which was created by provided handle.+updateDynamicBackendSettings :: DynamicBackendHandle -- ^ Handle of @DynamicBackend@.+ -> LoggingSettings -- ^ New logging settings.+ -> IO ()+updateDynamicBackendSettings handle settings = do+ atomically $ writeTChan (dbhBroadcast handle) settings++-- | Dynamic logging backend allows to change logging backend or it's settings+-- in runtime. When it sees new backend settings, it deinitializes old backend+-- and initializes new one.+--+-- How to use it:+--+-- * Before creating @DynamicSettings@, you have to select some initial+-- @LoggingSettings@ and create @DynamicBackendHandle@ with it.+-- * When you decide that you want to use new backend settings, call+-- @updateDynamicBackendSettings@ on existing @DynamicBackendHandle@.+-- @DynamicBackend@ will use new settings for the next logging function call.+-- * It is responsibility of caller code to do not change backends too frequently;+-- for example, if you are checking your config file for updates of logging+-- settings each 10s, you have to check that settings actually changed since+-- last time.+-- +-- It is possible to create one instance of @DynamicBackendHandle@ and pass it+-- to multiple threads.+--+data DynamicBackend = DynamicBackend {+ dbCurrentBackend :: MVar AnyLogBackend -- ^ Currently used logging backend+ , dbNewSettings :: TChan LoggingSettings -- ^ TChan with updates of settings. This is read only.+ }++-- Implementation is based on idea that Backend instances theirself are thread-safe;+-- i.e. it is safe to create several instances of any IsLogBackend data type in several threads+-- and they will coexist without problems (they do not use any shared state).+-- So, we can have one TChan, in which we can push updated settings when we want;+-- Each instance of DynamicBackend will have its own instance of underlying backend.+instance IsLogBackend DynamicBackend where+ data LogBackendSettings DynamicBackend = DynamicSettings DynamicBackendHandle++ initLogBackend (DynamicSettings (DynamicBackendHandle broadcast (LoggingSettings dfltSettings))) = do+ -- Duplicate broadcast TChan - create "own copy".+ mySettingsChan <- atomically $ dupTChan broadcast+ -- Initialize default backend+ backend <- initLogBackend dfltSettings+ backendVar <- newMVar (AnyLogBackend backend)+ return $ DynamicBackend backendVar mySettingsChan++ cleanupLogBackend (DynamicBackend backendVar _) = do+ -- Cleanup currently used backend+ backend <- takeMVar backendVar+ cleanupLogBackend backend++ wouldWriteMessage (DynamicBackend backendVar _) msg = do+ -- Delegate the call to currently used backend.+ backend <- readMVar backendVar+ wouldWriteMessage backend msg++ makeLogger (DynamicBackend backendVar settingsChan) msg = do+ -- See if there is an update of settings queued.+ mbNewSettings <- atomically $ tryReadTChan settingsChan+ -- Note: tryReadTChan will remove element from channel, if there is any.+ -- But, it will affect only "our own copy" of TChan. If there are other+ -- instances of DynamicBackend living in other threads with the same+ -- Handle, they will still see settings update in their own copies of+ -- TChan.+ case mbNewSettings of+ Nothing -> do+ -- No updates. Use current backend.+ backend <- readMVar backendVar+ makeLogger backend msg+ Just (LoggingSettings newSettings) -> do+ -- New settings came. We have to switch backends.+ oldBackend <- takeMVar backendVar+ -- Cleanup old backend+ cleanupLogBackend oldBackend+ -- Initialize new backend+ newBackend <- initLogBackend newSettings+ putMVar backendVar (AnyLogBackend newBackend)+ -- Delegate call to new backend+ makeLogger newBackend msg++-- | Filtering backend with mutable filter.+data FilteringM b = FilteringBackendM (MVar (LogMessage -> Bool)) b++-- | Specify filter as @LogFilter@. This filter can be changed later.+filteringM :: IsLogBackend b => LogFilter -> LogBackendSettings b -> IO (LogBackendSettings (FilteringM b))+filteringM fltr b = do+ fltrVar <- newMVar (checkLogLevel fltr)+ return $ FilteringM fltrVar b++-- | Exclude messages by filter. This filter can be changed later.+excludingM :: IsLogBackend b => LogFilter -> LogBackendSettings b -> IO (LogBackendSettings (FilteringM b))+excludingM fltr b = do+ fltrVar <- newMVar ex+ return $ FilteringM fltrVar b+ where+ ex msg = not $ checkContextFilter' [LogContextFilter Nothing (Just fltr)] (lmSource msg) (lmLevel msg)++instance IsLogBackend b => IsLogBackend (FilteringM b) where+ data LogBackendSettings (FilteringM b) =+ FilteringM (MVar (LogMessage -> Bool)) (LogBackendSettings b)++ wouldWriteMessage (FilteringBackendM fltrVar _) msg = do+ fltr <- readMVar fltrVar+ return $ fltr msg++ makeLogger (FilteringBackendM fltrVar backend) msg = do+ fltr <- readMVar fltrVar+ when (fltr msg) $ do+ makeLogger backend msg++ initLogBackend (FilteringM fltrVar settings) = do+ backend <- initLogBackend settings+ return $ FilteringBackendM fltrVar backend++ cleanupLogBackend (FilteringBackendM _ backend) = cleanupLogBackend backend+
src/System/Log/Heavy/Shortcuts.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE OverloadedStrings, TypeSynonymInstances, FlexibleInstances, ExistentialQuantification, TypeFamilies, GeneralizedNewtypeDeriving, StandaloneDeriving, MultiParamTypeClasses, UndecidableInstances, ScopedTypeVariables, AllowAmbiguousTypes, FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings, TypeSynonymInstances, FlexibleInstances, ExistentialQuantification, TypeFamilies, GeneralizedNewtypeDeriving, StandaloneDeriving, MultiParamTypeClasses, UndecidableInstances, ScopedTypeVariables, FlexibleContexts #-} -- | This module contains some shortcut functions that can be of use in simple usage cases. module System.Log.Heavy.Shortcuts@@ -15,7 +15,7 @@ import System.Log.Heavy.Types import System.Log.Heavy.Level-import System.Log.Heavy.Backends+import System.Log.Heavy.Util -- | Message stub with Info severity. infoMessage :: F.VarContainer vars => TL.Text -> vars -> LogMessage
src/System/Log/Heavy/Types.hs view
@@ -112,8 +112,8 @@ -- | Should return True if the specified message would be -- actually written to the log. Default implementation -- always returns True.- wouldWriteMessage :: b -> LogMessage -> Bool- wouldWriteMessage _ _ = True+ wouldWriteMessage :: b -> LogMessage -> IO Bool+ wouldWriteMessage _ _ = return True -- | Cleanup logging backend (release resources and so on) cleanupLogBackend :: b -> IO ()
+ src/System/Log/Heavy/Util.hs view
@@ -0,0 +1,87 @@+{-# LANGUAGE OverloadedStrings, TypeSynonymInstances, FlexibleInstances, ExistentialQuantification, TypeFamilies, GeneralizedNewtypeDeriving, StandaloneDeriving, MultiParamTypeClasses, UndecidableInstances, ScopedTypeVariables, FlexibleContexts #-}++module System.Log.Heavy.Util+ (+ -- * Functions of common use+ logMessage,+ -- * Utilities for backends implementation+ checkLogLevel, checkLogLevel',+ checkContextFilter, checkContextFilter', checkContextFilterM+ ) where++import Control.Monad (when)+import Control.Monad.Trans+import Data.List (isPrefixOf)++import System.Log.Heavy.Types+import System.Log.Heavy.Level++-- | Check if message source and level passes specified filters.+--+-- The message is passed if:+--+-- * No @include@ filters are defined in context stack, OR the message conforms to ANY of @include@ filters;+--+-- * AND the message does not conform to any of @exclude@ filters in the stack.+--+checkContextFilter' :: [LogContextFilter] -> LogSource -> Level -> Bool+checkContextFilter' filters source level =+ let includeFilters = [fltr | LogContextFilter (Just fltr) _ <- filters]+ excludeFilters = [fltr | LogContextFilter _ (Just fltr) <- filters]+ includeOk = null includeFilters || or [checkLogLevel' fltr source level | fltr <- includeFilters]+ excludeOk = or [checkLogLevel' fltr source level | fltr <- excludeFilters]+ in includeOk && not excludeOk++-- | Check if message matches filters from logging context.+--+-- The message is passed if:+--+-- * No @include@ filters are defined in context stack, OR the message conforms to ANY of @include@ filters;+--+-- * AND the message does not conform to any of @exclude@ filters in the stack.+--+checkContextFilter :: LogContext -> LogMessage -> Bool+checkContextFilter context msg =+ checkContextFilter' (map lcfFilter context) (lmSource msg) (lmLevel msg)++-- | Check if message matches filters from logging context.+-- This function is similar to @checkContextFilter@, but uses current context+-- from monadic state.+checkContextFilterM :: HasLogContext m => LogMessage -> m Bool+checkContextFilterM msg = do+ context <- getLogContext+ return $ checkContextFilter context msg++-- | Check if message level matches given filter.+checkLogLevel :: LogFilter -> LogMessage -> Bool+checkLogLevel fltr m =+ checkLogLevel' fltr (lmSource m) (lmLevel m)++-- | Check if message level matches given filter.+checkLogLevel' :: LogFilter -> LogSource -> Level -> Bool+checkLogLevel' fltr source level =+ case lookup (bestMatch source (map fst fltr)) fltr of+ Nothing -> False+ Just min -> level <= min+ where+ bestMatch :: LogSource -> [LogSource] -> LogSource+ bestMatch src list = go [] src list++ go :: LogSource -> LogSource -> [LogSource] -> LogSource+ go best src [] = best+ go best src (x:xs)+ | src == x = x+ | (x `isPrefixOf` src) && (length x > length best) = go x src xs+ | otherwise = go best src xs++-- | Log a message. This will add current context to context specified+-- in the message.+-- This function checks current context filter.+logMessage :: forall m. (HasLogging m, MonadIO m) => LogMessage -> m ()+logMessage msg = do+ ok <- checkContextFilterM msg+ when ok $ do+ context <- getLogContext+ logger <- getLogger+ liftIO $ logger $ msg {lmContext = context ++ lmContext msg}+