{-# 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