{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -Wno-orphans #-} {- | Module: Effectful.Katip Description: Effect to use Katip -} module Effectful.Katip ( -- * Framework Types Namespace (..), Environment (..), Severity (..), renderSeverity, textToSeverity, Verbosity (..), ToObject (..), LogItem (..), Item (..), ThreadIdText (..), PayloadSelection (..), Scribe (..), LogEnv (..), SimpleLogPayload, sl, defaultScribeSettings, ScribeSettings, scribeBufferSize, _scribeBufferSize, -- ** @lens@-Compatible Lenses itemApp, itemEnv, itemSeverity, itemThread, itemHost, itemProcess, itemPayload, itemMessage, itemTime, itemNamespace, itemLoc, logEnvHost, logEnvPid, logEnvApp, logEnvEnv, logEnvTimer, logEnvScribes, -- * Effect KatipE, -- ** Running The Effect runKatipE, runKatipContextE, startKatipE, startKatipContextE, -- * Initializing Loggers registerScribe, -- * Dropping Scribes Temporarily unregisterScribe, clearScribes, -- * Finalizing Scribes At Shutdown closeScribe, -- * Logging Functions LogStr (..), logStr, ls, showLS, -- ** Katip Logging Functions getLogEnv, localLogEnv, logF, logMsg, logT, logLoc, logItem, logKatipItem, logException, -- ** KatipContext Logging Functions getKatipContext, localKatipContext, getKatipNamespace, localKatipNamespace, logFM, logTM, logLocM, logItemM, logExceptionM, AnyLogContext, LogContexts, liftPayload, -- *** Temporarily Changing Log Behaviour katipAddNamespace, katipAddContext, katipNoLogging, -- * Included Scribes mkHandleScribe, mkHandleScribeWithFormatter, mkFileScribe, ColorStrategy (..), ItemFormatter, bracketFormat, jsonFormat, -- * Tools For Implementing Scribes PermitFunc, permitAND, permitOR, permitItem, payloadObject, itemJson, ) where import Effectful import Effectful.Dispatch.Static import Katip ( -- logging -- logContext AnyLogContext, -- formatting ColorStrategy (..), Environment (..), Item (..), ItemFormatter, LogContexts, LogEnv (..), LogItem (..), LogStr (..), Namespace (..), PayloadSelection (..), -- permission PermitFunc, Scribe (..), ScribeSettings, Severity (..), SimpleLogPayload, ThreadIdText (..), ToObject (..), Verbosity (..), bracketFormat, -- removing scribe clearScribes, defaultScribeSettings, itemApp, itemEnv, itemHost, itemJson, itemLoc, itemMessage, itemNamespace, itemPayload, itemProcess, itemSeverity, itemThread, itemTime, jsonFormat, liftPayload, logEnvApp, logEnvEnv, logEnvHost, logEnvPid, logEnvScribes, logEnvTimer, logStr, ls, payloadObject, permitAND, permitItem, permitOR, renderSeverity, scribeBufferSize, showLS, sl, textToSeverity, unregisterScribe, _scribeBufferSize, ) import Data.Kind import Data.Text (Text) import Katip qualified as K import Katip.Core (getLocTH) import Language.Haskell.TH (Loc) import Language.Haskell.TH.Lib import System.IO (Handle) import Unsafe.Coerce (unsafeCoerce) -- | A Effect you can use to run logging actions. there is only one effect as we can't have duplicated instances. type KatipE :: Effect data KatipE m a type instance DispatchOf KatipE = Static WithSideEffects data instance StaticRep KatipE = MkKatipE !LogEnv !LogContexts !Namespace -- | Run a KatipE Effect without a 'Namespace' or a 'LogContexts'. This also calls closeScribes runKatipE :: forall es a. (IOE :> es) => LogEnv -> Eff (KatipE : es) a -> Eff es a runKatipE l act = evalStaticRep (MkKatipE l mempty mempty) act <* liftIO (K.closeScribes l) -- | Run a KatipE Effect with a 'Namespace' and a 'LogContexts'. this also calls closeScribes runKatipContextE :: forall es a a1. (LogItem a, IOE :> es) => LogEnv -> a -> Namespace -> Eff (KatipE : es) a1 -> Eff es a1 runKatipContextE l pl ns act = evalStaticRep (MkKatipE l (liftPayload pl) ns) act <* liftIO (K.closeScribes l) instance forall es. (IOE :> es, KatipE :> es) => K.Katip (Eff es) where getLogEnv :: (IOE :> es, KatipE :> es) => Eff es LogEnv getLogEnv = getLogEnv localLogEnv :: (IOE :> es, KatipE :> es) => (LogEnv -> LogEnv) -> Eff es a -> Eff es a localLogEnv = localLogEnv instance forall es. (IOE :> es, KatipE :> es) => K.KatipContext (Eff es) where getKatipContext :: Eff es LogContexts getKatipContext = getKatipContext localKatipContext :: (IOE :> es, KatipE :> es) => (LogContexts -> LogContexts) -> Eff es a -> Eff es a localKatipContext = localKatipContext getKatipNamespace :: (IOE :> es, KatipE :> es) => Eff es Namespace getKatipNamespace = getKatipNamespace localKatipNamespace :: (IOE :> es, KatipE :> es) => (Namespace -> Namespace) -> Eff es a -> Eff es a localKatipNamespace = localKatipNamespace -- | Run a KatipE Effect without a 'Namespace' or a 'LogContexts' and creating a 'LogEnv' startKatipE :: (IOE :> es) => Namespace -> Environment -> Eff (KatipE : es) a -> Eff es a startKatipE ns env act = liftIO (K.initLogEnv ns env) >>= \lenv -> runKatipE lenv act -- | Run a KatipE Effect with a 'Namespace' and a 'LogContexts' and creating a 'LogEnv' startKatipContextE :: (IOE :> es, LogItem a) => Environment -> a -> Namespace -> Eff (KatipE : es) a1 -> Eff es a1 startKatipContextE env a ns act = liftIO (K.initLogEnv ns env) >>= \e -> runKatipContextE e a ns act {- | Add a scribe to the list. All future log calls will go to this scribe in addition to the others. Writes will be buffered per the ScribeSettings to prevent slow scribes from slowing down logging. Writes will be dropped if the buffer fills. -} registerScribe :: (KatipE :> es) => Text -> Scribe -> ScribeSettings -> Eff es () registerScribe txt scrb scrbs = do MkKatipE lenv lctx ns <- getStaticRep nlenv <- unsafeEff_ $ K.registerScribe txt scrb scrbs lenv putStaticRep $ MkKatipE nlenv lctx ns -- | Finalize a scribe early. Note that it isn't necessary to call this as both 'runKatipE' and 'runKatipContextE' call 'K.closeScribes' closeScribe :: (KatipE :> es) => Text -> Eff es () closeScribe name = do MkKatipE le lc ns <- getStaticRep newle <- unsafeEff_ $ K.closeScribe name le putStaticRep $ MkKatipE newle lc ns -- | get the 'KatipE' 'LogEnv' getLogEnv :: forall es. (KatipE :> es) => Eff es LogEnv getLogEnv = do s <- getStaticRep @KatipE case s of MkKatipE le _ _ -> return le -- | temporarily modify the 'LogEnv' localLogEnv :: forall es a. (KatipE :> es) => (LogEnv -> LogEnv) -> Eff es a -> Eff es a localLogEnv f = localStaticRep @KatipE $ \(MkKatipE le lc ns) -> MkKatipE (f le) lc ns -- | get the 'KatipE' 'LogContexts' getKatipContext :: (KatipE :> es) => Eff es LogContexts getKatipContext = do s <- getStaticRep @KatipE case s of MkKatipE _ lc _ -> return lc -- | temporarily modify the 'LogContexts' localKatipContext :: forall es a. (KatipE :> es) => (LogContexts -> LogContexts) -> Eff es a -> Eff es a localKatipContext f = localStaticRep @KatipE $ \(MkKatipE le lc ns) -> MkKatipE le (f lc) ns getKatipNamespace :: forall es. (KatipE :> es) => Eff es Namespace -- | get the 'KatipE' 'Namespace' getKatipNamespace = do s <- getStaticRep @KatipE case s of MkKatipE _ _ ns -> return ns -- | temporarily modify the 'Namespace' localKatipNamespace :: forall es a. (KatipE :> es) => (Namespace -> Namespace) -> Eff es a -> Eff es a localKatipNamespace f = localStaticRep @KatipE $ \(MkKatipE le lc ns) -> MkKatipE le lc $ f ns -- | escape hatch for implementing your own scribes unsafeEmbedIOE :: forall es a. (KatipE :> es) => ((KatipE :> es, IOE :> es) => Eff es a) -> Eff es a unsafeEmbedIOE act = useDict (unsafeCoerce (MkDict @(KatipE :> es)) :: Dict (KatipE :> es, IOE :> es)) act type Dict :: Constraint -> Type data Dict a where MkDict :: (a) => Dict a useDict :: forall a r. Dict a -> ((a) => r) -> r useDict MkDict r = r -- | Log with full context, but without any code location. logF :: forall a es. (LogItem a, KatipE :> es) => a -> Namespace -> Severity -> LogStr -> Eff es () logF a ns sev logs = unsafeEmbedIOE $ K.logF a ns sev logs -- | Log a message without any payload/context or code location. logMsg :: forall es. (KatipE :> es) => Namespace -> Severity -> LogStr -> Eff es () logMsg ns sev logs = unsafeEmbedIOE $ K.logMsg ns sev logs {-# INLINE logT #-} -- | Loc-tagged logging when using template-haskell. -- @ -- $(logT) obj mempty InfoS "Hello world" -- @ logT :: ExpQ logT = [|\a ns sev msg -> logItem a ns (Just $(getLocTH)) sev msg|] {-# INLINE logLoc #-} -- | 'Loc'-tagged logging using 'GHC.Stack.Stack' when available. -- This function does not require template-haskell as it automatically uses implicit-callstacks when the code is compiled using GHC > 7.8. -- Using an older version of the compiler will result in the emission of a log line without any location information, so be aware of it. -- @ -- logLoc obj mempty InfoS "Hello world" -- @ logLoc :: (LogItem a, KatipE :> es, HasCallStack) => a -> Namespace -> Severity -> LogStr -> Eff es () logLoc a ns sev logs = unsafeEmbedIOE $ K.logLoc a ns sev logs -- | Log with everything, including a source code location. -- This is very low level and you typically can use 'logT' in its place. logItem :: (LogItem a, KatipE :> es) => a -> Namespace -> Maybe Loc -> Severity -> LogStr -> Eff es () logItem a ns loc sev logs = unsafeEmbedIOE $ K.logItem a ns loc sev logs -- | Log an already constructed 'Item'. -- This is the lowest level function that other log* functions use. -- It can be useful when implementing centralised logging services. logKatipItem :: (LogItem a, KatipE :> es) => Item a -> Eff es () logKatipItem item = unsafeEmbedIOE $ K.logKatipItem item -- | Perform an action while logging any exceptions that may occur. -- @ -- >>> > logException () mempty ErrorS (error "foo") -- @ logException :: (LogItem a, KatipE :> es) => a -> Namespace -> Severity -> Eff es b -> Eff es b logException a ns sev act = unsafeEmbedIOE $ K.logException a ns sev act -- | Log with full context, but without any code location. -- Automatically supplies payload and namespace. logFM :: (KatipE :> es) => Severity -> LogStr -> Eff es () logFM sev logs = unsafeEmbedIOE $ K.logFM sev logs {-# INLINE logTM #-} -- | 'Loc'-tagged logging when using template-haskell. -- Automatically supplies payload and namespace. -- @ -- $(logTM) InfoS "Hello world" -- @ logTM :: ExpQ logTM = [|logItemM (Just $(getLocTH))|] {-#INLINE logLocM #-} -- | 'Loc'-tagged logging when using 'GHC.Stack.getCallStack' implicit-callstacks. -- Automatically supplies payload and namespace. -- Same consideration as 'logLoc' applies -- By default, location will be logged from the module that invokes logLocM. -- If you want to use logLocM in a helper, -- wrap the entire helper in withFrozenCallStack to retain the callsite of the helper in the logs. -- This function does not require template-haskell. -- @ -- logLocM InfoS "Hello world" -- @ logLocM :: (KatipE :> es, HasCallStack) => Severity -> LogStr -> Eff es () logLocM sev logs = unsafeEmbedIOE $ K.logLocM sev logs -- | Log with everything, including a source code location. -- This is very low level and you typically can use 'logTM' in its place. -- Automatically supplies payload and namespace. logItemM :: (KatipE :> es, HasCallStack) => Maybe Loc -> Severity -> LogStr -> Eff es () logItemM loc sev logs = unsafeEmbedIOE $ K.logItemM loc sev logs -- | Perform an action while logging any exceptions that may occur. -- @ -- >>> > error "foo" `logExceptionM` ErrorS -- @ logExceptionM :: (KatipE :> es) => Eff es a -> Severity -> Eff es a logExceptionM act sev = unsafeEmbedIOE $ K.logExceptionM act sev -- | Append a namespace segment to the current namespace for the given monadic action, -- then restore the previous state afterwards. katipAddNamespace :: (KatipE :> es) => Namespace -> Eff es a -> Eff es a katipAddNamespace ns = localKatipNamespace (<> ns) -- | Append some context to the current context for the given monadic action, -- then restore the previous state afterwards. -- Important note: be careful using this in a loop. -- If you're using something like forever or replicateM_ that does explicit sharing to avoid a memory leak, -- youll be fine as it will *sequence* calls to katipAddNamespace, -- so each loop will get the same context added. -- If you instead roll your own recursion and you're recursing in the action you provide, -- you'll instead accumulate tons of redundant contexts and even if they all merge on log, -- they are stored in a sequence and will leak memory. katipAddContext :: (KatipE :> es, LogItem i) => i -> Eff es a -> Eff es a katipAddContext ctx = localKatipContext (<> liftPayload ctx) -- | Disable all scribes for the given monadic action, -- then restore them afterwards. katipNoLogging :: (KatipE :> es) => Eff es a -> Eff es a katipNoLogging = localLogEnv $ \lenv -> lenv{_logEnvScribes = mempty} -- | Logs to a file handle such as stdout, stderr, or a file. -- Contexts and other information will be flattened out into bracketed fields. -- For example:] -- @ -- [2016-05-11 21:01:15][MyApp][Info][myhost.example.com][PID 1724][ThreadId 1154][main:Helpers.Logging Helpers/Logging.hs:32:7] Started -- [2016-05-11 21:01:15][MyApp.confrabulation][Debug][myhost.example.com][PID 1724][ThreadId 1154][confrab_factor:42.0][main:Helpers.Logging Helpers/Logging.hs:41:9] Confrabulating widgets, with extra namespace and context -- [2016-05-11 21:01:15][MyApp][Info][myhost.example.com][PID 1724][ThreadId 1154][main:Helpers.Logging Helpers/Logging.hs:43:7] Namespace and context are back to normal -- @ -- Returns the newly-created 'Scribe'. The finalizer flushes the handle. Handle mode is set to 'System.IO.LineBuffering' automatically. mkHandleScribe :: forall es. (KatipE :> es) => ColorStrategy -> Handle -> PermitFunc -> Verbosity -> Eff es Scribe mkHandleScribe cs h pf v = unsafeEmbedIOE $ liftIO $ K.mkHandleScribe cs h pf v -- | Logs to a file handle such as stdout, stderr, or a file. -- Takes a custom 'ItemFormatter' that can be used to format 'Item' as needed. -- Returns the newly-created 'Scribe'. -- The finalizer flushes the handle. -- Handle mode is set to 'System.IO.LineBuffering' automatically. mkHandleScribeWithFormatter :: forall es. (KatipE :> es) => (forall a. (LogItem a) => ItemFormatter a) -> ColorStrategy -> Handle -> PermitFunc -> Verbosity -> Eff es Scribe mkHandleScribeWithFormatter ifa cs h pf v = unsafeEmbedIOE $ liftIO $ K.mkHandleScribeWithFormatter ifa cs h pf v -- | A specialization of 'mkHandleScribe' that takes a 'FilePath' instead of a 'Handle'. It is responsible for opening the file in 'System.IO.AppendMode' and will close the file handle on closeScribe/closeScribes. Does not do log coloring. Sets handle to 'System.IO.LineBuffering' mode. mkFileScribe :: forall es. (KatipE :> es) => FilePath -> PermitFunc -> Verbosity -> Eff es Scribe mkFileScribe fp pf v = unsafeEmbedIOE $ liftIO $ K.mkFileScribe fp pf v