unclogging-0.1.0.0: src/Unclog/Subscriber.hs
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
{-# HLINT ignore "Avoid lambda" #-}
module Unclog.Subscriber
( -- * subscribing to a log channel
Subscriber (..)
, colourSubscriber
, simpleSubscriber
, fileSubscriber
-- * run a logging situation
, withLoggingWithSubscribers
-- * creating, subscribing and reading from channels
, newLogChan
, newLogChanIO
, subscribeLog
, readLogEntry
-- * helpers
, withSubscriber
, mkSubscriber
, mkSubscriberSimple
, bracketSubscriber
, refSubscriber
)
where
import Control.Concurrent.Async (cancelMany)
import Control.Monad (forever, when)
import Control.Monad.Codensity
import Data.ByteString.Builder qualified as BS
import Unclog.Common (LogEntry (level), LogLevel, PLogChan (..), SLogChan (..))
import Unclog.Render (colouredBuilder, simpleBuilder)
import UnliftIO
-- | a 'Subscriber' is something that can observe log entries and do something with them in the Monad @m@
newtype Subscriber m = MkSubscriber {runSubscriber :: Codensity m (LogEntry -> m ())}
-- | make a subscriber with a possible clean up (useful if you need to bracket your subscriber)
mkSubscriber :: (forall r. ((LogEntry -> m ()) -> m r) -> m r) -> Subscriber m
mkSubscriber k = MkSubscriber (Codensity k)
{-# INLINE mkSubscriber #-}
-- | make a subscriber from a simple log function
mkSubscriberSimple :: (LogEntry -> m ()) -> Subscriber m
mkSubscriberSimple k = MkSubscriber (pure k)
{-# INLINE mkSubscriberSimple #-}
-- | destruct a subscriber
withSubscriber :: Subscriber m -> ((LogEntry -> m ()) -> m b) -> m b
withSubscriber sub = runCodensity (runSubscriber sub)
{-# INLINE withSubscriber #-}
-- | write a log entry to a handle, printing all information, but use colour
colourSubscriber
:: MonadIO m
=> LogLevel
-- ^ the lowest loglevel to still log
-> Handle
-- ^ the handle to log to
-> Subscriber m
colourSubscriber lvl hdl = mkSubscriberSimple \entry -> when (entry.level >= lvl) do
liftIO $ BS.hPutBuilder hdl (colouredBuilder entry)
{-# INLINEABLE colourSubscriber #-}
-- | write a log entry to a handle, printing all information
simpleSubscriber
:: MonadIO m
=> LogLevel
-- ^ the lowest loglevel to still log
-> Handle
-- ^ the handle to log to
-> Subscriber m
simpleSubscriber lvl hdl = mkSubscriberSimple \entry -> when (entry.level >= lvl) do
liftIO $ BS.hPutBuilder hdl (simpleBuilder entry)
{-# INLINEABLE simpleSubscriber #-}
-- | create a simple subscriber that writes to a file
fileSubscriber
:: MonadUnliftIO m
=> LogLevel
-- ^ the lowest loglevel to still log
-> FilePath
-- ^ the file to log (append) to
-> Subscriber m
fileSubscriber lvl fp = mkSubscriber \k ->
withFile fp AppendMode \hdl ->
withSubscriber (simpleSubscriber lvl hdl) k
{-# INLINEABLE fileSubscriber #-}
-- | build a subscriber that requires a resource
bracketSubscriber
:: MonadUnliftIO m
=> m a
-- ^ acquire the resource
-> (a -> m b)
-- ^ release the resource
-> (a -> LogEntry -> m ())
-- ^ how to write a log entry given a resource
-> Subscriber m
bracketSubscriber acquire release entry = mkSubscriber \k ->
bracket
acquire
release
\a -> k (entry a)
{-# INLINEABLE bracketSubscriber #-}
-- | write log entries to an 'IORef'
refSubscriber :: MonadIO m => IORef [LogEntry] -> Subscriber m
refSubscriber ref = mkSubscriberSimple \entry -> atomicModifyIORef' ref \old -> (entry : old, ())
{-# INLINEABLE refSubscriber #-}
-- | it is not a priori unsafe to escape the chan out of the context, it will just mean that
-- all the subscribers are gone and nothing is logged anymore
withLoggingWithSubscribers
:: forall m r
. MonadUnliftIO m
=> [Subscriber m]
-- ^ a list of subscribers
-> (PLogChan -> m r)
-- ^ a channel, a client can write to
-> m r
withLoggingWithSubscribers subscribers k = do
(chan, subchans) <- atomically do
c <- newLogChan
(c,) <$> traverse (\sub -> (,sub) <$> subscribeLog c) subscribers
subAsyncs <- traverse (uncurry mkSubscriberAsync) subchans
k chan `finally` liftIO (cancelMany subAsyncs)
where
mkSubscriberAsync :: SLogChan -> Subscriber m -> m (Async b)
mkSubscriberAsync chan subscription = async $ runCodensity (runSubscriber subscription) \sub -> do
let -- this mask is needed because if we receive a cancel in the middle of executing the subcriber, the
-- subscriber is aborted and we're missing a log entry. This can happen when the subscribers are
-- still reading but the main task as stopped writing and is trying to flush the subscribers
runSubscription = forever $ mask_ do
-- the normal case just reads the next log entry from the channel and runs the subscriber on it
atomically (readLogEntry chan)
>>= sub
flushSubscription = do
atomically (tryReadTChan (unSLogChan chan)) >>= \case
Nothing -> pure ()
Just a -> do
sub a
flushSubscription
runSubscription `finally` flushSubscription
{-# INLINEABLE withLoggingWithSubscribers #-}
-- | create a new 'LogChan'
newLogChan :: STM PLogChan
newLogChan = MkPLogChan <$> newBroadcastTChan
{-# INLINE newLogChan #-}
-- | create a new 'LogChan' in 'IO'
newLogChanIO :: MonadIO m => m PLogChan
newLogChanIO = MkPLogChan <$> newBroadcastTChanIO
{-# INLINE newLogChanIO #-}
-- | subscribe to a publishing log chan to obtain a subscriber log chan
subscribeLog :: PLogChan -> STM SLogChan
subscribeLog = fmap MkSLogChan . dupTChan . unPLogChan
{-# INLINE subscribeLog #-}
-- | read one log entry from a subscriber log chan
readLogEntry :: SLogChan -> STM LogEntry
readLogEntry = readTChan . unSLogChan
{-# INLINE readLogEntry #-}