df1-0.1: lib/Df1/Types.hs
{-# LANGUAGE StandaloneDeriving #-}
module Df1.Types
( Log(Log, log_time, log_level, log_path, log_message)
, Level(Debug, Info, Notice, Warning, Error, Critical, Alert, Emergency)
, Path(Attr, Push)
, Segment, unSegment, segment
, Key, unKey, key
, Value, unValue, value
, Message, unMessage, message
) where
import Data.Semigroup (Semigroup)
import Data.Sequence as Seq
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import Data.String (IsString(fromString))
import qualified Data.Time.Clock.System as Time
--------------------------------------------------------------------------------
data Log = Log
{ log_time :: !Time.SystemTime
-- ^ First known timestamp when the log was generated.
--
-- We use 'Time.SystemTime' rather than 'Time.UTCTime' because it is
-- cheaper to obtain and to render. You can use
-- 'Data.Time.Clock.System.systemToUTCTime' to convert it if necessary.
, log_level :: !Level
-- ^ Importance level of the logged message.
, log_path :: !(Seq.Seq Path)
-- ^ 'Path' where the logged message was created from.
--
-- The leftmost 'Path' is the closest to the root. The rightmost 'Path' is
-- the one closest to where the log was generated.
--
-- An 'Seq.empty' 'Seq.Seq' is acceptable, conveying the idea of the “root
-- path”.
, log_message :: !Message
-- ^ Human-readable message itself.
} deriving (Eq, Show)
--------------------------------------------------------------------------------
-- | A message text.
--
-- If you have the @OverloadedStrings@ GHC extension enabled, you can build a
-- 'Message' using a string literal:
--
-- @
-- \"foo\" :: 'Message'
-- @
--
-- Please keep in mind that 'Message' will always strip surrounding whitespace.
-- That is:
--
-- @
-- \"x\" :: 'Message' == \" x\" == \"x \" == \" x \"
-- @
newtype Message = Message TL.Text
deriving (Eq, Show)
message :: TL.Text -> Message
message = Message . TL.dropAround (== ' ')
{-# INLINE message #-}
unMessage :: Message -> TL.Text
unMessage = \(Message x) -> x
{-# INLINE unMessage #-}
instance IsString Message where
fromString = message . TL.pack
{-# INLINE fromString #-}
instance Semigroup Message where
(<>) (Message a) (Message b) = Message (a <> b)
{-# INLINE (<>) #-}
instance Monoid Message where
mempty = Message mempty
{-# INLINE mempty #-}
mappend (Message a) (Message b) = Message (mappend a b)
{-# INLINE mappend #-}
--------------------------------------------------------------------------------
-- | Importance of the logged message.
--
-- These levels, listed in increasing order of importance, correspond to the
-- levels used by [syslog(3)](https://linux.die.net/man/3/syslog).
data Level
= Debug
-- ^ Message intended to be useful only when deliberately debugging a program.
| Info
-- ^ Informational message.
| Notice
-- ^ A condition that is not an error, but should possibly be handled
-- specially.
| Warning
-- ^ A warning condition, such as an exception being gracefully handled or
-- some missing configuration setting being assigned a default value.
| Error
-- ^ Error condition, such as an unhandled exception.
| Critical
-- ^ Critical condition that could result in system failure, such as a disk
-- running out of space.
| Alert
-- ^ A condition that should be corrected immediately, such as a corrupted
-- database.
| Emergency
-- ^ System is unusable.
deriving (Eq, Show, Bounded, Enum)
-- | Order of importance. For example, 'Emergency' is more important than
-- 'Debug':
--
-- @
-- 'Emergency' > 'Debug' == 'True'
-- @
deriving instance Ord Level
--------------------------------------------------------------------------------
-- | A path segment.
--
-- If you have the @OverloadedStrings@ GHC extension enabled, you can build a
-- 'Segment' using a string literal:
--
-- @
-- \"foo\" :: 'Segment'
-- @
--
-- Otherwise, you can use 'fromString' or the 'Segment' constructor directly.
newtype Segment = Segment T.Text
deriving (Eq, Show)
segment :: T.Text -> Segment
segment = Segment . T.dropAround (== ' ')
{-# INLINE segment #-}
unSegment :: Segment -> T.Text
unSegment = \(Segment x) -> x
{-# INLINE unSegment #-}
instance IsString Segment where
fromString = segment . T.pack
{-# INLINE fromString #-}
instance Semigroup Segment where
(<>) (Segment a) (Segment b) = Segment (a <> b)
{-# INLINE (<>) #-}
instance Monoid Segment where
mempty = Segment mempty
{-# INLINE mempty #-}
mappend (Segment a) (Segment b) = Segment (mappend a b)
{-# INLINE mappend #-}
--------------------------------------------------------------------------------
-- | An attribute key (see 'Attr').
--
-- If you have the @OverloadedStrings@ GHC extension enabled, you can build a
-- 'Key' using a string literal:
--
-- @
-- \"foo\" :: 'Key'
-- @
--
-- Otherwise, you can use 'fromString' or the 'key' function.
--
-- Please keep in mind that 'Key' will always strip surrounding whitespace.
-- That is:
--
-- @
-- \"x\" :: 'Key' == \" x\" == \"x \" == \" x \"
-- @
newtype Key = Key T.Text
deriving (Eq, Show)
key :: T.Text -> Key
key = Key . T.dropAround (== ' ')
{-# INLINE key #-}
unKey :: Key -> T.Text
unKey = \(Key x) -> x
{-# INLINE unKey #-}
instance IsString Key where
fromString = key . T.pack
{-# INLINE fromString #-}
instance Semigroup Key where
(<>) (Key a) (Key b) = Key (a <> b)
{-# INLINE (<>) #-}
instance Monoid Key where
mempty = Key mempty
{-# INLINE mempty #-}
mappend (Key a) (Key b) = Key (mappend a b)
{-# INLINE mappend #-}
--------------------------------------------------------------------------------
-- | An attribute value (see 'Attr').
--
-- If you have the @OverloadedStrings@ GHC extension enabled, you can build a
-- 'Value' using a string literal:
--
-- @
-- \"foo\" :: 'Value'
-- @
--
-- Otherwise, you can use 'fromString' or the 'value' function.
--
-- Please keep in mind that 'value' will always strip surrounding whitespace.
-- That is:
--
-- @
-- \"x\" :: 'Value' == \" x\" == \"x \" == \" x \"
-- @
newtype Value = Value TL.Text
deriving (Eq, Show)
unValue :: Value -> TL.Text
unValue = \(Value x) -> x
{-# INLINE unValue #-}
value :: TL.Text -> Value
value = Value . TL.dropAround (== ' ')
{-# INLINE value #-}
instance IsString Value where
fromString = value . TL.pack
{-# INLINE fromString #-}
instance Semigroup Value where
(<>) (Value a) (Value b) = Value (a <> b)
{-# INLINE (<>) #-}
instance Monoid Value where
mempty = Value mempty
{-# INLINE mempty #-}
mappend (Value a) (Value b) = Value (mappend a b)
{-# INLINE mappend #-}
--------------------------------------------------------------------------------
-- | 'Path' represents the hierarchical structure of logged messages.
--
-- For example, consider a /df1/ log line as like the following:
--
-- @
-- 1999-12-20T07:11:39.230553031Z /foo x=a y=b /qux z=c z=d WARNING Something
-- @
--
-- For that line, the 'log_path' attribute of the 'Log' datatype will contain
-- the following:
--
-- @
-- [ 'Push' ('segment' \"foo\")
-- , 'Attr' ('key' \"x\") ('value' \"a\")
-- , 'Attr' ('key' \"y\") ('value' \"b\")
-- , 'Push' ('segment' \"qux\")
-- , 'Attr' ('key' \"z\") ('value' \"c\")
-- , 'Attr' ('key' \"z\") ('value' \"d\")
-- ] :: 'Seq.Seq' 'Path'
-- @
--
-- Please notice that @[] :: 'Seq.Seq' 'Path'@ is a valid path insofar as /df1/
-- is concerned, and that 'Attr' and 'Push' can be juxtapositioned in any order.
data Path
= Push !Segment
| Attr !Key !Value
deriving (Eq, Show)