packages feed

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