Blammo-1.1.2.1: src/Blammo/Logging/LogSettings/LogLevels.hs
-- | Backwards-compatible extension to a simple 'LogLevel' parser/filter
--
-- Assume you are using this library/module to parse a @LOG_LEVEL@ environment
-- variable, which is used to filter your logs.
--
-- Running,
--
-- @
-- LOG_LEVEL=warn ./my-program
-- @
--
-- Will do what you expect: filter all logging to only those messages
-- at-or-above @warn@ level.
--
-- While,
--
-- @
-- LOG_LEVEL=debug ./my-program
-- @
--
-- Will enable debug logging throughout.
--
-- This is all un-surprising and this module does not change behavior in this
-- case whatsoever. But let's say that is entirely too noisy. Because you're
-- using Amazonka and persistent, and have correctly integrated your main
-- logging with them, you are now getting /tons/ of spam from their very-chatty
-- debug logs, and its drowning out the application debug logs you were hoping
-- to see.
--
-- Well, now can do this:
--
-- @
-- LOG_LEVEL="debug,Amazonka:info,SQL:warn" ./my-program
-- @
--
-- And suddenly your application's debug logs are standing out again, because
-- everything from the Amazonka source is filtered to info and the SQL source is
-- filtered to warn.
--
-- The format parsed by 'readLogLevels' is:
--
-- @
-- [<source:level>, ...,]<level>[, <source:level>, ...]
-- @
--
-- Where @<level>@ defines the minimum level for anything not overridden by
-- source. If you go on to add any @<source:level>@ pairs, that will change the
-- minimum level for messages from that source.
module Blammo.Logging.LogSettings.LogLevels
( LogLevels
, LogLevel (..)
, newLogLevels
, readLogLevels
, showLogLevels
, shouldLogLevel
, defaultLogLevels
) where
import Prelude
import Control.Monad.Logger.Aeson
import Data.Either (partitionEithers)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Data.Text (Text, pack, unpack)
import qualified Data.Text as T
data LogLevels = LogLevels
{ llDefaultLevel :: LogLevel
, llSourceLevels :: Map LogSource LogLevel
}
deriving stock (Eq, Show)
newLogLevels :: LogLevel -> [(LogSource, LogLevel)] -> LogLevels
newLogLevels level sourceLevels =
LogLevels
{ llDefaultLevel = level
, llSourceLevels = Map.fromList sourceLevels
}
readLogLevels :: String -> Either String LogLevels
readLogLevels s = toLogLevels . partitionEithers =<< traverse readPiece pieces
where
toLogLevels = \case
([], _) -> invalid "no level present"
(_ : _ : _, _) -> invalid "more than one level present"
([level], sourceLevels) -> pure $ newLogLevels level sourceLevels
readPiece t = case T.breakOn ":" t of
(a, ":") -> invalid $ "no level for source " <> unpack a
(a, b) | T.null a -> invalid $ "no source for level" <> unpack b
(a, b) | T.null b -> pure $ Left $ readLogLevel a
(a, b) -> pure $ Right (a, readLogLevel $ T.drop 1 b)
pieces = filter (not . T.null) $ map T.strip $ T.splitOn "," $ pack s
invalid reason = Left $ "Invalid log level " <> s <> ", " <> reason
readLogLevel :: Text -> LogLevel
readLogLevel t = case T.toLower t of
"debug" -> LevelDebug
"info" -> LevelInfo
"warn" -> LevelWarn
"error" -> LevelError
_ -> LevelOther t
showLogLevels :: LogLevels -> String
showLogLevels LogLevels {..} =
unpack $
T.intercalate "," $
showLogLevel llDefaultLevel
: map
(\(s, l) -> s <> ":" <> showLogLevel l)
(Map.toList llSourceLevels)
showLogLevel :: LogLevel -> Text
showLogLevel = \case
LevelDebug -> "debug"
LevelInfo -> "info"
LevelWarn -> "warn"
LevelError -> "error"
LevelOther t -> t
shouldLogLevel :: LogLevels -> LogSource -> LogLevel -> Bool
shouldLogLevel LogLevels {..} source = (`lgte` minLevel)
where
minLevel = fromMaybe llDefaultLevel $ Map.lookup source llSourceLevels
defaultLogLevels :: LogLevels
defaultLogLevels =
LogLevels {llDefaultLevel = LevelInfo, llSourceLevels = Map.empty}
-- | Like '(>=)', but treats @'LevelOther' "trace"@ as below 'LevelDebug'
--
-- Normally, 'LevelOther' is the highest level, but it's common to use the
-- @trace@ level as more verbose than @debug@. With this comparison in use, we
-- can safely use @'LevelOther' "trace"@ for that.
lgte :: LogLevel -> LogLevel -> Bool
lgte _ (LevelOther x) | T.toLower x == "trace" = True
lgte (LevelOther x) _ | T.toLower x == "trace" = False
lgte a b = a >= b