{-# LANGUAGE CPP #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
#if MIN_VERSION_base(4, 9, 0)
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
#endif
module Katip.Core where
import Control.Applicative as A
import Control.AutoUpdate
import Control.Concurrent
import qualified Control.Concurrent.Async as Async
import Control.Concurrent.STM
import qualified Control.Concurrent.STM.TBQueue as BQ
import Control.Exception.Safe
import Control.Monad (unless, void)
import Control.Monad.Base
import Control.Monad.IO.Class
import Control.Monad.IO.Unlift
import Control.Monad.Trans.Class
import Control.Monad.Trans.Control
#if !MIN_VERSION_either(4, 5, 0)
import Control.Monad.Trans.Either
#endif
import Control.Monad.Trans.Except
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.Reader
import Control.Monad.Trans.Resource (ResourceT, transResourceT)
import Control.Monad.Trans.State.Lazy (StateT, mapStateT)
import qualified Control.Monad.Trans.State.Strict as Strict (StateT, mapStateT)
import Control.Monad.Trans.Writer.Lazy (WriterT, mapWriterT)
import qualified Control.Monad.Trans.Writer.Strict as Strict (WriterT, mapWriterT)
import Control.Monad.Trans.RWS.Lazy (RWST, mapRWST)
import qualified Control.Monad.Trans.RWS.Strict as Strict (RWST, mapRWST)
import Data.Aeson (FromJSON (..), ToJSON (..),
object)
import qualified Data.Aeson as A
import Data.Foldable as FT
import qualified Data.HashMap.Strict as HM
import Data.List
import qualified Data.Map.Strict as M
import Data.Semigroup as SG
import Data.String
import Data.String.Conv
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Lazy.Builder as B
import Data.Time
import GHC.Generics hiding (to)
#if MIN_VERSION_base(4, 8, 0)
#if !MIN_VERSION_base(4, 9, 0)
import GHC.SrcLoc
#endif
import GHC.Stack
#endif
import Language.Haskell.TH
import qualified Language.Haskell.TH.Syntax as TH
import Lens.Micro
import Lens.Micro.TH
import Network.HostName
#if mingw32_HOST_OS
import Katip.Compat
#else
import System.Posix
#endif
readMay :: Read a => String -> Maybe a
readMay s = case [x | (x,t) <- reads s, ("","") <- lex t] of
[x] -> Just x
[] -> Nothing
_ -> Nothing
newtype Namespace = Namespace { unNamespace :: [Text] }
deriving (Eq,Show,Read,Ord,Generic,ToJSON,FromJSON,SG.Semigroup,Monoid)
instance IsString Namespace where
fromString s = Namespace [fromString s]
intercalateNs :: Namespace -> [Text]
intercalateNs (Namespace xs) = intersperse "." xs
newtype Environment = Environment { getEnvironment :: Text }
deriving (Eq,Show,Read,Ord,Generic,ToJSON,FromJSON,IsString)
data Severity
= DebugS
| InfoS
| NoticeS
| WarningS
| ErrorS
| CriticalS
| AlertS
| EmergencyS
deriving (Eq, Ord, Show, Read, Generic, Enum, Bounded)
data Verbosity = V0 | V1 | V2 | V3
deriving (Eq, Ord, Show, Read, Generic, Enum)
renderSeverity :: Severity -> Text
renderSeverity s = case s of
DebugS -> "Debug"
InfoS -> "Info"
NoticeS -> "Notice"
WarningS -> "Warning"
ErrorS -> "Error"
CriticalS -> "Critical"
AlertS -> "Alert"
EmergencyS -> "Emergency"
textToSeverity :: Text -> Maybe Severity
textToSeverity = go . T.toLower
where
go "debug" = Just DebugS
go "info" = Just InfoS
go "notice" = Just NoticeS
go "warning" = Just WarningS
go "error" = Just ErrorS
go "critical" = Just CriticalS
go "alert" = Just AlertS
go "emergency" = Just EmergencyS
go _ = Nothing
instance ToJSON Severity where
toJSON s = A.String (renderSeverity s)
instance FromJSON Severity where
parseJSON = A.withText "Severity" parseSeverity
where
parseSeverity t = case textToSeverity t of
Just x -> return x
Nothing -> fail $ "Invalid Severity " ++ toS t
instance ToJSON Verbosity where
toJSON s = A.String $ case s of
V0 -> "V0"
V1 -> "V1"
V2 -> "V2"
V3 -> "V3"
instance FromJSON Verbosity where
parseJSON = A.withText "Verbosity" $ \s -> case s of
"V0" -> return V0
"V1" -> return V1
"V2" -> return V2
"V3" -> return V3
_ -> fail $ "Invalid Verbosity " ++ toS s
newtype LogStr = LogStr { unLogStr :: B.Builder }
deriving (Generic, Show, Eq)
instance IsString LogStr where
fromString = LogStr . B.fromString
instance Semigroup LogStr where
(LogStr a) <> (LogStr b) = LogStr (a <> b)
instance Monoid LogStr where
mappend = (<>)
mempty = LogStr mempty
instance FromJSON LogStr where
parseJSON = A.withText "LogStr" parseLogStr
where
parseLogStr = return . LogStr . B.fromText
logStr :: StringConv a Text => a -> LogStr
logStr t = LogStr (B.fromText $ toS t)
ls :: StringConv a Text => a -> LogStr
ls = logStr
showLS :: Show a => a -> LogStr
showLS = ls . show
newtype ThreadIdText = ThreadIdText {
getThreadIdText :: Text
} deriving (ToJSON, FromJSON, Show, Eq, Ord)
mkThreadIdText :: ThreadId -> ThreadIdText
mkThreadIdText = ThreadIdText . T.pack . show
data Item a = Item {
_itemApp :: Namespace
, _itemEnv :: Environment
, _itemSeverity :: Severity
, _itemThread :: ThreadIdText
, _itemHost :: HostName
, _itemProcess :: ProcessID
, _itemPayload :: a
, _itemMessage :: LogStr
, _itemTime :: UTCTime
, _itemNamespace :: Namespace
, _itemLoc :: Maybe Loc
} deriving (Generic, Functor)
makeLenses ''Item
instance Eq a => Eq (Item a) where
a == b = FT.and [ _itemApp a == _itemApp b
, _itemEnv a == _itemEnv b
, _itemSeverity a == _itemSeverity b
, _itemThread a == _itemThread b
, _itemHost a == _itemHost b
, _itemProcess a == _itemProcess b
, _itemPayload a == _itemPayload b
, _itemMessage a == _itemMessage b
, _itemTime a == _itemTime b
, _itemNamespace a == _itemNamespace b
, case (_itemLoc a, _itemLoc b) of
(Nothing, Nothing) -> True
(Just l1, Just l2) -> FT.and [ loc_filename l1 == loc_filename l2
, loc_package l1 == loc_package l2
, loc_module l1 == loc_module l2
, loc_start l1 == loc_start l2
, loc_end l1 == loc_end l2
]
_ -> False
]
instance Show a => Show (Item a) where
showsPrec d Item{..} = showParen (d >= 11) ( showString "Item {"
. field "_itemApp" _itemApp
. field "_itemEnv" _itemEnv
. field "_itemSeverity" _itemSeverity
. field "_itemThread" _itemThread
. field "_itemHost" _itemHost
. field "_itemProcess" _itemProcess
. field "_itemPayload" _itemPayload
. field "_itemMessage" _itemMessage
. field "_itemTime" _itemTime
. field "_itemNamespace" _itemNamespace
. showString "_itemLoc = " . shows (LocShow <$> _itemLoc)
. showChar '}'
)
where
field n v = showString n . showString " = " . shows v . showString ", "
newtype LocShow = LocShow Loc
instance Show LocShow where
showsPrec d (LocShow Loc{..}) = showParen (d >= 11) ( showString "Loc {"
. field "loc_filename" loc_filename
. field "loc_package" loc_package
. field "loc_module" loc_module
. field "loc_start" loc_start
. showString "loc_end = " . shows loc_end
. showChar '}'
)
where
field n v = showString n . showString " = " . shows v . showString ", "
instance ToJSON a => ToJSON (Item a) where
toJSON Item{..} = A.object
[ "app" A..= _itemApp
, "env" A..= _itemEnv
, "sev" A..= _itemSeverity
, "thread" A..= getThreadIdText _itemThread
, "host" A..= _itemHost
, "pid" A..= ProcessIDJs _itemProcess
, "data" A..= _itemPayload
, "msg" A..= (B.toLazyText $ unLogStr _itemMessage)
, "at" A..= _itemTime
, "ns" A..= _itemNamespace
, "loc" A..= fmap LocJs _itemLoc
]
newtype LocJs = LocJs { getLocJs :: Loc }
instance ToJSON LocJs where
toJSON (LocJs (Loc fn p m (l, c) _)) = A.object
[ "loc_fn" A..= fn
, "loc_pkg" A..= p
, "loc_mod" A..= m
, "loc_ln" A..= l
, "loc_col" A..= c
]
instance FromJSON LocJs where
parseJSON = A.withObject "LocJs" parseLocJs
where
parseLocJs o = do
fn <- o A..: "loc_fn"
p <- o A..: "loc_pkg"
m <- o A..: "loc_mod"
l <- o A..: "loc_ln"
c <- o A..: "loc_col"
return $ LocJs $ Loc fn p m (l, c) (l, c)
instance FromJSON a => FromJSON (Item a) where
parseJSON = A.withObject "Item" parseItem
where
parseItem o = Item
<$> o A..: "app"
<*> o A..: "env"
<*> o A..: "sev"
<*> o A..: "thread"
<*> o A..: "host"
<*> (getProcessIDJs <$> o A..: "pid")
<*> o A..: "data"
<*> o A..: "msg"
<*> o A..: "at"
<*> o A..: "ns"
<*> (fmap getLocJs <$> o A..: "loc")
processIDToText :: ProcessID -> Text
processIDToText = toS . show
textToProcessID :: Text -> Maybe ProcessID
textToProcessID = readMay . toS
newtype ProcessIDJs = ProcessIDJs {
getProcessIDJs :: ProcessID
}
instance ToJSON ProcessIDJs where
toJSON (ProcessIDJs p) = A.String (processIDToText p)
instance FromJSON ProcessIDJs where
parseJSON = A.withText "ProcessID" parseProcessID
where
parseProcessID t = case textToProcessID t of
Just p -> return $ ProcessIDJs p
Nothing -> fail $ "Invalid ProcessIDJs " ++ toS t
data PayloadSelection
= AllKeys
| SomeKeys [Text]
deriving (Show, Eq)
instance Semigroup PayloadSelection where
AllKeys <> _ = AllKeys
_ <> AllKeys = AllKeys
SomeKeys as <> SomeKeys bs = SomeKeys (as <> bs)
instance Monoid PayloadSelection where
mempty = SomeKeys []
mappend = (<>)
class ToObject a where
toObject :: a -> A.Object
default toObject :: ToJSON a => a -> A.Object
toObject v = case toJSON v of
A.Object o -> o
_ -> mempty
instance ToObject ()
instance ToObject A.Object
class ToObject a => LogItem a where
payloadKeys :: Verbosity -> a -> PayloadSelection
instance LogItem () where payloadKeys _ _ = SomeKeys []
data AnyLogPayload = forall a. ToJSON a => AnyLogPayload a
newtype SimpleLogPayload = SimpleLogPayload {
unSimpleLogPayload :: [(Text, AnyLogPayload)]
}
instance ToJSON SimpleLogPayload where
toJSON (SimpleLogPayload as) = object $ map go as
where go (k, AnyLogPayload v) = k A..= v
instance ToObject SimpleLogPayload
instance LogItem SimpleLogPayload where
payloadKeys V0 _ = SomeKeys []
payloadKeys _ _ = AllKeys
instance Semigroup SimpleLogPayload where
SimpleLogPayload a <> SimpleLogPayload b = SimpleLogPayload (a <> b)
instance Monoid SimpleLogPayload where
mempty = SimpleLogPayload []
mappend = (<>)
sl :: ToJSON a => Text -> a -> SimpleLogPayload
sl a b = SimpleLogPayload [(a, AnyLogPayload b)]
payloadObject :: LogItem a => Verbosity -> a -> A.Object
payloadObject verb a = case FT.foldMap (flip payloadKeys a) [(V0)..verb] of
AllKeys -> toObject a
SomeKeys ks -> HM.filterWithKey (\ k _ -> k `FT.elem` ks) $ toObject a
itemJson :: LogItem a => Verbosity -> Item a -> A.Value
itemJson verb a = toJSON $ a & itemPayload %~ payloadObject verb
data Scribe = Scribe {
liPush :: forall a. LogItem a => Item a -> IO ()
, scribeFinalizer :: IO ()
}
instance Semigroup Scribe where
(Scribe pushA finA) <> (Scribe pushB finB) =
Scribe (\item -> pushA item >> pushB item) (finA `finally` finB)
instance Monoid Scribe where
mempty = Scribe (const (return ())) (return ())
mappend = (<>)
data ScribeHandle = ScribeHandle {
shScribe :: Scribe
, shChan :: BQ.TBQueue WorkerMessage
}
data WorkerMessage where
NewItem :: LogItem a => Item a -> WorkerMessage
PoisonPill :: WorkerMessage
permitItem :: Severity -> Item a -> Bool
permitItem sev i = _itemSeverity i >= sev
data LogEnv = LogEnv {
_logEnvHost :: HostName
, _logEnvPid :: ProcessID
, _logEnvApp :: Namespace
, _logEnvEnv :: Environment
, _logEnvTimer :: IO UTCTime
, _logEnvScribes :: M.Map Text ScribeHandle
}
makeLenses ''LogEnv
initLogEnv
:: Namespace
-> Environment
-> IO LogEnv
initLogEnv an env = LogEnv
<$> getHostName
<*> getProcessID
<*> pure an
<*> pure env
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime, updateFreq = 1000 }
<*> pure mempty
registerScribe
:: Text
-> Scribe
-> ScribeSettings
-> LogEnv
-> IO LogEnv
registerScribe nm scribe ScribeSettings {..} le = do
queue <- atomically (BQ.newTBQueue (fromIntegral _scribeBufferSize))
worker <- spawnScribeWorker scribe queue
let fin = do
atomically (BQ.writeTBQueue queue PoisonPill)
void (Async.waitCatch worker)
void (scribeFinalizer scribe)
let sh = ScribeHandle (scribe { scribeFinalizer = fin }) queue
return (le & logEnvScribes %~ M.insert nm sh)
spawnScribeWorker :: Scribe -> BQ.TBQueue WorkerMessage -> IO (Async.Async ())
spawnScribeWorker (Scribe write _) queue = Async.async go
where
go = do
newCmd <- atomically (BQ.readTBQueue queue)
case newCmd of
NewItem a -> do
void (tryAny (write a))
go
PoisonPill -> return ()
data ScribeSettings = ScribeSettings {
_scribeBufferSize :: Int
}
deriving (Show, Eq)
makeLenses ''ScribeSettings
defaultScribeSettings :: ScribeSettings
defaultScribeSettings = ScribeSettings 4096
unregisterScribe
:: Text
-> LogEnv
-> LogEnv
unregisterScribe nm = logEnvScribes %~ M.delete nm
clearScribes
:: LogEnv
-> LogEnv
clearScribes = logEnvScribes .~ mempty
closeScribe
:: Text
-> LogEnv
-> IO LogEnv
closeScribe nm le = do
maybe (return ()) (scribeFinalizer . shScribe) (M.lookup nm (_logEnvScribes le))
return (le & logEnvScribes %~ M.delete nm)
closeScribes
:: LogEnv
-> IO LogEnv
closeScribes le = do
let actions = [void (closeScribe k le) | k <- M.keys (_logEnvScribes le)]
FT.foldr finally (return ()) actions
return (le & logEnvScribes .~ mempty)
class MonadIO m => Katip m where
getLogEnv :: m LogEnv
localLogEnv :: (LogEnv -> LogEnv) -> m a -> m a
instance Katip m => Katip (ReaderT s m) where
getLogEnv = lift getLogEnv
localLogEnv = mapReaderT . localLogEnv
#if !MIN_VERSION_either(4, 5, 0)
instance Katip m => Katip (EitherT s m) where
getLogEnv = lift getLogEnv
localLogEnv = mapEitherT . localLogEnv
#endif
instance Katip m => Katip (ExceptT s m) where
getLogEnv = lift getLogEnv
localLogEnv = mapExceptT . localLogEnv
instance Katip m => Katip (MaybeT m) where
getLogEnv = lift getLogEnv
localLogEnv = mapMaybeT . localLogEnv
instance Katip m => Katip (StateT s m) where
getLogEnv = lift getLogEnv
localLogEnv = mapStateT . localLogEnv
instance (Katip m, Monoid w) => Katip (RWST r w s m) where
getLogEnv = lift getLogEnv
localLogEnv = mapRWST . localLogEnv
instance (Katip m, Monoid w) => Katip (Strict.RWST r w s m) where
getLogEnv = lift getLogEnv
localLogEnv = Strict.mapRWST . localLogEnv
instance Katip m => Katip (Strict.StateT s m) where
getLogEnv = lift getLogEnv
localLogEnv = Strict.mapStateT . localLogEnv
instance (Katip m, Monoid s) => Katip (WriterT s m) where
getLogEnv = lift getLogEnv
localLogEnv = mapWriterT . localLogEnv
instance (Katip m, Monoid s) => Katip (Strict.WriterT s m) where
getLogEnv = lift getLogEnv
localLogEnv = Strict.mapWriterT . localLogEnv
instance (Katip m) => Katip (ResourceT m) where
getLogEnv = lift getLogEnv
localLogEnv = transResourceT . localLogEnv
newtype KatipT m a = KatipT { unKatipT :: ReaderT LogEnv m a }
deriving ( Functor, Applicative, Monad, MonadIO
, MonadMask, MonadCatch, MonadThrow, MonadTrans, MonadBase b)
instance MonadIO m => Katip (KatipT m) where
getLogEnv = KatipT ask
localLogEnv f (KatipT m) = KatipT $ local f m
instance MonadTransControl KatipT where
type StT (KatipT) a = a
liftWith f = KatipT $ ReaderT $ \le -> f $ \t -> runKatipT le t
restoreT = KatipT . ReaderT . const
{-# INLINABLE liftWith #-}
{-# INLINABLE restoreT #-}
instance (MonadBaseControl b m) => MonadBaseControl b (KatipT m) where
type StM ((KatipT) m) a = ComposeSt (KatipT) m a
liftBaseWith = defaultLiftBaseWith
restoreM = defaultRestoreM
instance MonadUnliftIO m => MonadUnliftIO (KatipT m) where
askUnliftIO = KatipT $
withUnliftIO $ \u ->
pure (UnliftIO (unliftIO u . unKatipT))
runKatipT :: LogEnv -> KatipT m a -> m a
runKatipT le (KatipT f) = runReaderT f le
katipNoLogging
:: ( Katip m
)
=> m a
-> m a
katipNoLogging = localLogEnv (\le -> set logEnvScribes mempty le)
logItem
:: (A.Applicative m, LogItem a, Katip m)
=> a
-> Namespace
-> Maybe Loc
-> Severity
-> LogStr
-> m ()
logItem a ns loc sev msg = do
LogEnv{..} <- getLogEnv
liftIO $ do
item <- Item
<$> pure _logEnvApp
<*> pure _logEnvEnv
<*> pure sev
<*> (mkThreadIdText <$> myThreadId)
<*> pure _logEnvHost
<*> pure _logEnvPid
<*> pure a
<*> pure msg
<*> _logEnvTimer
<*> pure (_logEnvApp <> ns)
<*> pure loc
FT.forM_ (M.elems _logEnvScribes) $ \ ScribeHandle {..} -> atomically (tryWriteTBQueue shChan (NewItem item))
tryWriteTBQueue
:: TBQueue a
-> a
-> STM Bool
tryWriteTBQueue q a = do
full <- isFullTBQueue q
unless full (writeTBQueue q a)
return (not full)
logF
:: (Applicative m, LogItem a, Katip m)
=> a
-> Namespace
-> Severity
-> LogStr
-> m ()
logF a ns sev msg = logItem a ns Nothing sev msg
logException
:: (Katip m, LogItem a, MonadCatch m, Applicative m)
=> a
-> Namespace
-> Severity
-> m b
-> m b
logException a ns sev action = action `catchAny` \e -> f e >> throwM e
where
f e = logF a ns sev (msg e)
msg e = ls (T.pack "An exception has occured: ") <> showLS e
logMsg
:: (Applicative m, Katip m)
=> Namespace
-> Severity
-> LogStr
-> m ()
logMsg ns sev msg = logF () ns sev msg
instance TH.Lift Namespace where
lift (Namespace xs) =
let xs' = map T.unpack xs
in [| Namespace (map T.pack xs') |]
instance TH.Lift Verbosity where
lift V0 = [| V0 |]
lift V1 = [| V1 |]
lift V2 = [| V2 |]
lift V3 = [| V3 |]
instance TH.Lift Severity where
lift DebugS = [| DebugS |]
lift InfoS = [| InfoS |]
lift NoticeS = [| NoticeS |]
lift WarningS = [| WarningS |]
lift ErrorS = [| ErrorS |]
lift CriticalS = [| CriticalS |]
lift AlertS = [| AlertS |]
lift EmergencyS = [| EmergencyS |]
liftLoc :: Loc -> Q Exp
liftLoc (Loc a b c (d1, d2) (e1, e2)) = [|Loc
$(TH.lift a)
$(TH.lift b)
$(TH.lift c)
($(TH.lift d1), $(TH.lift d2))
($(TH.lift e1), $(TH.lift e2))
|]
#if MIN_VERSION_base(4, 8, 0)
getLoc :: HasCallStack => Maybe Loc
getLoc = case getCallStack callStack of
[] -> Nothing
xs -> Just . toLoc . head $ filter filterKatip xs
where
filterKatip :: (String, SrcLoc) -> Bool
filterKatip (_, srcloc) = not $
"katip-" `isPrefixOf` srcLocPackage srcloc
toLoc :: (String, SrcLoc) -> Loc
toLoc (_, l) = Loc {
loc_filename = srcLocFile l
, loc_package = srcLocPackage l
, loc_module = srcLocModule l
, loc_start = (srcLocStartLine l, srcLocStartCol l)
, loc_end = (srcLocEndLine l, srcLocEndCol l)
}
#else
getLoc :: Maybe Loc
getLoc = Nothing
#endif
getLocTH :: ExpQ
getLocTH = [| $(location >>= liftLoc) |]
logT :: ExpQ
logT = [| \ a ns sev msg -> logItem a ns (Just $(getLocTH)) sev msg |]
#if MIN_VERSION_base(4, 8, 0)
logLoc :: (Applicative m, LogItem a, Katip m, HasCallStack)
#else
logLoc :: (Applicative m, LogItem a, Katip m)
#endif
=> a
-> Namespace
-> Severity
-> LogStr
-> m ()
logLoc a ns = logItem a ns getLoc
locationToString :: Loc -> String
locationToString loc = (loc_package loc) ++ ':' : (loc_module loc) ++
' ' : (loc_filename loc) ++ ':' : (line loc) ++ ':' : (char loc)
where
line = show . fst . loc_start
char = show . snd . loc_start