{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE TypeFamilies #-}
module Colog.Message
(
Msg (..)
, Message
, log
, logDebug
, logInfo
, logWarning
, logError
, logException
, fmtMessage
, showSeverity
, showSourceLoc
, FieldType
, MessageField (..)
, unMessageField
, extractField
, FieldMap
, defaultFieldMap
, RichMessage (..)
, fmtRichMessageDefault
, upgradeMessageAction
) where
import Prelude hiding (log)
import Control.Concurrent (ThreadId, myThreadId)
import Control.Exception (Exception, displayException)
import Control.Monad.IO.Class (MonadIO (..))
import Data.Kind (Type)
import Data.Semigroup ((<>))
import Data.Text (Text)
import Data.Text.Lazy (toStrict)
import Data.TypeRepMap (TypeRepMap)
import GHC.Exts (IsList (..))
import GHC.OverloadedLabels (IsLabel (..))
import GHC.Stack (CallStack, SrcLoc (..), callStack, getCallStack, withFrozenCallStack)
import GHC.TypeLits (KnownSymbol, Symbol)
import System.Console.ANSI (Color (..), ColorIntensity (Vivid), ConsoleLayer (Foreground), SGR (..),
setSGRCode)
import Colog.Core (LogAction, Severity (..), cmap)
import Colog.Monad (WithLog, logMsg)
import qualified Chronos as C
import qualified Data.Text as T
import qualified Data.Text.Lazy.Builder as TB
import qualified Data.TypeRepMap as TM
data Msg sev = Msg
{ msgSeverity :: !sev
, msgStack :: !CallStack
, msgText :: !Text
}
type Message = Msg Severity
log :: WithLog env (Msg sev) m => sev -> Text -> m ()
log msgSeverity msgText =
withFrozenCallStack (logMsg Msg{ msgStack = callStack, .. })
logDebug :: WithLog env Message m => Text -> m ()
logDebug = withFrozenCallStack (log Debug)
logInfo :: WithLog env Message m => Text -> m ()
logInfo = withFrozenCallStack (log Info)
logWarning :: WithLog env Message m => Text -> m ()
logWarning = withFrozenCallStack (log Warning)
logError :: WithLog env Message m => Text -> m ()
logError = withFrozenCallStack (log Error)
logException :: forall e m env . (WithLog env Message m, Exception e) => e -> m ()
logException = withFrozenCallStack (logError . T.pack . displayException)
fmtMessage :: Message -> Text
fmtMessage Msg{..} =
showSeverity msgSeverity
<> showSourceLoc msgStack
<> msgText
showSeverity :: Severity -> Text
showSeverity = \case
Debug -> color Green "[Debug] "
Info -> color Blue "[Info] "
Warning -> color Yellow "[Warning] "
Error -> color Red "[Error] "
where
color :: Color -> Text -> Text
color c txt =
T.pack (setSGRCode [SetColor Foreground Vivid c])
<> txt
<> T.pack (setSGRCode [Reset])
square :: Text -> Text
square t = "[" <> t <> "] "
showSourceLoc :: CallStack -> Text
showSourceLoc cs = square showCallStack
where
showCallStack :: Text
showCallStack = case getCallStack cs of
[] -> "<unknown loc>"
[(name, loc)] -> showLoc name loc
(_, loc) : (callerName, _) : _ -> showLoc callerName loc
showLoc :: String -> SrcLoc -> Text
showLoc name SrcLoc{..} =
T.pack srcLocModule <> "." <> T.pack name <> "#" <> T.pack (show srcLocStartLine)
type family FieldType (fieldName :: Symbol) :: Type
type instance FieldType "threadId" = ThreadId
type instance FieldType "posixTime" = C.Time
newtype MessageField (m :: Type -> Type) (fieldName :: Symbol) where
MessageField :: forall fieldName m . m (FieldType fieldName) -> MessageField m fieldName
unMessageField :: forall fieldName m . MessageField m fieldName -> m (FieldType fieldName)
unMessageField (MessageField f) = f
{-# INLINE unMessageField #-}
instance (KnownSymbol fieldName, a ~ m (FieldType fieldName))
=> IsLabel fieldName (a -> TM.WrapTypeable (MessageField m)) where
#if MIN_VERSION_base(4,11,0)
fromLabel field = TM.WrapTypeable $ MessageField @fieldName field
#else
fromLabel field = TM.WrapTypeable $ MessageField @_ @fieldName field
#endif
{-# INLINE fromLabel #-}
extractField
:: Applicative m
=> Maybe (MessageField m fieldName)
-> m (Maybe (FieldType fieldName))
extractField = traverse unMessageField
{-# INLINE extractField #-}
type FieldMap (m :: Type -> Type) = TypeRepMap (MessageField m)
defaultFieldMap :: MonadIO m => FieldMap m
defaultFieldMap = fromList
[ #threadId (liftIO myThreadId)
, #posixTime (liftIO C.now)
]
data RichMessage (m :: Type -> Type) = RichMessage
{ richMessageMsg :: {-# UNPACK #-} !Message
, richMessageMap :: {-# UNPACK #-} !(FieldMap m)
}
fmtRichMessageDefault :: MonadIO m => RichMessage m -> m Text
fmtRichMessageDefault RichMessage{..} = do
maybeThreadId <- extractField $ TM.lookup @"threadId" richMessageMap
maybePosixTime <- extractField $ TM.lookup @"posixTime" richMessageMap
pure $ formatRichMessage maybeThreadId maybePosixTime richMessageMsg
where
formatRichMessage :: Maybe ThreadId -> Maybe C.Time -> Message -> Text
formatRichMessage (maybe "" showThreadId -> thread) (maybe "" showTime -> time) Msg{..} =
showSeverity msgSeverity
<> time
<> showSourceLoc msgStack
<> thread
<> msgText
showTime :: C.Time -> Text
showTime t =
square
$ toStrict
$ TB.toLazyText
$ C.builder_DmyHMS timePrecision datetimeFormat (C.timeToDatetime t)
where
timePrecision = C.SubsecondPrecisionFixed 3
datetimeFormat = C.DatetimeFormat (Just '-') (Just ' ') (Just ':')
showThreadId :: ThreadId -> Text
showThreadId = square . T.pack . show
upgradeMessageAction
:: forall m .
FieldMap m
-> LogAction m (RichMessage m)
-> LogAction m Message
upgradeMessageAction fieldMap = cmap addMap
where
addMap :: Message -> RichMessage m
addMap msg = RichMessage msg fieldMap