packages feed

prodapi-0.1.0.0: src/Prod/Tracer.hs

-- https://www.youtube.com/watch?v=qzOQOmmkKEM&feature=emb_logo

module Prod.Tracer (
    Tracer (..),
    silent,
    traceIf,
    traceBoth,

    -- * common utilities
    tracePrint,
    traceHPrint,
    traceHPut,
    encodeJSON,
    pulls,

    -- * re-exports
    Contravariant (..),
    Divisible (..),
    Decidable (..),
) where

import Control.Monad ((>=>))
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Aeson (ToJSON, encode)
import Data.ByteString.Lazy (ByteString, hPut)
import Data.Functor.Contravariant
import Data.Functor.Contravariant.Divisible
import System.IO (Handle, hPrint)

newtype Tracer m a = Tracer {runTracer :: (a -> m ())}

instance Contravariant (Tracer m) where
    contramap f (Tracer g) = Tracer (g . f)

instance (Applicative m) => Divisible (Tracer m) where
    conquer = silent
    divide = traceSplit

instance (Applicative m) => Decidable (Tracer m) where
    lose _ = silent
    choose = tracePick

-- | Disable Tracing.
{-# INLINE silent #-}
silent :: (Applicative m) => Tracer m a
silent = Tracer (const $ pure ())

{- | Splits a tracer into two chunks that are run sequentially.

This name can be confusing but it has to be thought backwards for Contravariant logging:
We compose a target tracer from two tracers but we split the content of the trace.

Note that the split function may actually duplicate inputs (that's how traceBoth works).
-}
{-# INLINEABLE traceSplit #-}
traceSplit :: (Applicative m) => (c -> (a, b)) -> Tracer m a -> Tracer m b -> Tracer m c
traceSplit split (Tracer f1) (Tracer f2) = Tracer (go . split)
  where
    go (b, c) = f1 b *> f2 c

{- | If you are given two tracers and want to pass both.
Composition occurs in sequence.
-}
{-# INLINEABLE traceBoth #-}
traceBoth :: (Applicative m) => Tracer m a -> Tracer m a -> Tracer m a
traceBoth t1 t2 = traceSplit (\x -> (x, x)) t1 t2

{- | Picks a tracer based on the emitted object.
Example logic that can be built is traceIf that silent messages.
-}
{-# INLINEABLE tracePick #-}
tracePick :: (Applicative m) => (c -> Either a b) -> Tracer m a -> Tracer m b -> Tracer m c
tracePick split (Tracer f1) (Tracer f2) = Tracer $ \a ->
    let e = split a
     in either f1 f2 e

-- | Filter by dynamically testing values.
{-# INLINEABLE traceIf #-}
traceIf :: (Applicative m) => (a -> Bool) -> Tracer m a -> Tracer m a
traceIf predicate t = tracePick (\x -> if predicate x then Left () else Right x) silent t

-- | A tracer that prints emitted events.
tracePrint :: (MonadIO m, Show a) => Tracer m a
tracePrint = Tracer (liftIO . print)

-- | A tracer that prints emitted to some handle.
traceHPrint :: (MonadIO m, Show a) => Handle -> Tracer m a
traceHPrint handle = Tracer (liftIO . hPrint handle)

-- | A tracer that puts some ByteString to some handle.
traceHPut :: (MonadIO m) => Handle -> Tracer m ByteString
traceHPut handle = Tracer (liftIO . hPut handle)

-- | A conversion encoding values to JSON.
{-# INLINE encodeJSON #-}
encodeJSON :: (ToJSON a) => Tracer m ByteString -> Tracer m a
encodeJSON = contramap encode

{- | Pulls a value to complete a trace when a trace occurs.

This function allows to combines pushed values with pulled values.  Hence,
performing some scheduling between behaviours.
Typical usage would be to annotate a trace with a background value, or perform
data augmentation in a pipelines of traces.

Note that if you rely on this function you need to pay attention of the
blocking effect of 'pulls': the traced value c is not forwarded until a
value b is available.
-}
{-# INLINE pulls #-}
pulls :: (Monad m) => (c -> m b) -> Tracer m b -> Tracer m c
pulls act (Tracer f1) = Tracer $ act >=> f1