-- This Source Code Form is subject to the terms of the Mozilla Public -- License, v. 2.0. If a copy of the MPL was not distributed with this -- file, You can obtain one at http://mozilla.org/MPL/2.0/. {-# LANGUAGE BangPatterns #-} {-# LANGUAGE OverloadedStrings #-} -- | Small layer on top of @fast-logger@ which adds log-levels and -- timestamp support and not much more. module System.Logger ( -- * Settings Settings , defSettings , logLevel , setLogLevel , logLevelOf , setLogLevelOf , output , setOutput , format , setFormat , delimiter , setDelimiter , netstrings , setNetStrings , bufSize , setBufSize , name , setName -- * Type definitions , Logger , Level (..) , Output (..) , DateFormat , iso8601UTC -- * Core API , new , create , level , flush , close , clone , settings -- ** Logging , log , trace , debug , info , warn , err , fatal , module M ) where import Prelude hiding (log) import Control.Applicative import Control.AutoUpdate import Control.Monad import Control.Monad.IO.Class import Data.Maybe (fromMaybe) import Data.Text (Text) import Data.UnixTime import System.Environment (lookupEnv) import System.Logger.Message as M import System.Logger.Settings import qualified Data.Map.Strict as Map import qualified System.Log.FastLogger as FL data Logger = Logger { logger :: FL.LoggerSet , settings :: Settings , getDate :: IO (Msg -> Msg) } -- | Create a new 'Logger' with the given 'Settings'. -- Please note that the 'logLevel' can be dynamically adjusted by setting -- the environment variable @LOG_LEVEL@ accordingly. Likewise the buffer -- size can be dynamically set via @LOG_BUFFER@ and netstrings encoding -- can be enabled with @LOG_NETSTR=True@ -- -- Since version 0.11 one can also use @LOG_LEVEL_MAP@ to specify log -- levels per (named) logger. The syntax uses standard haskell syntax for -- association lists of type @[(Text, Level)]@. For example: -- -- @ -- $ LOG_LEVEL=Info LOG_LEVEL_MAP='[("foo", Warn), ("bar", Trace)]' cabal repl -- > g1 <- new defSettings -- > let g2 = clone (Just "foo") g -- > let g3 = clone (Just "bar") g -- > let g4 = clone (Just "xxx") g -- > logLevel (settings g1) -- Info -- > logLevel (settings g2) -- Warn -- > logLevel (settings g3) -- Trace -- > logLevel (settings g4) -- Info -- @ new :: MonadIO m => Settings -> m Logger new s = liftIO $ do !n <- fmap (readNote "Invalid LOG_BUFFER") <$> lookupEnv "LOG_BUFFER" !l <- fmap (readNote "Invalid LOG_LEVEL") <$> lookupEnv "LOG_LEVEL" !e <- fmap (readNote "Invalid LOG_NETSTR") <$> lookupEnv "LOG_NETSTR" !m <- fromMaybe "[]" <$> lookupEnv "LOG_LEVEL_MAP" let !k = logLevelMap s `mergeWith` m let !s' = setLogLevel (fromMaybe (logLevel s) l) . setNetStrings (fromMaybe (netstrings s) e) . setLogLevelMap k $ s g <- fn (output s) (fromMaybe (bufSize s) n) Logger g s' <$> mkGetDate (format s) where fn StdOut = FL.newStdoutLoggerSet fn StdErr = FL.newStderrLoggerSet fn (Path p) = flip FL.newFileLoggerSet p mkGetDate "" = return (return id) mkGetDate f = mkAutoUpdate defaultUpdateSettings { updateAction = msg . formatUnixTimeGMT (template f) <$> getUnixTime } mergeWith m e = Map.fromList (readNote "Invalid LOG_LEVEL_MAP" e) `Map.union` m -- | Invokes 'new' with default settings and the given output as log sink. create :: MonadIO m => Output -> m Logger create o = new $ setOutput o defSettings readNote :: Read a => String -> String -> a readNote m s = case reads s of [(a, "")] -> a _ -> error m -- | Logs a message with the given level if greater or equal to the -- logger's threshold. log :: MonadIO m => Logger -> Level -> (Msg -> Msg) -> m () log g l m = unless (level g > l) . liftIO $ putMsg g l m {-# INLINE log #-} -- | Abbreviation of 'log' using the corresponding log level. trace, debug, info, warn, err, fatal :: MonadIO m => Logger -> (Msg -> Msg) -> m () trace g = log g Trace debug g = log g Debug info g = log g Info warn g = log g Warn err g = log g Error fatal g = log g Fatal {-# INLINE trace #-} {-# INLINE debug #-} {-# INLINE info #-} {-# INLINE warn #-} {-# INLINE err #-} {-# INLINE fatal #-} -- | Clone the given logger and optionally give it a name -- (use @Nothing@ to clear). -- -- If 'logLevelOf' returns a custom 'Level' for this name -- then the cloned logger will use it for its log messages. clone :: Maybe Text -> Logger -> Logger clone Nothing g = g { settings = setName Nothing (settings g) } clone (Just n) g = let s = settings g l = fromMaybe (logLevel s) $ logLevelOf n s in g { settings = setName (Just n) . setLogLevel l $ s } -- | Force buffered bytes to output sink. flush :: MonadIO m => Logger -> m () flush = liftIO . FL.flushLogStr . logger -- | Closes the logger. close :: MonadIO m => Logger -> m () close g = liftIO $ FL.rmLoggerSet (logger g) -- | Inspect this logger's threshold. level :: Logger -> Level level = logLevel . settings {-# INLINE level #-} putMsg :: MonadIO m => Logger -> Level -> (Msg -> Msg) -> m () putMsg g l f = liftIO $ do d <- getDate g let n = netstrings $ settings g let x = delimiter $ settings g let s = nameMsg $ settings g let m = render x n (d . lmsg l . s . f) FL.pushLogStr (logger g) (FL.toLogStr m) lmsg :: Level -> (Msg -> Msg) lmsg Trace = msg (val "T") lmsg Debug = msg (val "D") lmsg Info = msg (val "I") lmsg Warn = msg (val "W") lmsg Error = msg (val "E") lmsg Fatal = msg (val "F") {-# INLINE lmsg #-}