contra-tracer (empty) → 0.1.0.0
raw patch · 5 files changed
+439/−0 lines, 5 filesdep +basedep +contravariant
Dependencies added: base, contravariant
Files
- LICENSE +5/−0
- README.md +75/−0
- contra-tracer.cabal +27/−0
- src/Control/Tracer.hs +240/−0
- src/Control/Tracer/Arrow.hs +92/−0
+ LICENSE view
@@ -0,0 +1,5 @@+Copyright (c) 2021 Alexander Vieth.+All rights reserved.++Redistribution and use in source and binary forms are permitted provided that the above copyright notice and this paragraph are duplicated in all such forms and that any documentation, advertising materials, and other materials related to such distribution and use acknowledge that the software was developed by the <organization>. The name of the <organization> may not be used to endorse or promote products derived from this software without specific prior written permission.+THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
+ README.md view
@@ -0,0 +1,75 @@+# contra-tracer++Logging and monitoring should be treated as a software feature and not just a+debugging tool.+Yet it's common to find logging definitions which use monads and typeclasses+to do what is essentially printf debugging: the log items are big types like+text or JSON, and these items may be logged any where, any time, because the+logging implementation is part of some omnipresent application monad.++This package provides a minimal set of definitions for _contravariant tracing_.+It is intended to express the pattern--found in logging and monitoring--in which+domain-specific values (e.g. events, statistics) are provided to+domain-agnostic processors (e.g. syslog). A program which has a `Tracer m t` in+scope is able to log any `t` with side-effects in `m`. By choosing `t` to be as+small as possible, the program becomes more modular, because a tracer on a+bigger type is also a tracer on a smaller type:++```Haskell+-- This is from Data.Functor.Contravariant+-- (t -> s) shows that s is bigger than t+contramap :: (t -> s) -> Tracer m s -> Tracer m t+```++The `Contravariant` instance on `Tracer m` is the mechanism by which a+domain-agnostic tracer is adapted to stand in where a domain-specific+tracer is required. This is why we call it contravariant tracing.++```Haskell+-- Puts text to stdout.+stdoutTracer :: Tracer IO Text++-- A somain-specific event type.+data Event = EventA | EventB Int++-- The log format for an Event.+eventToText :: Event -> Text++-- The stdoutTracer becomes an Event tracer by way of the Contravariant+-- instance.+eventTracer :: Tracer IO Event+eventTracer = contramap eventToText stdoutTracer+```++This style is intended to discourage the use of very big types like text or JSON+within programs which do logging. When expressed in this style, such a program+will not even have the possibility of doing the printf debugging style referred+to in the opening paragraph, because there is no `Tracer m Text` in scope!++```Haskell+-- Some action that can use any tracer on Event in IO.+-- It does not need to be able to log text in order to run.+action :: Tracer IO Event -> IO Int+action tracer = do+ traceWith tracer EventA+ stuff <- doSomething+ traceWith tracer (EventB 42)+ pure stuff++main :: IO ()+main = do+ _ <- action eventTracer+ pure ()+```++## Arrow tracers++The contravariant tracer is defined in terms of the arrow tracer found in the+module `Control.Tracer.Arrow`. The motivation for the arrow tracer is to+encourage the programmer to throw in `traceWith` calls liberally, and leave+them in so that they will not bit-rot, with the understanding that there will+be no runtime overhead if tracing is disabled. This module is only relevant when+dealing with a tracer which will ignore certain inputs but not others. With the+arrow representation, it is often possible to judge that a tracer will not emit+anything, without forcing the input, so that the `traceWith` call becomes a+no-op.
+ contra-tracer.cabal view
@@ -0,0 +1,27 @@+name: contra-tracer+version: 0.1.0.0+synopsis: Arrow and contravariant tracers+description: A simple interface for logging, tracing and monitoring+license: BSD3+license-files: LICENSE+author: Alexander Vieth+maintainer: aovieth@gmail.com+copyright: 2021 Alexander Vieth+category: Logging+build-type: Simple+extra-source-files: README.md+cabal-version: >=1.10++source-repository head+ type: git+ location: https://github.com/avieth/contra-tracer++library+ hs-source-dirs: src+ exposed-modules: Control.Tracer+ Control.Tracer.Arrow++ default-language: Haskell2010+ build-depends: base < 5+ if impl(ghc < 8.5)+ build-depends: contravariant
+ src/Control/Tracer.hs view
@@ -0,0 +1,240 @@+{-|+Module : Control.Tracer+Description : A simple interface for logging, tracing, and monitoring+Copyright : (c) Alexander Vieth, 2019+Maintainer : aovieth@gmail.com+License : Apache-2.0++=== General usage++'Tracer' is a contravariant functor intended to express the pattern in which+values of its parameter type are used to produce effects which are prescribed+by the caller, as in tracing, logging, code instrumentation, etc.++Programs should be written to use as specific a tracer as possible, i.e. to+take as a parameter a @Tracer m domainSpecificType@. To combine these programs+into an executable which does meaningful tracing, an implementation of that+tracing should be used to make a @Tracer probablyIO implementationTracingType@,+which is 'contramap'ped to fit @Tracer m domainSpecificType@ wherever it is+needed, for the various @domainSpecificType@s that appear throughout the+program.++=== An example++This short example shows how a tracer can be deployed, highlighting the use of+'contramap' to fit a general tracer which writes text to a file, where a+specific tracer which takes domain-specific events is expected.++> -- Writes text to some log file.+> traceToLogFile :: FilePath -> Tracer IO Text+>+> -- Domain-specific event type.+> data Event = EventA | EventB Int+>+> -- The log-file format for an Event.+> eventToText :: Event -> Text+>+> -- Some action that can use any tracer on Event, in any monad.+> actionWithTrace :: Monad m => Tracer m Event -> m ()+> actionWithTrace tracer = do+> traceWith tracer EventA+> traceWith tracer (EventB 42)+>+> -- Set up a log file tracer, then use it where the Event tracer is expected.+> main :: IO ()+> main = do+> textTacer <- traceToLogFile "log.txt"+> let eventTracer :: Tracer IO Event+> eventTracer = contramap eventToText tracer+> actionWithTrace eventTracer+-}++{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Control.Tracer+ ( Tracer (..)+ , traceWith+ , arrow+ , use+ , Arrow.squelch+ , Arrow.emit+ , Arrow.effect+ -- * Simple tracers+ , nullTracer+ , stdoutTracer+ , debugTracer+ -- * Transforming tracers+ , natTracer+ , Arrow.nat+ , traceMaybe+ , squelchUnless+ -- * Re-export of Contravariant+ , Contravariant(..)+ ) where++import Control.Arrow ((|||), (&&&), arr, runKleisli)+import Control.Category ((>>>))+import Data.Functor.Contravariant (Contravariant (..))+import Debug.Trace (traceM)++import qualified Control.Tracer.Arrow as Arrow++-- | This type describes some effect in @m@ which depends upon some value of+-- type @a@, for which the /output value/ is not of interest (only the effects).+--+-- The motivating use case is to describe tracing, logging, monitoring, and+-- similar features, in which the programmer wishes to provide some values to+-- some /other/ program which will do some real world side effect, such as+-- writing to a log file or bumping a counter in some monitoring system.+--+-- The actual implementation of such a program will probably work on rather+-- large, domain-agnostic types like @Text@, @ByteString@, JSON values for+-- structured logs, etc.+--+-- But the call sites which ultimately /invoke/ these implementations will deal+-- with smaller, domain-specific types that concisely describe events, metrics,+-- debug information, etc.+--+-- This difference is reconciled by the 'Contravariant' instance for 'Tracer'.+-- 'Data.Functor.Contravariant.contramap' is used to change the input type of+-- a tracer. This allows for a more general tracer to be used where a more+-- specific one is expected.+--+-- Intuitively: if you can map your domain-specific type @Event@ to a @Text@+-- representation, then any @Tracer m Text@ can stand in where a+-- @Tracer m Event@ is required.+--+-- > eventToText :: Event -> Text+-- >+-- > traceTextToLogFile :: Tracer m Text+-- >+-- > traceEventToLogFile :: Tracer m Event+-- > traceEventToLogFile = contramap eventToText traceTextToLogFile+--+-- Effectful tracers that actually do interesting stuff can be defined+-- using 'emit', and composed via 'contramap'.+--+-- The 'nullTracer' can be used as a stand-in for any tracer, doing no+-- side-effects and producing no interesting value.+--+-- To deal with branching, the arrow interface on the underlying+-- 'Control.Tracer.Arrow.Tracer' should be used. Arrow notation can be helpful+-- here.+--+-- For example, a common pattern is to trace only some variants of a sum type.+--+-- > data Event = This Int | That Bool+-- >+-- > traceOnlyThat :: Tracer m Int -> Tracer m Bool+-- > traceOnlyThat tr = Tracer $ proc event -> do+-- > case event of+-- > This i -> use tr -< i+-- > That _ -> squelch -< ()+-- +-- The key point of using the arrow representation we have here is that this+-- tracer will not necessarily need to force @event@: if the input tracer @tr@+-- does not force its value, then @event@ will not be forced. To elaborate,+-- suppose @tr@ is @nullTracer@. Then this expression becomes+--+-- > classify (This i) = Left i+-- > classify (That _) = Right ()+-- >+-- > traceOnlyThat tr+-- > = Tracer $ Pure classify >>> (squelch ||| squelch) >>> Pure (either id id)+-- > = Tracer $ Pure classify >>> Pure (either (const (Left ())) (const (Right ()))) >>> Pure (either id id)+-- > = Tracer $ Pure (classify >>> either (const (Left ())) (const (Right ())) >>> either id id)+--+-- So that when this tracer is run by 'traceWith' we get+--+-- > traceWith (traceOnlyThat tr) x+-- > = traceWith (Pure _)+-- > = pure ()+--+-- It is _essential_ that the computation of the tracing effects cannot itself+-- have side-effects, as this would ruin the ability to short-circuit when+-- it is known that no tracing will be done: the side-effects of a branch+-- could change the outcome of another branch. This would fly in the face of+-- a crucial design goal: you can leave your tracer calls in the program so+-- they do not bitrot, but can also make them zero runtime cost by substituting+-- 'nullTracer' appropriately.+newtype Tracer m a = Tracer { runTracer :: Arrow.Tracer m a () }++instance Monad m => Contravariant (Tracer m) where+ contramap f tracer = Tracer (arr f >>> use tracer)++-- | @tr1 <> tr2@ will run @tr1@ and then @tr2@ with the same input.+instance Monad m => Semigroup (Tracer m s) where+ Tracer a1 <> Tracer a2 = Tracer (a1 &&& a2 >>> arr discard)+ where+ discard :: ((), ()) -> ()+ discard = const ()++instance Monad m => Monoid (Tracer m s) where+ mappend = (<>)+ mempty = nullTracer++{-# INLINE traceWith #-}+-- | Run a tracer with a given input.+traceWith :: Monad m => Tracer m a -> a -> m ()+traceWith (Tracer tr) a = runKleisli (Arrow.runTracer tr) a++-- | Inverse of 'use'.+arrow :: Arrow.Tracer m a () -> Tracer m a+arrow = Tracer++-- | Inverse of 'arrow'. Useful when writing arrow tracers which use a+-- contravariant tracer (the newtype in this module).+use :: Tracer m a -> Arrow.Tracer m a ()+use = runTracer++-- | A tracer which does nothing.+nullTracer :: Monad m => Tracer m a+nullTracer = Tracer Arrow.squelch++-- | Create a simple contravariant tracer which runs a given side-effect.+emit :: Applicative m => (a -> m ()) -> Tracer m a+emit f = Tracer (Arrow.emit f)++-- | Run a tracer only for the Just variant of a Maybe. If it's Nothing, the+-- 'nullTracer' is used (no output).+--+-- The arrow representation allows for proper laziness: if the tracer parameter+-- does not produce any tracing effects, then the predicate won't even be+-- evaluated. Contrast with the simple contravariant representation as+-- @a -> m ()@, in which the predicate _must_ be forced no matter what,+-- because it's impossible to know a priori whether that function will not+-- produce any tracing effects.+--+-- It's written out explicitly for demonstration. Could also use arrow+-- notation:+--+-- > traceMaybe p tr = Tracer $ proc a -> do+-- > case k a of+-- > Just b -> use tr -< b+-- > Nothing -> Arrow.squelch -< ()+--+traceMaybe :: Monad m => (a -> Maybe b) -> Tracer m b -> Tracer m a+traceMaybe k tr = Tracer $ classify >>> (Arrow.squelch ||| use tr)+ where+ classify = arr (maybe (Left ()) Right . k)++-- | Uses 'traceMaybe' to give a tracer which emits only if a predicate is true.+squelchUnless :: Monad m => (a -> Bool) -> Tracer m a -> Tracer m a+squelchUnless p = traceMaybe (\a -> if p a then Just a else Nothing)++-- | Use a natural transformation to change the @m@ type. This is useful, for+-- instance, to use concrete IO tracers in monad transformer stacks that have+-- IO as their base.+natTracer :: forall m n s . (forall x . m x -> n x) -> Tracer m s -> Tracer n s+natTracer h (Tracer tr) = Tracer (Arrow.nat h tr)++-- | Trace strings to stdout. Output could be jumbled when this is used from+-- multiple threads. Consider 'debugTracer' instead.+stdoutTracer :: Tracer IO String+stdoutTracer = emit putStrLn++-- | Trace strings using 'Debug.Trace.traceM'. This will use stderr. See+-- documentation in "Debug.Trace" for more details.+debugTracer :: Applicative m => Tracer m String+debugTracer = emit traceM
+ src/Control/Tracer/Arrow.hs view
@@ -0,0 +1,92 @@+{-|+Module : Control.Tracer.Arrow+Copyright : (c) Alexander Vieth, 2019+Licence : Apache-2.0+Maintainer : aovieth@gmail.com+-}++{-# LANGUAGE GADTs #-}+{-# LANGUAGE Arrows #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE BangPatterns #-}++module Control.Tracer.Arrow+ ( Tracer (..)+ , runTracer+ , compute+ , emit+ , effect+ , squelch+ , nat+ ) where++import Prelude hiding ((.), id)+import Control.Arrow+import Control.Category++-- | Formal representation of a tracer arrow as a Kleisli arrow over some+-- monad, but tagged so that we know whether it has any effects which will emit+-- a trace.+data Tracer m a b where+ -- | An emitting part, followed by a non-emitting part.+ -- The non-emitting part is there so that later emitting parts can be+ -- tacked-on later.+ Emitting :: Kleisli m a x -> Kleisli m x b -> Tracer m a b+ -- | No emitting. There may be side-effects, but they are assumed to be+ -- benign and will be discarded by 'runTracer'.+ Squelching :: Kleisli m a b -> Tracer m a b++-- | The resulting Kleisli arrow includes all of the effects required to do+-- the emitting part.+runTracer :: Monad m => Tracer m a () -> Kleisli m a ()+runTracer (Emitting emits _noEmits) = emits >>> arr (const ())+runTracer (Squelching _ ) = arr (const ())++-- | Ignore the input and do not emit. The name is intended to lead to clear+-- and suggestive arrow expressions.+squelch :: Applicative m => Tracer m a ()+squelch = compute (const ())++-- | Do an emitting effect. Contrast with 'effect' which does not make the+-- tracer an emitting tracer.+emit :: Applicative m => (a -> m ()) -> Tracer m a ()+emit f = Emitting (Kleisli f) (Kleisli (const (pure ())))++-- | Do a non-emitting effect. This effect will only be run if some part of+-- the tracer downstream emits (see 'emit').+effect :: (a -> m b) -> Tracer m a b+effect = Squelching . Kleisli++-- | Pure computation in a tracer: no side effects or emits.+compute :: Applicative m => (a -> b) -> Tracer m a b+compute f = effect (pure . f)++instance Monad m => Category (Tracer m) where+ id = compute id+ Squelching l . Squelching r = Squelching (l . r)+ -- Crucial: the squelching parts stay together. Could also have written+ -- = Emitting (rp . re) l+ -- but that would miss opportunities to skip doing work.+ Squelching l . Emitting re rp = Emitting re (l . rp)+ -- Contrast with the above clause: here the emitting part comes _after_ the+ -- squelching part, so the squelching part becomes part of the emitting part.+ Emitting le lp . Squelching r = Emitting (le . r) lp+ Emitting le lp . Emitting re rp = Emitting (le . rp . re) lp++instance Monad m => Arrow (Tracer m) where+ arr = compute+ Squelching l *** Squelching r = Squelching (l *** r )+ Squelching l *** Emitting re rp = Emitting (id *** re) (l *** rp)+ Emitting le lp *** Squelching r = Emitting (le *** id) (lp *** r )+ Emitting le lp *** Emitting re rp = Emitting (le *** re) (lp *** rp)++instance Monad m => ArrowChoice (Tracer m) where+ Squelching l +++ Squelching r = Squelching (l +++ r)+ Squelching l +++ Emitting re rp = Emitting (id +++ re) (l +++ rp)+ Emitting le lp +++ Squelching r = Emitting (le +++ id) (lp +++ r )+ Emitting le lp +++ Emitting re rp = Emitting (le +++ re) (lp +++ rp)++-- | Use a natural transformation to change the underlying monad.+nat :: (forall x . m x -> n x) -> Tracer m a b -> Tracer n a b+nat h (Squelching (Kleisli k)) = Squelching (Kleisli (h . k))+nat h (Emitting (Kleisli k) (Kleisli l)) = Emitting (Kleisli (h . k)) (Kleisli (h . l))