plow-log-async-0.1.4.0: src/Plow/Logging/Async.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Plow.Logging.Async (withAsyncHandleTracer) where
import qualified Control.Monad.IO.Class
import Data.Conduit ((.|))
import qualified Data.Conduit as Conduit
import qualified Data.Conduit.TMChan as Conduit.TMChan
import Data.String (fromString)
import Data.Text (Text)
import qualified Data.Text.IO as T
import Data.Time (getCurrentTime)
import Plow.Logging (IOTracer (..), Tracer (..), traceWith)
import System.IO (Handle, hFlush)
import UnliftIO (MonadUnliftIO)
import UnliftIO.Async (link, withAsync)
import qualified UnliftIO.STM as STM
-- | Returns (in CPS) a 'IOTracer' that pushes messages to a thread-safe queue.
-- This 'IOTracer' won't block unless the queue is full (size is configurable with
-- queueSize)
--
-- An async thread that continuously consumes traces in queue by printing them to a
-- 'Handle' will be launched. Any exceptions thrown inside (or to) the thread will be
-- rethrown in the caller of this function
--
-- Example use
--
-- main =
-- withAsyncHandleTracer stdout 100 $ \tracer' -> do
-- -- We use contramap to convert the tracer to a tracer that accepts
-- -- domain-specic trace types and displays them as Text
-- let tracer = contramap displaySomeTrace tracer'
-- traceWith tracer (SomeTrace a b c)
-- ...
withAsyncHandleTracer :: MonadUnliftIO m => Handle -> Int -> (IOTracer Text -> m a) -> m a
withAsyncHandleTracer handle queueSize f = do
chan <- STM.atomically $ Conduit.TMChan.newTBMChan queueSize
withAsync (logConsumer chan) $ \logConsumerThread -> do
let tracer = asyncTracer chan
link logConsumerThread
res <- f tracer
traceWith tracer "exit" >> waitUntilEmpty chan >> return res
where
logConsumer chan =
Conduit.runConduit $
( Conduit.TMChan.sourceTBMChan chan
.| Conduit.awaitForever
( \(time, msg) ->
Control.Monad.IO.Class.liftIO $ do
T.hPutStrLn handle $ fromString (show time <> ": ") <> msg
hFlush handle
)
)
asyncTracer chan = IOTracer $
Tracer $ \msg -> do
time <- Control.Monad.IO.Class.liftIO $ getCurrentTime
Conduit.runConduit $
Conduit.yield (time, msg)
.| Conduit.TMChan.sinkTBMChan chan
waitUntilEmpty chan =
STM.atomically $
Conduit.TMChan.isEmptyTBMChan chan >>= \case
True -> return ()
False -> STM.retrySTM