{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeInType #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE DataKinds #-}
-- | An extensible effect that wraps 'Control.Monad.Log.MonadLog' into an extensible effect.
module Control.Eff.Log
( handleLogsWith
, Logs(..)
, logMessageFreeEff
, logMsg
, module ExtLog
, LogChannel()
, logChannelPutIO
, forwardLogsToChannel
, forkLogChannel
, joinLogChannel
, logChannelBracket
)
where
import Control.Concurrent
import Control.Concurrent.STM
import Control.Eff as Eff
import Control.Exception ( bracket )
import Control.Monad ( void
, when
)
import Control.Monad.Log as ExtLog
hiding ( )
import Control.Monad.Trans.Control
import Data.Kind
import qualified Control.Eff.Lift as Eff
import qualified Control.Monad.Log as Log
-- | The 'Eff'ect type to wrap 'ExtLog.MonadLog'.
-- This is a
data Logs message a where
LogMessageFree :: (forall n . Monoid n => (message -> n) -> n) -> Logs message ()
-- | Effectful version of the /strange/ 'ExtLog.logMessageFree' function.
logMessageFreeEff
:: Member (Logs message) r
=> (forall n . Monoid n => (message -> n) -> n)
-> Eff r ()
logMessageFreeEff foldMapish = send (LogMessageFree foldMapish)
-- | Effectful version of the 'ExtLog.logMessage' function.
logMsg :: Member (Logs m) r => m -> Eff r ()
logMsg msg = logMessageFreeEff ($ msg)
-- | Handle 'Logs' effects using 'Log.LoggingT' 'Log.Handler's.
handleLogsWith
:: forall m r message a
. (Monad m, SetMember Eff.Lift (Eff.Lift m) r)
=> Eff (Logs message ': r) a
-> (forall b . (Log.Handler m message -> m b) -> m b)
-> Eff r a
handleLogsWith actionThatLogs foldHandler = Eff.handle_relay return
go
actionThatLogs
where
go :: Logs message b -> (b -> Eff r c) -> Eff r c
go (LogMessageFree foldMapish) k =
Eff.lift (foldHandler (Log.runLoggingT (Log.logMessageFree foldMapish)))
>>= k
-- * Concurrent Logging
-- | Input queue for a concurrent logger.
data LogChannel message =
LogChannel { fromLogChannel :: TQueue message
, logChannelOpen :: TVar Bool
, logChannelThread :: ThreadId
}
-- | Send the log messages to a 'LogChannel'.
forwardLogsToChannel
:: forall r message a
. (SetMember Eff.Lift (Eff.Lift IO) r)
=> LogChannel message
-> Eff (Logs message ': r) a
-> Eff r a
forwardLogsToChannel logChan actionThatLogs = do
handleLogsWith actionThatLogs
(\withHandler -> withHandler (logChannelPutIO logChan))
-- | Enqueue a log message into a log channel
logChannelPutIO :: LogChannel message -> message -> IO ()
logChannelPutIO c m = atomically
(do
isOpen <- readTVar (logChannelOpen c)
when isOpen (writeTQueue (fromLogChannel c) m)
)
-- | Fork 'LogChannel' backed by a process that repeatedly receives log messages
-- sent by 'forwardLogstochannel' or 'logChannelPutIO'. The process logs by
-- invoken the given IO action. To stop and terminate a 'LogChannel' invoke
-- 'joinLogChannel'.
forkLogChannel
:: forall message
. (message -> IO ())
-> Maybe message
-> IO (LogChannel message)
forkLogChannel handle mFirstMsg = do
(msgQ, isOpenV) <- atomically
(do
tq <- newTQueue
v <- newTVar True
mapM_ (writeTQueue tq) mFirstMsg
return (tq, v)
)
thread <- forkFinally (logLoop msgQ isOpenV) (const (cleanUp msgQ isOpenV))
return (LogChannel msgQ isOpenV thread)
where
cleanUp :: TQueue message -> TVar Bool -> IO ()
cleanUp tq isOpenVar =
atomically
(do
writeTVar isOpenVar False
flushTQueue tq
)
>>= mapM_ handle
logLoop :: TQueue message -> TVar Bool -> IO ()
logLoop tq isOpenVar = do
mMsg <- atomically
(do
isOpen <- readTVar isOpenVar
if isOpen then Just <$> readTQueue tq else return Nothing
)
case mMsg of
Just msg -> do
handle msg
logLoop tq isOpenVar
Nothing -> return ()
-- | Close a log channel. Subsequent loggin requests will no be handled any
-- more.
joinLogChannel :: Maybe message -> LogChannel message -> IO ()
joinLogChannel closeLogMessage (LogChannel tq isOpenVar thread) = do
wasOpen <- atomically
(do
isOpen <- readTVar isOpenVar
if isOpen
then do
writeTVar isOpenVar False
mapM_ (writeTQueue tq) closeLogMessage
return True
else return False
)
when wasOpen (killThread thread)
-- | Wrap 'LogChannel' creation and destruction around a monad action in
-- 'bracket'y manner.
logChannelBracket
:: Maybe message
-> Maybe message
-> (LogChannel message -> IO a)
-> LoggingT message IO a
logChannelBracket mWelcome mGoodbye f = control
(\runInIO -> do
myTId <- myThreadId
let logHandler = void . runInIO . logMessage
bracket (forkLogChannel logHandler mWelcome) (joinLogChannel mGoodbye) f
)