plow-log-0.1.6.0: src/Plow/Logging.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
module Plow.Logging
( Tracer (..),
traceWith,
HasEnumerableConstructors,
invalidSilencedConstructors,
warnInvalidSilencedConstructorsWith,
withSilencedTracer,
withAllowedTracer,
withMaybeTracer,
withEitherTracer,
filterTracer,
simpleStdOutTracer,
simpleStdErrTracer,
voidTracer,
IOTracer (..),
)
where
import Control.Monad (when)
import Control.Monad.IO.Class (MonadIO (..))
import Data.Functor.Contravariant (Contravariant (..))
import Data.List (intercalate, intersect)
import Data.Proxy (Proxy)
import Data.String (IsString (..))
import Plow.Logging.EnumerableConstructors
import System.IO (hPutStrLn, stderr)
newtype Tracer m a = Tracer (a -> m ())
instance Monad m => Semigroup (Tracer m a) where
(Tracer f) <> (Tracer g) = Tracer $ \a -> f a >> g a
instance Monad m => Monoid (Tracer m a) where
mempty = Tracer $ \_ -> pure ()
class TraceWith x m where
traceWith :: x a -> a -> m ()
instance TraceWith (Tracer m) m where
traceWith (Tracer t) = t
instance Contravariant (Tracer m) where
contramap f (Tracer t) = Tracer (t . f)
invalidSilencedConstructors :: HasEnumerableConstructors a => Proxy a -> [String] -> [String]
invalidSilencedConstructors p silencedConstructors =
let cs = allConstructors p
in filter (\c -> not $ c `elem` cs) silencedConstructors
-- | Given a "string-like" tracer, outputs a warning message if the supplied 'silencedConstructors' do not
-- match the output of 'allConstructors' for the given tracer type 'a'
warnInvalidSilencedConstructorsWith :: (Applicative m, HasEnumerableConstructors a, IsString s) => Proxy a -> [String] -> Tracer m s -> m ()
warnInvalidSilencedConstructorsWith p silencedConstructors t = case invalidSilencedConstructors p silencedConstructors of
[] -> pure ()
invalid -> traceWith t $ fromString $ "Detected invalid silenced logging options: " <> intercalate ", " invalid
filterTracer :: Applicative m => (a -> Bool) -> Tracer m a -> Tracer m a
filterTracer test (Tracer f) = Tracer $ \m -> when (test m) (f m)
-- | Modifies a given tracer so that any message with a constructor name appearing in 'silencedConstructors'
-- will be discarded. In order to work as expected, the type 'a' is required to have an automatically
-- derived instance of 'HasEnumerableConstructors'.
-- @
-- data Foo = Florb Int | Fleeb String | Bar Bool deriving (Generic, HasEnumerableConstructors)
-- @
-- then calling
-- > traceWith (withSilencedTracer ["Bar"] t) $ Bar False
-- is equivalent to
-- > pure ()
-- and
-- > traceWith (withSilencedTracer ["Bar"] t) $ Florb 3
-- will be definitionally equal to
-- > traceWith t $ Florb 3
withSilencedTracer :: (Applicative m, HasEnumerableConstructors a) => [String] -> Tracer m a -> Tracer m a
withSilencedTracer silencedConstructors = filterTracer (\m -> null $ listConstructors m `intersect` silencedConstructors)
-- | The opposite of 'withSilencedTracer'. This tracer only loggs messages which match a constructor in
-- 'allowedConstructors'.
withAllowedTracer :: (Applicative m, HasEnumerableConstructors a) => [String] -> Tracer m a -> Tracer m a
withAllowedTracer allowedConstructors = filterTracer (\m -> not $ null $ listConstructors m `intersect` allowedConstructors)
-- | Turns a tracer for some 'a' into a tracer for 'Maybe a', which traces 'Just x' using the original tracer
-- | and ignores 'Nothing' (i.e. 'pure ()')
withMaybeTracer :: Applicative m => Tracer m a -> Tracer m (Maybe a)
withMaybeTracer (Tracer t) = Tracer (maybe (pure ()) t)
-- | Takes two tracers for values 'a' and 'b' to a tracer for 'Either a b', which selects the appropriate tracer for each value
withEitherTracer :: Applicative m => Tracer m a -> Tracer m b -> Tracer m (Either a b)
withEitherTracer (Tracer ta) (Tracer tb) = Tracer $ \case
Left a -> ta a
Right b -> tb b
simpleStdOutTracer :: MonadIO m => Tracer m String
simpleStdOutTracer = Tracer $ liftIO . putStrLn
simpleStdErrTracer :: MonadIO m => Tracer m String
simpleStdErrTracer = Tracer $ liftIO . hPutStrLn stderr
-- | Tracer that discards/ignores all messages. Useful in test suites, if we don't care about logging output
voidTracer :: Applicative m => Tracer m t
voidTracer = Tracer $ const $ pure ()
-- | To avoid having to write those pesky 'liftIO's when changing monads, e.g. when
-- passing the logger into a servant server, we can hide the monad entirely behind an
-- existential, requiring a 'MonadIO' instance.
-- We wrap in newtype instead of just a type synonym, to avoid having to have
-- RankNTypes turned on everywhere.
newtype IOTracer a = IOTracer (forall m. MonadIO m => Tracer m a)
instance Contravariant IOTracer where
contramap f (IOTracer t) = IOTracer $ contramap f t
instance MonadIO m => TraceWith IOTracer m where
traceWith (IOTracer (Tracer t)) = t