on master help sign in
Add monadic interface with examples
commit9cf2b9551b70e67623f929ae454432a7f1910f2c authorMichael Xavier <michael@michaelxavier.net> dateThu 12 Mar 2015, 19:15 (12 years ago) parent17f5bfe
5 files changed+365−10
katip/examples/Main.hs+87−3 katip/katip.cabal+4 katip/src/Katip.hs+13−1 katip/src/Katip/Core.hs+6−6 katip/src/Katip/Monadic.hs+255
katip/examples/Main.hs+87−3
@@ -1,8 +1,15 @@
1−{-# LANGUAGE TemplateHaskell #-}
1+{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE MultiParamTypeClasses #-}
3+{-# LANGUAGE TemplateHaskell #-}
4+
5+{-# LANGUAGE NoMonomorphismRestriction #-}
6+
27
38module Main where
49
510-------------------------------------------------------------------------------
11+import Control.Applicative
12+import Control.Monad.Reader
613import Data.Aeson
714import Data.Text
815-------------------------------------------------------------------------------
@@ -17,9 +24,10 @@ main = runKatipT _ioLogEnv $ do
1724 logm "example" InfoS "Easy to emit from IO directly!"
1825 logf myContext "example" InfoS "Here's a more stateful item."
1926 $(logt) myContext "example" InfoS "Here's one with code location."
27+ runHandler handler
2028
2129
22−
30+myContext :: MyContext
2331myContext = MyContext "blah" 3
2432
2533data MyContext = MyContext {
@@ -31,8 +39,84 @@ data MyContext = MyContext {
3139instance ToJSON MyContext where
3240 toJSON mc = object ["foo" .= foo mc, "bar" .= bar mc]
3341
42+instance ToObject MyContext
43+
3444instance LogContext MyContext where
35− payloadKeys _ mc = AllKeys
45+ payloadKeys _ _ = AllKeys
46+
47+-------------------------------------------------------------------------------
48+type Handler a = WebT (KatipT IO) a
49+
50+
51+runHandler :: Handler a -> KatipT IO a
52+runHandler h = runReaderT (unWebT h) req
53+ where
54+ req = Request "GET" "/foo/bar" "example.com"
55+
56+withDB :: (ConnInfo -> Handler ()) -> Handler ()
57+withDB f = f $ ConnInfo "1234" "exampledb" ()
58+
59+
60+handler :: Handler ()
61+handler = do
62+ $(logtM) InfoS "got request"
63+ withDB $ \ci -> do
64+ let dbctx = DBLog (ciConnId ci)
65+ let ns = Namespace ["db", ciConnDB ci]
66+ runContextualLogT (return dbctx) (return ns) $ do
67+ liftIO $ putStrLn "DB things!"
68+ $(logtM) InfoS "calling db"
69+
70+-------------------------------------------------------------------------------
71+data Request = Request {
72+ reqMethod :: Text
73+ , reqPath :: Text
74+ , reqReferer :: Text
75+ }
76+
77+instance ToJSON Request where
78+ toJSON req = object [ "method" .= reqMethod req
79+ , "path" .= reqPath req
80+ , "referer" .= reqReferer req
81+ ]
82+
83+instance ToObject Request
84+
85+instance LogContext Request where
86+ payloadKeys V3 _ = AllKeys
87+ payloadKeys _ _ = SomeKeys ["method", "path"]
88+
89+newtype WebT m a = WebT {
90+ unWebT :: ReaderT Request m a
91+ } deriving ( Monad
92+ , Functor
93+ , Applicative
94+ , MonadReader Request
95+ , MonadTrans
96+ , MonadIO
97+ , Katip
98+ )
99+
100+instance Monad m => ContextualLog (WebT m) where
101+ getLogContexts = liftM liftPayload ask
102+ getNamespace = return $ Namespace ["myApp"]
103+
104+
105+-------------------------------------------------------------------------------
106+data ConnInfo = ConnInfo {
107+ ciConnId :: Text
108+ , ciConnDB :: Text
109+ , ciConn :: ()
110+ }
111+
112+data DBLog = DBLog {
113+ dblConnId :: Text
114+ }
36115
116+instance ToJSON DBLog where
117+ toJSON dbl = object ["connId" .= dblConnId dbl]
37118
119+instance ToObject DBLog
38120
121+instance LogContext DBLog where
122+ payloadKeys _ _ = AllKeys
katip/katip.cabal+4
@@ -15,6 +15,7 @@ library
1515 exposed-modules:
1616 Katip
1717 Katip.Core
18+ Katip.Monadic
1819 Katip.Scribes.Handle
1920
2021 -- other-modules:
@@ -46,6 +47,9 @@ library
4647 , transformers >= 0.3 && < 0.5
4748 , unix >= 2.5 && < 2.8
4849 , unordered-containers >= 0.2 && < 0.3
50+ , monad-control >= 1.0 && < 1.1
51+ , mtl
52+ , transformers-base
4953
5054 hs-source-dirs: src
5155 default-language: Haskell2010
katip/src/Katip.hs+13−1
@@ -8,10 +8,14 @@ module Katip
88 , Environment (..)
99 , Severity (..), renderSeverity
1010 , Verbosity (..)
11+ , ToObject (..)
1112 , LogContext (..)
1213 , PayloadSelection (..)
1314 , Scribe (..)
1415 , LogEnv (..)
16+ , ContextualLog(..)
17+ , AnyLogContext
18+ , LogContexts, liftPayload
1519
1620 -- * A Built-in Monad For Logging
1721 , KatipT (..)
@@ -29,7 +33,14 @@ module Katip
2933 , logf
3034 , logm
3135 , logt
32− , logi
36+ , logItem
37+ , logfM
38+ , logtM
39+ , logItemM
40+
41+ -- * Transformer for appending to a log context
42+ , ContextualLogT
43+ , runContextualLogT
3344
3445 -- * Included Scribes
3546 , mkHandleScribe
@@ -38,5 +49,6 @@ module Katip
3849
3950-------------------------------------------------------------------------------
4051import Katip.Core
52+import Katip.Monadic
4153import Katip.Scribes.Handle
4254-------------------------------------------------------------------------------
katip/src/Katip/Core.hs+6−6
@@ -46,7 +46,7 @@ import System.Posix
4646
4747
4848-------------------------------------------------------------------------------
49−newtype Namespace = Namespace { getNamespace :: [Text] }
49+newtype Namespace = Namespace { unNamespace :: [Text] }
5050 deriving (Eq,Show,Read,Ord,Generic,ToJSON,Monoid)
5151
5252instance IsString Namespace where
@@ -358,7 +358,7 @@ runKatipT le (KatipT f) = runReaderT f le
358358-------------------------------------------------------------------------------
359359-- | Log with everything, including a source code location. This is
360360-- very low level and you typically can use 'logt' in its place.
361−logi
361+logItem
362362 :: (Applicative m, LogContext a, Katip m)
363363 => a
364364 -> Namespace
@@ -366,7 +366,7 @@ logi
366366 -> Severity
367367 -> LogStr
368368 -> m ()
369−logi a ns loc sev msg = do
369+logItem a ns loc sev msg = do
370370 LogEnv{..} <- getLogEnv
371371 item <- Item
372372 <$> pure _logEnvNs
@@ -396,7 +396,7 @@ logf
396396 -> LogStr
397397 -- ^ The log message
398398 -> m ()
399−logf a ns sev msg = logi a ns Nothing sev msg
399+logf a ns sev msg = logItem a ns Nothing sev msg
400400
401401
402402-------------------------------------------------------------------------------
@@ -455,9 +455,9 @@ getLoc = [| $(location >>= liftLoc) |]
455455-------------------------------------------------------------------------------
456456-- | 'Loc'-tagged logging when using template-haskell is OK.
457457--
458−-- @$(logt) obj mempty Info "Hello world"@
458+-- @$(logt) obj mempty InfoS "Hello world"@
459459logt :: ExpQ
460−logt = [| \ a ns sev msg -> logi a ns (Just $(getLoc)) sev msg |]
460+logt = [| \ a ns sev msg -> logItem a ns (Just $(getLoc)) sev msg |]
461461
462462
463463-- taken from the file-location package
katip/src/Katip/Monadic.hsnew file+255
@@ -0,0 +1,255 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE FlexibleInstances #-}
3+{-# LANGUAGE GADTs #-}
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5+{-# LANGUAGE MultiParamTypeClasses #-}
6+{-# LANGUAGE StandaloneDeriving #-}
7+{-# LANGUAGE TemplateHaskell #-}
8+{-# LANGUAGE TypeFamilies #-}
9+{-# LANGUAGE UndecidableInstances #-}
10+
11+-- | Provides support for treating payloads and namespaces as
12+-- composable contexts. The common pattern would be to provide a
13+-- 'ContextualLog' instance for your base monad. When your program
14+-- changes to a more detailed context, say to the database, you can
15+-- use 'ContextualLogT' to tack on a typed context and a namespace
16+-- which will be merged into the enclosing monad's context and
17+-- namespace.
18+module Katip.Monadic
19+ (
20+ -- * Monadic variants of logging functions from "Katip.Core"
21+ logfM
22+ , logtM
23+ , logItemM
24+
25+ -- * Machinery for merging typed log payloads/contexts
26+ , ContextualLog(..)
27+ , AnyLogContext
28+ , LogContexts
29+ , liftPayload
30+
31+ -- * Transformer for appending to a log context
32+ , ContextualLogT'
33+ , ContextualLogT
34+ , runContextualLogT
35+ ) where
36+
37+
38+-------------------------------------------------------------------------------
39+import Control.Applicative
40+import Control.Monad
41+import Control.Monad.Base
42+import Control.Monad.Catch
43+import Control.Monad.Error.Class
44+import Control.Monad.Fix
45+import Control.Monad.IO.Class
46+import Control.Monad.RWS.Class
47+import Control.Monad.Trans.Class
48+import Control.Monad.Trans.Control
49+import Control.Monad.Trans.Error (ErrorT)
50+import Control.Monad.Trans.Identity (IdentityT)
51+import Control.Monad.Trans.List (ListT)
52+import Control.Monad.Trans.Maybe (MaybeT)
53+import Control.Monad.Trans.Reader (ReaderT (..), runReaderT)
54+import Control.Monad.Trans.RWS (RWST)
55+import qualified Control.Monad.Trans.RWS.Strict as Strict (RWST)
56+import Control.Monad.Trans.State (StateT)
57+import qualified Control.Monad.Trans.State.Strict as Strict (StateT)
58+import Control.Monad.Trans.Writer (WriterT)
59+import qualified Control.Monad.Trans.Writer.Strict as Strict (WriterT)
60+import Data.Aeson
61+import qualified Data.HashMap.Strict as HM
62+import Data.Monoid
63+import Language.Haskell.TH
64+-------------------------------------------------------------------------------
65+import Katip.Core
66+-------------------------------------------------------------------------------
67+
68+-- | A wrapper around a log context that erases type information so
69+-- that contexts from multiple layers can be combined intelligently.
70+data AnyLogContext where
71+ AnyLogContext :: (LogContext a) => a -> AnyLogContext
72+
73+
74+-------------------------------------------------------------------------------
75+-- | Heterogeneous list of log contexts that provides a smart
76+-- 'LogContext' instance for combining multiple payload policies.
77+newtype LogContexts = LogContexts [AnyLogContext] deriving (Monoid)
78+
79+instance ToJSON LogContexts where
80+ toJSON (LogContexts cs) =
81+ Object $ mconcat $ map (\(AnyLogContext v) -> toObject v) cs
82+
83+instance ToObject LogContexts
84+
85+instance LogContext LogContexts where
86+ payloadKeys verb (LogContexts vs) = mconcat $ map payloadKeys' vs
87+ where
88+ -- To ensure AllKeys doesn't leak keys from other values when
89+ -- combined, we resolve AllKeys to its equivalent SomeKeys
90+ -- representation first.
91+ payloadKeys' (AnyLogContext v) = case payloadKeys verb v of
92+ AllKeys -> SomeKeys $ HM.keys $ toObject v
93+ x -> x
94+
95+
96+-------------------------------------------------------------------------------
97+-- | Lift a log context into the generic wrapper
98+liftPayload :: (LogContext a) => a -> LogContexts
99+liftPayload = LogContexts . (:[]) . AnyLogContext
100+
101+
102+-------------------------------------------------------------------------------
103+-- | A monadic context that has an inherant way to get logging
104+-- context and namespace. Examples include a web application monad or
105+-- database monad. Combine with 'ContextualLogT' to nest.
106+class ContextualLog m where
107+ getLogContexts :: m LogContexts
108+ getNamespace :: m Namespace
109+
110+--TODO: is this INLINABLE?
111+#define TRANS(T) \
112+ instance (ContextualLog m, Monad m) => ContextualLog (T m) where \
113+ getLogContexts = lift getLogContexts; \
114+ getNamespace = lift getNamespace
115+
116+#define TRANS_CTX(CTX, T) \
117+ instance (CTX, ContextualLog m, Monad m) => ContextualLog (T m) where \
118+ getLogContexts = lift getLogContexts; \
119+ getNamespace = lift getNamespace
120+
121+TRANS(IdentityT)
122+TRANS(MaybeT)
123+TRANS(ListT)
124+TRANS(ReaderT r)
125+TRANS(Strict.StateT s)
126+TRANS(StateT s)
127+TRANS_CTX(Error e, ErrorT e)
128+TRANS_CTX(Monoid w, Strict.WriterT w)
129+TRANS_CTX(Monoid w, WriterT w)
130+TRANS_CTX(Monoid w, Strict.RWST r w s)
131+TRANS_CTX(Monoid w, RWST r w s)
132+
133+deriving instance (Monad m, ContextualLog m) => ContextualLog (KatipT m)
134+
135+-------------------------------------------------------------------------------
136+-- | Log with everything, including a source code location. This is
137+-- very low level and you typically can use 'logtM' in its
138+-- place. Automaticallysupplies payload and namespace.
139+logItemM
140+ :: (Applicative m, ContextualLog m, Katip m)
141+ => Maybe Loc
142+ -> Severity
143+ -> LogStr
144+ -> m ()
145+logItemM loc sev msg = do
146+ ctx <- getLogContexts
147+ ns <- getNamespace
148+ logItem ctx ns loc sev msg
149+
150+
151+
152+-------------------------------------------------------------------------------
153+-- | Log with full context, but without any code
154+-- location. Automatically supplies payload and namespace.
155+logfM
156+ :: (Applicative m, ContextualLog m, Katip m)
157+ => Severity
158+ -- ^ Severity of the message
159+ -> LogStr
160+ -- ^ The log message
161+ -> m ()
162+logfM sev msg = do
163+ ctx <- getLogContexts
164+ ns <- getNamespace
165+ logf ctx ns sev msg
166+
167+
168+-------------------------------------------------------------------------------
169+-- | 'Loc'-tagged logging when using template-haskell is OK. Automatically supplies payload and namespace.
170+--
171+-- @$(logt) InfoS "Hello world"@
172+logtM :: ExpQ
173+logtM = [| logItemM (Just $(getLoc)) |]
174+
175+
176+-------------------------------------------------------------------------------
177+-- | Wrapper over 'ReaderT' that provides a passthrough instance for
178+-- 'MonadReader' so your stack will not be affected. Also provides a
179+-- 'ContextualLog' instance which combines with the inner monad's
180+-- instance.
181+--
182+-- @
183+-- instance ContextualLog m WebMonad where
184+-- getLogContexts = ...
185+-- getNamespace = Namespace ["web"]
186+--
187+-- dbCallInWeb = runContextualLogT getDBContext (Namespace ["db"]) $ do
188+-- someStuff
189+-- @
190+--
191+-- In the above example, any logging inside of @dbCallInWeb@ will get
192+-- the (optionally) monadic log context for the db, combine it with
193+-- the web server's and will use the namespace @Namespace ["web","db"]@.
194+--
195+-- 'ContextualLogT' is exported which fixes @n ~ m@. The two type
196+-- variables must be present for defining a 'MonadTransControl' instance.
197+newtype ContextualLogT' n m a = ContextualLogT {
198+ unContextualLogT :: ReaderT (n LogContexts, n Namespace) m a
199+ } deriving ( Functor
200+ , Applicative
201+ , Monad
202+ , MonadIO
203+ , MonadThrow
204+ , MonadCatch
205+ , MonadBase b
206+ , MonadState s
207+ , MonadWriter w
208+ , MonadError e
209+ , MonadPlus
210+ , MonadFix
211+ , Katip
212+ )
213+
214+type ContextualLogT m a = ContextualLogT' m m a
215+
216+instance MonadTrans (ContextualLogT' n) where
217+ lift = ContextualLogT . lift
218+
219+-- Reader is a passthrough. We don't expose our internal reader so as not to conflict
220+instance (MonadReader r m) => MonadReader r (ContextualLogT' n m) where
221+ ask = lift ask
222+ local f (ContextualLogT (ReaderT m)) = ContextualLogT $ ReaderT $ \r ->
223+ local f (m r)
224+
225+instance MonadTransControl (ContextualLogT' n) where
226+ type StT (ContextualLogT' n) a = StT (ReaderT (n LogContexts)) a
227+ liftWith = defaultLiftWith ContextualLogT unContextualLogT
228+ restoreT = defaultRestoreT ContextualLogT
229+ {-# INLINE liftWith #-}
230+ {-# INLINE restoreT #-}
231+
232+instance (MonadBaseControl b m) => MonadBaseControl b (ContextualLogT' n m) where
233+ type StM ((ContextualLogT' n) m) a = ComposeSt (ContextualLogT' n) m a
234+ liftBaseWith = defaultLiftBaseWith
235+ restoreM = defaultRestoreM
236+
237+instance (ContextualLog m, Monad m) => ContextualLog (ContextualLogT' m m) where
238+ getLogContexts = ContextualLogT $ ReaderT $ \(gctxs, _) -> do
239+ ctxs <- gctxs
240+ ctxs' <- getLogContexts
241+ return $ ctxs <> ctxs'
242+ getNamespace = ContextualLogT $ ReaderT $ \(_, getNsInner) -> do
243+ nsOuter <- getNamespace
244+ nsInner <- getNsInner
245+ return $ nsOuter <> nsInner
246+
247+
248+runContextualLogT
249+ :: (Functor m, LogContext a)
250+ => m a
251+ -> m Namespace
252+ -> ContextualLogT m b
253+ -> m b
254+runContextualLogT lgtr nsgtr m = runReaderT (unContextualLogT m) (liftPayload <$> lgtr, nsgtr)
255+