unbeliever-0.7.3.0: lib/Core/Program/Logging.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE BangPatterns #-}
{-# OPTIONS_HADDOCK prune #-}
module Core.Program.Logging
(
putMessage
, Verbosity(..)
, event
, debug
, debugS
, debugR
) where
import Chrono.TimeStamp (TimeStamp(..), getCurrentTimeNanoseconds)
import Control.Concurrent.MVar (readMVar)
import Control.Concurrent.STM (atomically)
import Control.Concurrent.STM.TQueue (writeTQueue)
import Control.Exception (evaluate)
import Control.Monad (when)
import Control.Monad.Reader.Class (MonadReader(ask))
import Data.Fixed
import Data.Hourglass (timePrint, TimeFormatElem(..))
import qualified Data.Text.Short as S (replicate)
import Core.Text.Rope
import Core.Text.Utilities
import Core.System.Base
import Core.Program.Context
{-
class Monad m => MonadLog a m where
logMessage :: Monoid a => Severity -> a -> m ()
-}
putMessage :: Context τ -> Message -> IO ()
putMessage context message@(Message now _ text potentialValue) = do
let start = startTimeFrom context
let output = outputChannelFrom context
let logger = loggerChannelFrom context
let display = case potentialValue of
Just value ->
if contains '\n' value
then text <> " =\n" <> value
else text <> " = " <> value
Nothing -> text
let result = formatLogMessage start now display
atomically $ do
writeTQueue output result
writeTQueue logger message
formatLogMessage :: TimeStamp -> TimeStamp -> Rope -> Rope
formatLogMessage start now message =
let
start' = unTimeStamp start
now' = unTimeStamp now
stampZ = timePrint
[ Format_Hour
, Format_Text ':'
, Format_Minute
, Format_Text ':'
, Format_Second
, Format_Text 'Z'
] now
-- I hate doing math in Haskell
elapsed = fromRational (toRational (now' - start') / 1e9) :: Fixed E3
in
mconcat
[ intoRope stampZ
, " ("
, padWithZeros 9 (show elapsed)
, ") "
, message
]
--
-- | Utility function to prepend \'0\' characters to a string representing a
-- number.
--
{-
Cloned from **locators** package Data.Locators.Hashes, BSD3 licence
-}
padWithZeros :: Int -> String -> Rope
padWithZeros digits str =
intoRope pad <> intoRope str
where
pad = S.replicate len "0"
len = digits - length str
{-|
Note a significant event, state transition, status, or debugging
message. This:
@
'event' "Starting..."
@
will result in
> 13:05:55Z (0000.001) Starting...
appearing on stdout /and/ the message being sent down the logging
channel. The output string is current time in UTC, and time elapsed
since startup shown to the nearest millisecond (our timestamps are to
nanosecond precision, but you don't need that kind of resolution in
in ordinary debugging).
Messages sent to syslog will be logged at @Info@ level severity.
-}
event :: Rope -> Program τ ()
event text = do
context <- ask
liftIO $ do
level <- readMVar (verbosityLevelFrom context)
when (isEvent level) $ do
now <- getCurrentTimeNanoseconds
putMessage context (Message now Event text Nothing)
isEvent :: Verbosity -> Bool
isEvent level = case level of
Output -> False
Event -> True
Debug -> True
isDebug :: Verbosity -> Bool
isDebug level = case level of
Output -> False
Event -> False
Debug -> True
{-|
Output a debugging message formed from a label and a value. This is like
'event' above but for the (rather common) case of needing to inspect or
record the value of a variable when debugging code. This:
@
'setProgramName' \"hello\"
name <- 'getProgramName'
'debug' \"programName\" name
@
will result in
> 13:05:58Z (0003.141) programName = hello
appearing on stdout /and/ the message being sent down the logging channel,
assuming these actions executed about three seconds after program start.
Messages sent to syslog will be logged at @Debug@ level severity.
-}
debug :: Rope -> Rope -> Program τ ()
debug label value = do
context <- ask
liftIO $ do
level <- readMVar (verbosityLevelFrom context)
when (isDebug level) $ do
now <- getCurrentTimeNanoseconds
!value' <- evaluate value
putMessage context (Message now Debug label (Just value'))
{-|
Convenience for the common case of needing to inspect the value
of a general variable which has a 'Show' instance
-}
debugS :: Show α => Rope -> α -> Program τ ()
debugS label value = debug label (intoRope (show value))
{-|
Convenience for the common case of needing to inspect the value of a
general variable for which there is a 'Render' instance and so can pretty
print the supplied argument to the log. This will pass the detected
terminal width to the 'render' function, resulting in appopriate line
wrapping when rendering your value (if logging to something other than
console the default width of @80@ will be applied).
-}
debugR :: Render α => Rope -> α -> Program τ ()
debugR label thing = do
context <- ask
liftIO $ do
level <- readMVar (verbosityLevelFrom context)
when (isDebug level) $ do
now <- getCurrentTimeNanoseconds
let columns = terminalWidthFrom context
-- TODO take into account 22 width already consumed by timestamp
-- TODO move render to putMessage? putMessageR?
let value = render columns thing
!value' <- evaluate value
putMessage context (Message now Debug label (Just value'))