{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-| Module : Colog.Polysemy.Formatting.Render Description : Render log messages. -} module Colog.Polysemy.Formatting.Render ( renderThreadTimeMessage , renderThreadTimeMessageShort , fIso8601Tz , fSeverity , fThread , fCallerFromStack , fCaller , fCallerLong' , fCallerLong , fCallerShort' , fCallerShort ) where import Colog (Msg(..), Severity(..)) import Control.Concurrent (ThreadId) import Data.Char (isUpper) import Data.Function ((&)) import qualified Data.Text as T import Data.Text.Lazy.Builder (Builder) import Data.Time (FormatTime, utcToZonedTime) import Formatting import Formatting.Time import GHC.Stack (CallStack, SrcLoc(..), getCallStack) import Colog.Polysemy.Formatting.Color (Color(..), UseColor, getWithFG) import Colog.Polysemy.Formatting.LogEnv (LogEnv(..)) import Colog.Polysemy.Formatting.ThreadTimeMessage (ThreadTimeMessage(..)) -- | Render the message, optionally in color, with green " | " separating fields, and these fields: -- -- * Severity (e.g. "INFO", see 'fSeverity'), -- * Timestamp (e.g. "2020-10-13T16:58:43.982720690+1100", see 'fIso8601Tz'), -- * Thread Id (e.g. "Thread 8", see 'fThread'), -- * Caller (e.g. "MyApp.CLI.cliMain#43", see 'fCallerLong'), and -- * The log message itself. -- -- E.g: @"INFO | 2020-10-13T17:06:52.408921221+1100 | Thread 8 | MyApp.CLI.cliMain#43 | MyApp version 0.1.0.0"@ -- -- The first three columns are fixed-width, which makes visual scanning of the log easier. renderThreadTimeMessage :: LogEnv -> ThreadTimeMessage -> T.Text renderThreadTimeMessage = renderThreadTimeMessage' fCallerLong -- | Like 'renderThreadTimeMessage', but abbreviate the caller by removing lowercase letters from the module name. renderThreadTimeMessageShort :: LogEnv -> ThreadTimeMessage -> T.Text renderThreadTimeMessageShort = renderThreadTimeMessage' fCallerShort renderThreadTimeMessage' :: ((Color -> Builder -> Builder) -> Format Builder (CallStack -> Builder)) -> LogEnv -> ThreadTimeMessage -> T.Text renderThreadTimeMessage' renderCaller (LogEnv useColor zone) (ThreadTimeMessage threadId time (Msg severity stack message)) = let withFG = getWithFG useColor in sformat (fFieldsGreenBarSep useColor) [ bformat (fSeverity withFG) severity , bformat (fIso8601Tz withFG) (utcToZonedTime zone time) , bformat fThread threadId , bformat (renderCaller withFG) stack , bformat stext message ] fFieldsGreenBarSep :: UseColor -> Format r ([Builder] -> r) fFieldsGreenBarSep useColor = later $ \fields -> let withFG = getWithFG useColor sep = format builder $ withFG Green " | " in bformat (intercalated sep builder) fields -- | Render a timestamp in ISO-8601 format, in color, to 9 decimal places, -- e.g.: "2020-10-13T16:58:43.982720690+1100" -- -- The "T" is rendered in green, the time in yellow, the rest without color. fIso8601Tz :: FormatTime a => (Color -> Builder -> Builder) -> Format r (a -> r) fIso8601Tz withFG = later $ \time -> mconcat [ bformat dateDash time , withFG Green "T" , withFG Yellow $ bformat hmsL time , withFG Yellow $ bformat (right 10 '0') (bformat decimals time) , bformat tz time ] -- | Render the 'Severity' of the message, with color, using 4 characters to maintain alignment: -- -- * DBUG in green, -- * INFO in blue, -- * WARN in yellow, or -- * ERR in red. fSeverity :: (Color -> Builder -> Builder) -> Format r (Severity -> r) fSeverity withFG = later $ \case Debug -> withFG Green "DBUG" Info -> withFG Blue "INFO" Warning -> withFG Yellow "WARN" Error -> withFG Red "ERR " -- | Render the Id of the thread that the log message was generated in, -- with a fixed width, at least until the thread Ids exceed 100,000, -- e.g. "Thread 97". fThread :: Format r (ThreadId -> r) fThread = later $ \tid -> let s = show tid in bformat ("Thread " % left 5 ' ') (drop 9 s) fCallerFromStack :: Format r (Maybe (String, SrcLoc) -> r) -> Format r (CallStack -> r) fCallerFromStack = mapf callStackLoc where callStackLoc :: CallStack -> Maybe (String, SrcLoc) callStackLoc cs = case getCallStack cs of [] -> Nothing [(name, loc)] -> Just (name, loc) (_, loc) : (callerName, _) : _ -> Just (callerName, loc) fCaller :: (Color -> Builder -> Builder) -> Format r (String -> String -> Int -> r) fCaller withFG = string % "." % colored Cyan string % "#" % colored Red int where colored c f = later $ \input -> bformat f input & withFG c fCallerLong' :: (Color -> Builder -> Builder) -> Format r (Maybe (String, SrcLoc) -> r) fCallerLong' withFG = maybed "" $ later $ \(name, SrcLoc{..}) -> bformat (fCaller withFG) srcLocModule name srcLocStartLine -- | Render the fully qualified function that called the log function, -- and line number in the source file, e.g. "MyApp.CLI.cliMain#43", -- with the function name in cyan and line number in red. fCallerLong :: (Color -> Builder -> Builder) -> Format r (CallStack -> r) fCallerLong withFG = fCallerFromStack (fCallerLong' withFG) fCallerShort' :: (Color -> Builder -> Builder) -> Format r (Maybe (String, SrcLoc) -> r) fCallerShort' withFG = maybed "?" $ later $ \(name, SrcLoc{..}) -> bformat (fCaller withFG) (abbreviateModule srcLocModule) name srcLocStartLine where abbreviateModule = filter (\c -> isUpper c || c == '.') -- | Render the fully qualified function that called the log function, -- and line number in the source file, abbreviating the module path by -- removing lower-case letters, e.g. "MA.CLI.cliMain#43", -- with the function name in cyan and line number in red. fCallerShort :: (Color -> Builder -> Builder) -> Format r (CallStack -> r) fCallerShort withFG = fCallerFromStack (fCallerShort' withFG)