om-logging-1.1.0.4: src/OM/Logging.hs
{-# LANGUAGE OverloadedStrings #-}
{- |
Description: Logging utilities.
Various combinators for composing "monad-logger" loggers.
-}
module OM.Logging (
-- * Standard OM logging
standardLogging,
withStandardFormat,
-- * Logging Combinators
withTime,
withThread,
withLevel,
withPrefix,
withPackage,
-- ** Filters
filterLogging,
levelFilter,
-- ** Destinations
teeLogging,
stdoutLogging,
fdLogging,
-- * Other types
parseLevel,
JSONLevel(..),
) where
import Control.Concurrent (myThreadId)
import Control.Monad (when)
import Control.Monad.Logger (LogLevel(LevelDebug, LevelError, LevelInfo,
LevelOther, LevelWarn), Loc, LogSource, LogStr, loc_package)
import Data.Aeson (FromJSON(parseJSON), Value(String))
import Data.List (intercalate)
import Data.List.Split (splitOn)
import Data.String (IsString)
import Data.Text (Text)
import Data.Time (getCurrentTime)
import Data.Time.Format (defaultTimeLocale, formatTime)
import OM.Show (showt)
import System.IO (Handle, hFlush, stdout)
import System.Log.FastLogger (fromLogStr, toLogStr)
import qualified Data.ByteString.Char8 as BS8
import qualified Data.Text as T
{- | Log to more than one logging destination. -}
teeLogging
:: (Loc -> LogSource -> LogLevel -> LogStr -> IO ()) {- ^ Destination 1. -}
-> (Loc -> LogSource -> LogLevel -> LogStr -> IO ()) {- ^ Destination 2. -}
-> Loc
-> LogSource
-> LogLevel
-> LogStr
-> IO ()
teeLogging logging1 logging2 loc src level msg = do
logging1 loc src level msg
logging2 loc src level msg
{- |
Filter out some log messages. Only messages matching the predicate
are logged to the underlying logger.
-}
filterLogging
:: (Loc -> LogSource -> LogLevel -> LogStr -> Bool)
{- ^ The filter to apply. -}
-> (Loc -> LogSource -> LogLevel -> LogStr -> IO ())
{- ^ The downstream logging destination. -}
-> Loc
-> LogSource
-> LogLevel
-> LogStr
-> IO ()
filterLogging p base loc src level msg =
when (p loc src level msg)
(base loc src level msg)
{- |
@levelFilter level@ is a filter predicate that matches all log messages
with @level@ or above.
-}
levelFilter :: LogLevel -> Loc -> LogSource -> LogLevel -> LogStr -> Bool
levelFilter target _ _ level _ = level >= target
{- | Prepend the 'Control.Concurrent.ThreadId' to the beginning of the log. -}
withThread
:: (Loc -> LogSource -> LogLevel -> LogStr -> IO ())
-> Loc
-> LogSource
-> LogLevel
-> LogStr
-> IO ()
withThread base loc src level msg = do
tid <- myThreadId
base loc src level (toLogStr (squareBracket (showt tid :: Text)) <> msg)
{- | Add timing information to the beginning of logs. -}
withTime
:: (Loc -> LogSource -> LogLevel -> LogStr -> IO ())
-> Loc
-> LogSource
-> LogLevel
-> LogStr
-> IO ()
withTime base loc src level msg = do
now <- getCurrentTime
let
time :: LogStr
time =
toLogStr
. squareBracket
. T.pack
. formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%S%06Q %Z"
$ now
base loc src level (time <> msg)
{- | Add the originating package to the log message. -}
withPackage
:: (Loc -> LogSource -> LogLevel -> LogStr -> IO ())
-> Loc
-> LogSource
-> LogLevel
-> LogStr
-> IO ()
withPackage base loc src level msg =
let
{-
The package information looks like this
`om-legion-6.4.1.1-LnoDD8xLijN7DglolZGFIp`, but we only want the
actual package name, which is why we do the `reverse . split`
business.
-}
package =
toLogStr
. squareBracket
$ case
reverse
. splitOn "-"
. loc_package
$ loc
of
_hash : _version : nameComponents ->
intercalate "-" (reverse nameComponents)
_ -> loc_package loc
in base loc src level (package <> msg)
{- | Add the Logging level to the log output. -}
withLevel
:: (Loc -> LogSource -> LogLevel -> LogStr -> IO ())
-> Loc
-> LogSource
-> LogLevel
-> LogStr
-> IO ()
withLevel base loc src level msg =
let
levelStr :: LogStr
levelStr = squareBracket . toLogStr . showLevel $ level
in
base loc src level (levelStr <> msg)
{- | Prefix a fixed string to the log output. -}
withPrefix
:: LogStr
-> (Loc -> LogSource -> LogLevel -> LogStr -> IO ())
-> Loc
-> LogSource
-> LogLevel
-> LogStr
-> IO ()
withPrefix prefix base loc src level msg =
base loc src level (prefix <> msg)
{- | Help with putting things in square brackets. -}
squareBracket :: (IsString s, Monoid s) => s -> s
squareBracket t = "[" <> t <> "]"
{- | Stringify a log level. -}
showLevel :: LogLevel -> Text
showLevel (LevelOther level) = T.toUpper level
showLevel level = T.toUpper . T.drop 5 . showt $ level
{- |
Log messages to stdout. This is very bare bones. It only logs the
message itself with no other information. It is meant to be used in
conjunction with some of the other combinators, like `withLevel`.
-}
stdoutLogging :: Loc -> LogSource -> LogLevel -> LogStr -> IO ()
stdoutLogging = fdLogging stdout
{- | Like 'stdoutLogging', but log to a file handle. -}
fdLogging :: Handle -> Loc -> LogSource -> LogLevel -> LogStr -> IO ()
fdLogging fd _ _ _ msg = do
BS8.hPutStr fd (fromLogStr msg <> "\n")
hFlush fd
{- | The standard logging for most OM programs. -}
standardLogging
:: LogLevel
-> Loc
-> LogSource
-> LogLevel
-> LogStr
-> IO ()
standardLogging logLevel =
withStandardFormat logLevel stdoutLogging
{- |
Log to the indicated destination, applying the "standard" filters
and formats.
-}
withStandardFormat
:: LogLevel {- ^ The minimum log level that will be logged. -}
-> (Loc -> LogSource -> LogLevel -> LogStr -> IO ()) {- ^ The base logger. -}
-> Loc
-> LogSource
-> LogLevel
-> LogStr
-> IO ()
withStandardFormat logLevel =
filterLogging (levelFilter logLevel)
. withPrefix ": "
. withThread
. withPackage
. withLevel
. withTime
{- | A FromJSON instance to figure out the logging level. -}
newtype JSONLevel = JSONLevel {
unJSONLevel :: LogLevel
}
instance FromJSON JSONLevel where
parseJSON (String str) =
return (JSONLevel (parseLevel str))
parseJSON v =
fail $ "Can't parse logging level from: " ++ show v
{- | Parse a logging level from a string. -}
parseLevel :: Text -> LogLevel
parseLevel "DEBUG" = LevelDebug
parseLevel "INFO" = LevelInfo
parseLevel "WARN" = LevelWarn
parseLevel "ERROR" = LevelError
parseLevel other = LevelOther other