packages feed

extensible-effects-concurrent-0.21.0: test/LogMessageIdeaTest.hs

module LogMessageIdeaTest where

import           Control.Concurrent
import           Control.DeepSeq
import           Control.Lens
import           Data.Default
import           Data.Maybe
import qualified Data.Text                     as T
import           Data.Time.Clock
import           Data.Time.Format
import           GHC.Generics            hiding ( to )
import           GHC.Stack
import           System.FilePath.Posix
import           Text.Printf
import           Control.Eff
import           Control.Eff.Reader.Lazy


-- | A message data type inspired by the RFC-5424 Syslog Protocol
data LogMessage =
  MkLogMessage { _lmFacility :: !Facility
               , _lmSeverity :: !Severity
               , _lmTimestamp :: (Maybe UTCTime)
               , _lmHostname :: (Maybe T.Text)
               , _lmAppName :: (Maybe T.Text)
               , _lmProcessId :: (Maybe T.Text)
               , _lmMessageId :: (Maybe T.Text)
               , _lmStructuredData :: [StructuredDataElement]
               , _lmThreadId :: (Maybe ThreadId)
               , _lmSrcLoc :: (Maybe SrcLoc)
               , _lmMessage :: T.Text}
  deriving (Eq, Generic)

instance Default LogMessage where
  def = MkLogMessage def def def def def def def def def def ""

instance NFData LogMessage

-- | RFC-5424 defines how structured data can be included in a log message.
data StructuredDataElement =
  SdElement { _sdElementId :: !T.Text
            , _sdElementParameters :: ![SdParameter]}
  deriving (Eq, Ord, Generic, Show)


renderSdElement :: StructuredDataElement -> T.Text
renderSdElement (SdElement sdId params) = "[" <> sdName sdId <> if null params
  then ""
  else " " <> T.unwords (renderSdParameter <$> params) <> "]"

instance NFData StructuredDataElement

-- | Component of an RFC-5424 'StructuredDataElement'
data SdParameter =
  MkSdParameter !T.Text !T.Text
  deriving (Eq, Ord, Generic, Show)

renderSdParameter :: SdParameter -> T.Text
renderSdParameter (MkSdParameter k v) =
  sdName k <> "=\"" <> sdParamValue v <> "\""

-- | Extract the name of an 'SdParameter' the length is cropped to 32 according to RFC 5424.
sdName :: T.Text -> T.Text
sdName =
  T.take 32 . T.filter (\c -> c == '=' || c == ']' || c == ' ' || c == '"')

-- | Extract the value of an 'SdParameter'.
sdParamValue :: T.Text -> T.Text
sdParamValue = T.concatMap $ \case
  '"'  -> "\\\""
  '\\' -> "\\\\"
  ']'  -> "\\]"
  x    -> T.singleton x

instance NFData SdParameter

-- | An rfc 5424 severity
newtype Severity =
  Severity {fromSeverity :: Int}
  deriving (Eq, Ord, Generic, NFData)

instance Show Severity where
  show (Severity 1) = "ALERT    "
  show (Severity 2) = "CRITICAL "
  show (Severity 3) = "ERROR    "
  show (Severity 4) = "WARNING  "
  show (Severity 5) = "NOTICE   "
  show (Severity 6) = "INFO     "
  show (Severity x) | x <= 0    = "EMERGENCY"
                    | otherwise = "DEBUG    "
--  *** Severities

-- | Smart constructor for the RFC-5424 __emergency__ 'LogMessage' 'Severity'.
-- This corresponds to the severity value __0__.
-- See 'lmSeverity'.
emergencySeverity :: Severity
emergencySeverity = Severity 0

-- | Smart constructor for the RFC-5424 __alert__ 'LogMessage' 'Severity'.
-- This corresponds to the severity value __1__.
-- See 'lmSeverity'.
alertSeverity :: Severity
alertSeverity = Severity 1

-- | Smart constructor for the RFC-5424 __critical__ 'LogMessage' 'Severity'.
-- This corresponds to the severity value __2__.
-- See 'lmSeverity'.
criticalSeverity :: Severity
criticalSeverity = Severity 2

-- | Smart constructor for the RFC-5424 __error__ 'LogMessage' 'Severity'.
-- This corresponds to the severity value __3__.
-- See 'lmSeverity'.
errorSeverity :: Severity
errorSeverity = Severity 3

-- | Smart constructor for the RFC-5424 __warning__ 'LogMessage' 'Severity'.
-- This corresponds to the severity value __4__.
-- See 'lmSeverity'.
warningSeverity :: Severity
warningSeverity = Severity 4

-- | Smart constructor for the RFC-5424 __notice__ 'LogMessage' 'Severity'.
-- This corresponds to the severity value __5__.
-- See 'lmSeverity'.
noticeSeverity :: Severity
noticeSeverity = Severity 5

-- | Smart constructor for the RFC-5424 __informational__ 'LogMessage' 'Severity'.
-- This corresponds to the severity value __6__.
-- See 'lmSeverity'.
informationalSeverity :: Severity
informationalSeverity = Severity 6

-- | Smart constructor for the RFC-5424 __debug__ 'LogMessage' 'Severity'.
-- This corresponds to the severity value __7__.
-- See 'lmSeverity'.
debugSeverity :: Severity
debugSeverity = Severity 7

instance Default Severity where
  def = debugSeverity


-- | An rfc 5424 facility
newtype Facility = Facility {fromFacility :: Int}
  deriving (Eq, Ord, Show, Generic, NFData)

-- | Smart constructor for the RFC-5424 'LogMessage' facility @kernelMessages@.
-- See 'lmFacility'.
kernelMessages :: Facility
kernelMessages = Facility 0

-- | Smart constructor for the RFC-5424 'LogMessage' facility @userLevelMessages@.
-- See 'lmFacility'.
userLevelMessages :: Facility
userLevelMessages = Facility 1

-- | Smart constructor for the RFC-5424 'LogMessage' facility @mailSystem@.
-- See 'lmFacility'.
mailSystem :: Facility
mailSystem = Facility 2

-- | Smart constructor for the RFC-5424 'LogMessage' facility @systemDaemons@.
-- See 'lmFacility'.
systemDaemons :: Facility
systemDaemons = Facility 3

-- | Smart constructor for the RFC-5424 'LogMessage' facility @securityAuthorizationMessages4@.
-- See 'lmFacility'.
securityAuthorizationMessages4 :: Facility
securityAuthorizationMessages4 = Facility 4

-- | Smart constructor for the RFC-5424 'LogMessage' facility @linePrinterSubsystem@.
-- See 'lmFacility'.
linePrinterSubsystem :: Facility
linePrinterSubsystem = Facility 6

-- | Smart constructor for the RFC-5424 'LogMessage' facility @networkNewsSubsystem@.
-- See 'lmFacility'.
networkNewsSubsystem :: Facility
networkNewsSubsystem = Facility 7

-- | Smart constructor for the RFC-5424 'LogMessage' facility @uucpSubsystem@.
-- See 'lmFacility'.
uucpSubsystem :: Facility
uucpSubsystem = Facility 8

-- | Smart constructor for the RFC-5424 'LogMessage' facility @clockDaemon@.
-- See 'lmFacility'.
clockDaemon :: Facility
clockDaemon = Facility 9

-- | Smart constructor for the RFC-5424 'LogMessage' facility @securityAuthorizationMessages10@.
-- See 'lmFacility'.
securityAuthorizationMessages10 :: Facility
securityAuthorizationMessages10 = Facility 10

-- | Smart constructor for the RFC-5424 'LogMessage' facility @ftpDaemon@.
-- See 'lmFacility'.
ftpDaemon :: Facility
ftpDaemon = Facility 11

-- | Smart constructor for the RFC-5424 'LogMessage' facility @ntpSubsystem@.
-- See 'lmFacility'.
ntpSubsystem :: Facility
ntpSubsystem = Facility 12

-- | Smart constructor for the RFC-5424 'LogMessage' facility @logAuditFacility@.
-- See 'lmFacility'.
logAuditFacility :: Facility
logAuditFacility = Facility 13

-- | Smart constructor for the RFC-5424 'LogMessage' facility @logAlertFacility@.
-- See 'lmFacility'.
logAlertFacility :: Facility
logAlertFacility = Facility 14

-- | Smart constructor for the RFC-5424 'LogMessage' facility @clockDaemon2@.
-- See 'lmFacility'.
clockDaemon2 :: Facility
clockDaemon2 = Facility 15

-- | Smart constructor for the RFC-5424 'LogMessage' facility @local0@.
-- See 'lmFacility'.
local0 :: Facility
local0 = Facility 16

-- | Smart constructor for the RFC-5424 'LogMessage' facility @local1@.
-- See 'lmFacility'.
local1 :: Facility
local1 = Facility 17

-- | Smart constructor for the RFC-5424 'LogMessage' facility @local2@.
-- See 'lmFacility'.
local2 :: Facility
local2 = Facility 18

-- | Smart constructor for the RFC-5424 'LogMessage' facility @local3@.
-- See 'lmFacility'.
local3 :: Facility
local3 = Facility 19

-- | Smart constructor for the RFC-5424 'LogMessage' facility @local4@.
-- See 'lmFacility'.
local4 :: Facility
local4 = Facility 20

-- | Smart constructor for the RFC-5424 'LogMessage' facility @local5@.
-- See 'lmFacility'.
local5 :: Facility
local5 = Facility 21

-- | Smart constructor for the RFC-5424 'LogMessage' facility @local6@.
-- See 'lmFacility'.
local6 :: Facility
local6 = Facility 22

-- | Smart constructor for the RFC-5424 'LogMessage' facility @local7@.
-- See 'lmFacility'.
local7 :: Facility
local7 = Facility 23

instance Default Facility where
  def = local7

makeLensesWith (lensRules & generateSignatures .~ False) ''StructuredDataElement

-- | A lens for 'SdParameter's
sdElementParameters
  :: Functor f
  => ([SdParameter] -> f [SdParameter])
  -> StructuredDataElement
  -> f StructuredDataElement

-- | A lens for the key or ID of a group of RFC 5424 key-value pairs.
sdElementId
  :: Functor f
  => (T.Text -> f T.Text)
  -> StructuredDataElement
  -> f StructuredDataElement

makeLensesWith (lensRules & generateSignatures .~ False) ''LogMessage

-- | A lens for the UTC time of a 'LogMessage'
-- The function 'setLogMessageTimestamp' can be used to set the field.
lmTimestamp
  :: Functor f
  => (Maybe UTCTime -> f (Maybe UTCTime))
  -> LogMessage
  -> f LogMessage

-- | A lens for the 'ThreadId' of a 'LogMessage'
-- The function 'setLogMessageThreadId' can be used to set the field.
lmThreadId
  :: Functor f
  => (Maybe ThreadId -> f (Maybe ThreadId))
  -> LogMessage
  -> f LogMessage

-- | A lens for the 'StructuredDataElement' of a 'LogMessage'
lmStructuredData
  :: Functor f
  => ([StructuredDataElement] -> f [StructuredDataElement])
  -> LogMessage
  -> f LogMessage

-- | A lens for the 'SrcLoc' of a 'LogMessage'
lmSrcLoc
  :: Functor f
  => (Maybe SrcLoc -> f (Maybe SrcLoc))
  -> LogMessage
  -> f LogMessage

-- | A lens for the 'Severity' of a 'LogMessage'
lmSeverity
  :: Functor f => (Severity -> f Severity) -> LogMessage -> f LogMessage

-- | A lens for a user defined of /process/ id of a 'LogMessage'
lmProcessId
  :: Functor f
  => (Maybe T.Text -> f (Maybe T.Text))
  -> LogMessage
  -> f LogMessage

-- | A lens for a user defined /message id/ of a 'LogMessage'
lmMessageId
  :: Functor f
  => (Maybe T.Text -> f (Maybe T.Text))
  -> LogMessage
  -> f LogMessage

-- | A lens for the user defined textual message of a 'LogMessage'
lmMessage :: Functor f => (T.Text -> f T.Text) -> LogMessage -> f LogMessage

-- | A lens for the hostname of a 'LogMessage'
-- The function 'setLogMessageHostname' can be used to set the field.
lmHostname
  :: Functor f
  => (Maybe T.Text -> f (Maybe T.Text))
  -> LogMessage
  -> f LogMessage

-- | A lens for the 'Facility' of a 'LogMessage'
lmFacility
  :: Functor f => (Facility -> f Facility) -> LogMessage -> f LogMessage

-- | A lens for the RFC 5424 /application/ name of a 'LogMessage'
--
-- One useful pattern for using this field, is to implement log filters that allow
-- info and debug message from the application itself while only allowing warning and error
-- messages from third party libraries:
--
-- > debugLogsForAppName myAppName lm =
-- >   view lmAppName lm == Just myAppName || lmSeverityIsAtLeast warningSeverity lm
--
-- This concept is also implemented in 'discriminateByAppName'.
lmAppName
  :: Functor f
  => (Maybe T.Text -> f (Maybe T.Text))
  -> LogMessage
  -> f LogMessage

instance Show LogMessage where
  show = T.unpack . T.unlines . renderLogMessageBodyFixWidth


type LogRenderer a = LogMessage -> a

withRenderer :: LogRenderer a -> Eff (Reader (LogRenderer a) ': e) b -> Eff e b
withRenderer = runReader

newtype SeverityText = MkSeverityText { fromSeverityText :: T.Text }
  deriving (Semigroup)

mkSyslogSeverityText :: LogRenderer SeverityText
mkSyslogSeverityText (MkLogMessage !f !s _ _ _ _ _ _ _ _ _)
   = MkSeverityText $ "<" <> T.pack (show (fromSeverity s + fromFacility f * 8)) <> ">"

newtype FacilityText = MkFacilityText { fromFacilityText :: T.Text }
  deriving (Semigroup)

mkSyslogFacilityText :: LogRenderer FacilityText
mkSyslogFacilityText _ = MkFacilityText ""

newtype TimestampText = MkTimestampText { fromTimestampText :: T.Text }
  deriving (Semigroup)

mkFormattedTimestampText :: LogTimestampFormat -> LogRenderer (Maybe TimestampText)
mkFormattedTimestampText f (MkLogMessage _ _ ts _ _ _ _ _ _ _ _) =
  MkTimestampText . formatLogTimestamp f <$> ts

newtype MessageText = MkMessageText { fromMessageText :: T.Text }
  deriving (Semigroup)

mkMessageText :: LogRenderer MessageText
mkMessageText = MkMessageText . renderLogMessageBody

renderDevLogMessage :: LogRenderer T.Text
renderDevLogMessage =
  run
    $ withRenderer mkSyslogSeverityText
    $ withRenderer mkSyslogFacilityText
    $ withRenderer mkMessageText
    $ withRenderer (fromMaybe (MkTimestampText "-") <$> mkFormattedTimestampText rfc5424NoZTimestamp)
    $ mkDevLogMessage

mkDevLogMessage ::
  ( '[ Reader (LogRenderer SeverityText)
     , Reader (LogRenderer FacilityText)
     , Reader (LogRenderer TimestampText)
     , Reader (LogRenderer MessageText)
     ]
    <:: e
  )
  => Eff e (LogRenderer T.Text)
mkDevLogMessage =
  (\s ts m -> s <> pure " " <> ts <> pure " " <> m)
    <$> (fmap fromSeverityText  <$> ask)
    <*> (fmap fromTimestampText <$> ask)
    <*> (fmap fromMessageText   <$> ask)


-- | A time stamp formatting function
newtype LogTimestampFormat =
  MkLogTimestampFormat { formatLogTimestamp :: UTCTime -> T.Text }

-- | Make a  'LogTimestampFormat' using 'formatTime' in the 'defaultLocale'.
mkLogTimestampFormat
  :: String -- ^ The format string that is passed to 'formatTime'
  -> LogTimestampFormat
mkLogTimestampFormat s = MkLogTimestampFormat (T.pack . formatTime defaultTimeLocale s)

-- | Don't render the time stamp
suppressTimestamp :: LogTimestampFormat
suppressTimestamp = MkLogTimestampFormat (const "")

-- | Render the time stamp using @"%h %d %H:%M:%S"@
rfc3164Timestamp :: LogTimestampFormat
rfc3164Timestamp = mkLogTimestampFormat "%h %d %H:%M:%S"

-- | Render the time stamp to @'iso8601DateFormat' (Just "%H:%M:%S%6QZ")@
rfc5424Timestamp :: LogTimestampFormat
rfc5424Timestamp = mkLogTimestampFormat (iso8601DateFormat (Just "%H:%M:%S%6QZ"))

-- | Render the time stamp like 'rfc5424Timestamp' does, but omit the terminal @Z@ character.
rfc5424NoZTimestamp :: LogTimestampFormat
rfc5424NoZTimestamp = mkLogTimestampFormat (iso8601DateFormat (Just "%H:%M:%S%6Q"))



-- | Print the /body/ of a 'LogMessage'
renderLogMessageBodyFixWidth :: LogMessage -> [T.Text]
renderLogMessageBodyFixWidth (MkLogMessage _f _s _ts _hn _an _pid _mi _sd ti loc msg) =
  if T.null msg
    then []
    else
      maybe "" ((<> " -") . T.pack . show) ti
      : (msg <> T.replicate (max 0 (60 - T.length msg)) " ")
      : maybe
          []
          (\sl -> pure
            (T.pack $ printf "% 30s line %i"
                             (takeFileName (srcLocFile sl))
                             (srcLocStartLine sl)
            )
          )
          loc

-- | Print the /body/ of a 'LogMessage' without any /tab-stops/
renderLogMessageBody :: LogMessage -> T.Text
renderLogMessageBody (MkLogMessage _f _s _ts _hn _an _pid _mi _sd ti loc msg) =
     maybe "" (\tis -> T.pack (show tis) <> " ") ti
  <> msg
  <> maybe
          ""
          (\sl ->
             T.pack $ printf " at %s:%i"
                             (takeFileName (srcLocFile sl))
                             (srcLocStartLine sl)
          )
          loc