di-1.2: Typeful hierarchical structured logging using di, mtl and df1.

Safe HaskellNone
LanguageHaskell2010

Di

Contents

Description

This module is a highly opinionated, basic, and yet sufficient choice of a concrete stack of logging solutions belonging to the di logging ecosystem—an otherwise rather general ecosystem, flexible and full of choices.

For most logging scenarios out there, the choices made here should suffice, but if you find these are not sufficient for your particular use case, please refer to other libraries of the di logging ecosystem such as di-core, di-monad, di-handle, or di-df1, and you are likely to find a compatible and composable solution there. For this reason, staring with this package rather than one of the those other lower-level packages is always recommended.

The choices made here are:

  • We encourage a mtl approach through a typeclass called MonadDi, for which all of the monad transformers in transformers and pipes have instances.
  • We provide our own DiT monad transformer which has a MonadDi instance, as well as instances for all the relevant typeclasses in the base, mtl, and exceptions libraries. All of the MonadDi instances exported by this package expect a DiT transformer in the stack somewhere, and defer all work to it.
  • We embrace the df1 hierarchical structured logging format, both at the type-level and when rendering the log lines as text. Most notably, this means that we embrace the df1 importance Levels.
  • We commit logs to the outside world by printing them to stderr.
  • Exceptions are logged at their throw site (see onException).

You will notice that some of the functions in this module mention the types Level, Path and Message, and some other functions talk about level, path and msg type variables. This is because even while our particular set of choices require some monomorphic types, as demonstrated by the Df1 and MonadDf1 type-synonyms, the larger di logging ecosystem treats these values polymorphically, so they will show up in the types in one way or another, either in concrete or polymorphic form. This can seem a bit noisy, but the good news is that if, for example, want to call a third party library that uses other types for conveying the idea of a “log importance level” or a “log message”, then you can do so if you can convert between these different types. You are of course encouraged to use the Df1 and MonadDf1 type-synonyms yourself. For more information about this, see Di.Monad and Di.Core, but not today.

The intended usage of this module is:

import qualified Di
Synopsis

Documentation

new Source #

Arguments

:: (MonadIO m, MonadMask m) 
=> (Di Level Path Message -> m a)

This type is the same as Df1 -> m a.

^ Within this scope, you can use the obtained Di safely, even concurrently. As soon as m a finishes, new will block until all logs have finished processing, before returning.

WARNING: Even while new commit pure :: m (Di Level Path Message) type-checks, attempting to use the obtained Di outside its intended scope will fail.

-> m a 

Obtain a Di that will write logs in the df1 format to stderr.

Generally, you will want to call new just once per application, right from your main function. For example:

main :: IO ()
main = do
   new $ \di -> do
      -- The rest of your program goes here.
      -- You can start logging right away.
      runDiT di $ do
          notice "Welcome to my program!"
          -- You can use push to separate different
          -- logging scopes of your program:
          push "initialization" $ do
              -- something something do initialization
              notice "Starting web server"
          push "server" $ do
              -- And you can use attr to add metadata to
              -- messages logged within a particular scope.
              attr "port" "80" $ do
                   info "Listening for new clients"
                   clientAddress <- somehow get a client connection
                   push "handler" $ do
                      attr "client-address" clientAddress $ do
                         info "Connection established"
                         -- If you throw an exception with throw,
                         -- it will be logged automatically.
                         throw (userError "Oops!")

That program will render something like this to stderr (in colors!):

2018-05-06T19:48:06.194579393Z NOTICE Welcome to my program!
2018-05-06T19:48:06.195041422Z /initialization NOTICE Starting web server
2018-05-06T19:48:06.195052862Z /server port=80 INFO Listening for new clients
2018-05-06T19:48:06.195059084Z /server port=80 /handler client%2daddress=192%2e168%2e0%2e25%3a32528 INFO Connection established
2018-05-06T19:48:06.195059102Z /server port=80 /handler client%2daddress=192%2e168%2e0%2e25%3a32528 exception=user%20error%20(Oops!) WARNING Exception thrown

Notice that by default, all exceptions thrown using throw are logged at their throw site with Warning level.

(Unrelated: Notice how df1 escapes pretty much all punctuation characters. This is temporal until the df1 format is formalized and a more limited set of punctuation characters is reserved.)

type Df1 = Di Level Path Message #

Convenience type-synonym for a Di restricted to all the df1 monomorphic types.

Df1 == Di Level Path Message
   :: *

This type-synonym is not used within the di-df1 library itself because all functions exposed in the library have more general types. However, users are encouraged to use Df1 if they find it useful to reduce boilerplate and improve type inferrence.

Monadic API

type MonadDf1 = MonadDi Level Path Message #

Convenience type-synonym for a MonadDi restricted to all the df1 monomorphic types.

MonadDf1 == MonadDi Level Path Message
   :: (* -> *) -> Constraint

MonadDf1 m == MonadDi Level Path Message m
   :: Constraint

This type-synonym is not used within the di-df1 library itself because all functions exposed in the library have more general types. However, users are encouraged to use MonadDf1 if they find it useful to reduce boilerplate and improve type inferrence.

Hierarchy

push #

Arguments

:: MonadDi level Path msg m 
=> Segment 
-> m a 
-> m a 

Push a new Segment to the MonadDi.

data Path #

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 /bar /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 "bar")
, Push (segment "qux")
, Attr (key "z") (value "c")
, Attr (key "z") (value "d")
] :: Seq Path

Please notice that [] :: Seq Path is a valid path insofar as df1 is concerned, and that Attr and Push can be juxtapositioned in any order.

Instances
Eq Path 
Instance details

Defined in Df1.Types

Methods

(==) :: Path -> Path -> Bool #

(/=) :: Path -> Path -> Bool #

Show Path 
Instance details

Defined in Df1.Types

Methods

showsPrec :: Int -> Path -> ShowS #

show :: Path -> String #

showList :: [Path] -> ShowS #

data Segment #

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 segment.

Notice that "" :: Segment is acceptable, and will be correctly rendered and parsed back.

Instances
Eq Segment 
Instance details

Defined in Df1.Types

Methods

(==) :: Segment -> Segment -> Bool #

(/=) :: Segment -> Segment -> Bool #

Show Segment 
Instance details

Defined in Df1.Types

IsString Segment 
Instance details

Defined in Df1.Types

Methods

fromString :: String -> Segment #

Semigroup Segment 
Instance details

Defined in Df1.Types

Monoid Segment 
Instance details

Defined in Df1.Types

ToSegment Segment

Identity.

Instance details

Defined in Df1.Types

Methods

segment :: Segment -> Segment #

class ToSegment a where #

Convert an arbitrary type to a Segment.

You are encouraged to create custom ToSegment instances for your types making sure you avoid rendering sensitive details such as passwords, so that they don't accidentally end up in logs.

Any characters that need to be escaped for rendering will be automatically escaped at rendering time. You don't need to escape them here.

Minimal complete definition

segment

Methods

segment :: a -> Segment #

Instances
ToSegment Text
x :: Text == unSegment (segment x)
Instance details

Defined in Df1.Types

Methods

segment :: Text -> Segment #

ToSegment Text
x :: Text == toStrict (unSegment (segment x))
Instance details

Defined in Df1.Types

Methods

segment :: Text -> Segment #

ToSegment String
x :: String == unpack (unSegment (segment x))
Instance details

Defined in Df1.Types

Methods

segment :: String -> Segment #

ToSegment Segment

Identity.

Instance details

Defined in Df1.Types

Methods

segment :: Segment -> Segment #

Metadata

attr #

Arguments

:: MonadDi level Path msg m 
=> Key 
-> Value 
-> m a 
-> m a 

Push a new attribute Key and Value to the MonadDi.

data Key #

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 key.

Notice that "" :: Key is acceptable, and will be correctly rendered and parsed back.

Instances
Eq Key 
Instance details

Defined in Df1.Types

Methods

(==) :: Key -> Key -> Bool #

(/=) :: Key -> Key -> Bool #

Show Key 
Instance details

Defined in Df1.Types

Methods

showsPrec :: Int -> Key -> ShowS #

show :: Key -> String #

showList :: [Key] -> ShowS #

IsString Key 
Instance details

Defined in Df1.Types

Methods

fromString :: String -> Key #

Semigroup Key 
Instance details

Defined in Df1.Types

Methods

(<>) :: Key -> Key -> Key #

sconcat :: NonEmpty Key -> Key #

stimes :: Integral b => b -> Key -> Key #

Monoid Key 
Instance details

Defined in Df1.Types

Methods

mempty :: Key #

mappend :: Key -> Key -> Key #

mconcat :: [Key] -> Key #

ToKey Key

Identity.

Instance details

Defined in Df1.Types

Methods

key :: Key -> Key #

class ToKey a where #

Convert an arbitrary type to a Key.

You are encouraged to create custom ToKey instances for your types making sure you avoid rendering sensitive details such as passwords, so that they don't accidentally end up in logs.

Any characters that need to be escaped for rendering will be automatically escaped at rendering time. You don't need to escape them here.

Minimal complete definition

key

Methods

key :: a -> Key #

Instances
ToKey Text
x :: Text == unKey (key x)
Instance details

Defined in Df1.Types

Methods

key :: Text -> Key #

ToKey Text
x :: Text == toStrict (unKey (key x))
Instance details

Defined in Df1.Types

Methods

key :: Text -> Key #

ToKey String
x :: String == unpack (unKey (key x))
Instance details

Defined in Df1.Types

Methods

key :: String -> Key #

ToKey Key

Identity.

Instance details

Defined in Df1.Types

Methods

key :: Key -> Key #

data Value #

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 value.

Notice that "" :: Value is acceptable, and will be correctly rendered and parsed back.

Instances
Eq Value 
Instance details

Defined in Df1.Types

Methods

(==) :: Value -> Value -> Bool #

(/=) :: Value -> Value -> Bool #

Show Value 
Instance details

Defined in Df1.Types

Methods

showsPrec :: Int -> Value -> ShowS #

show :: Value -> String #

showList :: [Value] -> ShowS #

IsString Value 
Instance details

Defined in Df1.Types

Methods

fromString :: String -> Value #

Semigroup Value 
Instance details

Defined in Df1.Types

Methods

(<>) :: Value -> Value -> Value #

sconcat :: NonEmpty Value -> Value #

stimes :: Integral b => b -> Value -> Value #

Monoid Value 
Instance details

Defined in Df1.Types

Methods

mempty :: Value #

mappend :: Value -> Value -> Value #

mconcat :: [Value] -> Value #

ToValue Value

Identity.

Instance details

Defined in Df1.Types

Methods

value :: Value -> Value #

class ToValue a where #

Convert an arbitrary type to a Value.

You are encouraged to create custom ToValue instances for your types making sure you avoid rendering sensitive details such as passwords, so that they don't accidentally end up in logs.

Any characters that need to be escaped for rendering will be automatically escaped at rendering time. You don't need to escape them here.

Minimal complete definition

value

Methods

value :: a -> Value #

Instances
ToValue Text
x :: Text == unValue (value x)
Instance details

Defined in Df1.Types

Methods

value :: Text -> Value #

ToValue Text
x :: Text == toStrict (unValue (value x))
Instance details

Defined in Df1.Types

Methods

value :: Text -> Value #

ToValue String
x :: String == unpack (unValue (value x))
Instance details

Defined in Df1.Types

Methods

value :: String -> Value #

ToValue Value

Identity.

Instance details

Defined in Df1.Types

Methods

value :: Value -> Value #

Messages

data Level #

Importance of the logged message.

These levels, listed in increasing order of importance, correspond to the levels used by syslog(3).

Instances
Bounded Level 
Instance details

Defined in Df1.Types

Enum Level 
Instance details

Defined in Df1.Types

Eq Level 
Instance details

Defined in Df1.Types

Methods

(==) :: Level -> Level -> Bool #

(/=) :: Level -> Level -> Bool #

Ord Level

Order of importance. For example, Emergency is more important than Debug:

Emergency > Debug  ==  True
Instance details

Defined in Df1.Types

Methods

compare :: Level -> Level -> Ordering #

(<) :: Level -> Level -> Bool #

(<=) :: Level -> Level -> Bool #

(>) :: Level -> Level -> Bool #

(>=) :: Level -> Level -> Bool #

max :: Level -> Level -> Level #

min :: Level -> Level -> Level #

Show Level 
Instance details

Defined in Df1.Types

Methods

showsPrec :: Int -> Level -> ShowS #

show :: Level -> String #

showList :: [Level] -> ShowS #

data Message #

A message text.

If you have the OverloadedStrings GHC extension enabled, you can build a Message using a string literal:

"foo" :: Message

Otherwise, you can use fromString or message.

Notice that "" :: Message is acceptable, and will be correctly rendered and parsed back.

Instances
Eq Message 
Instance details

Defined in Df1.Types

Methods

(==) :: Message -> Message -> Bool #

(/=) :: Message -> Message -> Bool #

Show Message 
Instance details

Defined in Df1.Types

IsString Message 
Instance details

Defined in Df1.Types

Methods

fromString :: String -> Message #

Semigroup Message 
Instance details

Defined in Df1.Types

Monoid Message 
Instance details

Defined in Df1.Types

ToMessage Message

Identity.

Instance details

Defined in Df1.Types

Methods

message :: Message -> Message #

class ToMessage a where #

Convert an arbitrary type to a Message.

You are encouraged to create custom ToMessage instances for your types making sure you avoid rendering sensitive details such as passwords, so that they don't accidentally end up in logs.

Any characters that need to be escaped for rendering will be automatically escaped at rendering time. You don't need to escape them here.

Minimal complete definition

message

Methods

message :: a -> Message #

Instances
ToMessage Text
x :: Text == unMessage (message x)
Instance details

Defined in Df1.Types

Methods

message :: Text -> Message #

ToMessage Text
x :: Text == toStrict (unMessage (message x))
Instance details

Defined in Df1.Types

Methods

message :: Text -> Message #

ToMessage String
x :: String == unpack (unMessage (message x))
Instance details

Defined in Df1.Types

Methods

message :: String -> Message #

ToMessage Message

Identity.

Instance details

Defined in Df1.Types

Methods

message :: Message -> Message #

debug :: MonadDi Level path Message m => Message -> m () #

Log a message intended to be useful only when deliberately debugging a program.

info :: MonadDi Level path Message m => Message -> m () #

Log an informational message.

notice :: MonadDi Level path Message m => Message -> m () #

Log a condition that is not an error, but should possibly be handled specially.

warning :: MonadDi Level path Message m => Message -> m () #

Log a warning condition, such as an exception being gracefully handled or some missing configuration setting being assigned a default value.

error :: MonadDi Level path Message m => Message -> m () #

Log an error condition, such as an unhandled exception.

alert :: MonadDi Level path Message m => Message -> m () #

Log a condition that should be corrected immediately, such as a corrupted database.

critical :: MonadDi Level path Message m => Message -> m () #

Log a critical condition that could result in system failure, such as a disk running out of space.

emergency :: MonadDi Level path Message m => Message -> m () #

Log a message stating that the system is unusable.

Exceptions

throw :: (MonadDi level path msg m, Exception e) => e -> m a #

Throw an Exception, but not without logging it first according to the rules established by onException, and further restricted by the rules established by filter.

If the exception doesn't need to be logged, according to the policy set with onException, then this function behaves just as throwSTM.

WARNING: Note that when m is STM, or ultimately runs on STM, then throw will not log the exception, just throw it. This might change in the future if we figure out how to make it work safely.

Basic DiT support

type Df1T = DiT Level Path Message #

Convenience type-synonym for a DiT restricted to all the df1 monomorphic types.

Df1T == DiT Level Path Message
   :: (* -> *) -> * -> *

Df1T m == DiT Level Path Message m
   :: * -> *

Df1T m a == DiT Level Path Message m a
   :: *

This type-synonym is not used within the di-df1 library itself because all functions exposed in the library have more general types. However, users are encouraged to use MonadDf1 if they find it useful to reduce boilerplate and improve type inferrence.

runDiT #

Arguments

:: MonadIO m 
=> Di level path msg 
-> DiT level path msg m a 
-> m a 

Run a DiT.

forall di.
   runDiT di (diT (\nat' di' -> pure (nat', di')))
       == pure (natSTM, di)

This is like runDiT', but specialized to run with an underlying MonadIO.

runDiT  ==  runDiT' (liftIO . atomically)

Please notice that runDiT doesn't perform a flush on the given Di before returning. You are responsible for doing that (or, more likely, new will do it for you).

Also, notice that runDiT is a monad morphism from DiT m to m.

hoistDiT #

Arguments

:: (forall x. n x -> m x)

Natural transformation from n to m.

-> (forall x. m x -> n x)

Monad morphism from m to n.

-> DiT level path msg m a

Monad morphism from DiT m to DiT n.

-> DiT level path msg n a 

Lift a monad morphism from m to n to a monad morphism from DiT level path msg m to DiT level path msg n.

Notice that DiT itself is not a functor in the category of monads, so it can't be an instance of MFunctor from the mmorph package. However, it becomes one if you pair it with a natural transformation nat :: forall x. n x -> m x. That is:

forall nat.  such that nat is a natural transformation
   hoistDiT nat  ==  hoist

In practical terms, it means that most times you can “hoist” a DiT anyway, just not through hoist.