packages feed

log4hs 0.4.0.0 → 0.5.0.0

raw patch · 28 files changed

+683/−962 lines, 28 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Logging: debug :: ExpQ
- Logging: error :: ExpQ
- Logging: fatal :: ExpQ
- Logging: info :: ExpQ
- Logging: logv :: ExpQ
- Logging: warn :: ExpQ
- Logging.Aeson: instance Data.Aeson.Types.FromJSON.FromJSON (Data.Map.Internal.Map GHC.Base.String Text.Format.Format.Format1 -> GHC.Types.IO Logging.Types.Class.Handler.SomeHandler)
- Logging.Aeson: instance Data.Aeson.Types.FromJSON.FromJSON (GHC.Base.String -> Data.Map.Internal.Map GHC.Base.String Logging.Types.Class.Handler.SomeHandler -> Logging.Types.Sink.Sink)
- Logging.Aeson: instance Data.Aeson.Types.FromJSON.FromJSON (GHC.Types.IO Logging.Types.Class.Handler.SomeHandler)
- Logging.Aeson: instance Data.Aeson.Types.FromJSON.FromJSON (GHC.Types.IO Logging.Types.Handlers.FileHandler.FileHandler)
- Logging.Aeson: instance Data.Aeson.Types.FromJSON.FromJSON (GHC.Types.IO Logging.Types.Handlers.RotatingFileHandler.RotatingFileHandler)
- Logging.Aeson: instance Data.Aeson.Types.FromJSON.FromJSON (GHC.Types.IO Logging.Types.Manager.Manager)
- Logging.Aeson: instance Data.Aeson.Types.FromJSON.FromJSON Logging.Types.Filter.Filter
- Logging.Aeson: instance Data.Aeson.Types.FromJSON.FromJSON Logging.Types.Handlers.StreamHandler.StreamHandler
- Logging.Aeson: instance Data.Aeson.Types.FromJSON.FromJSON Logging.Types.Level.Level
- Logging.Aeson: instance Data.Aeson.Types.FromJSON.FromJSON Logging.Types.Sink.Sink
- Logging.Aeson: instance Data.Aeson.Types.FromJSON.FromJSON Text.Format.Format.Format1
- Logging.Types: newtype Filter
- Logging.Utils: addZonedTime :: NominalDiffTime -> ZonedTime -> ZonedTime
- Logging.Utils: diffZonedTime :: ZonedTime -> ZonedTime -> NominalDiffTime
- Logging.Utils: microseconds :: NominalDiffTime -> Integer
- Logging.Utils: milliseconds :: NominalDiffTime -> Integer
- Logging.Utils: modifyBaseName :: FilePath -> (String -> String) -> FilePath
- Logging.Utils: openLogFile :: FilePath -> TextEncoding -> IO Handle
- Logging.Utils: rotateFile :: FilePath -> FilePath -> IO ()
- Logging.Utils: seconds :: NominalDiffTime -> Integer
- Logging.Utils: timestamp :: NominalDiffTime -> Double
- Logging.Utils: zonedTimeToPOSIXSeconds :: ZonedTime -> NominalDiffTime
+ Logging.Prelude: addZonedTime :: NominalDiffTime -> ZonedTime -> ZonedTime
+ Logging.Prelude: appendBaseName :: FilePath -> String -> FilePath
+ Logging.Prelude: diffZonedTime :: ZonedTime -> ZonedTime -> NominalDiffTime
+ Logging.Prelude: instance GHC.Classes.Eq GHC.IO.Encoding.Types.TextEncoding
+ Logging.Prelude: microseconds :: NominalDiffTime -> Integer
+ Logging.Prelude: milliseconds :: NominalDiffTime -> Integer
+ Logging.Prelude: modifyBaseName :: FilePath -> (String -> String) -> FilePath
+ Logging.Prelude: openLogFile :: FilePath -> TextEncoding -> IO Handle
+ Logging.Prelude: seconds :: NominalDiffTime -> Integer
+ Logging.Prelude: timestamp :: NominalDiffTime -> Double
+ Logging.Prelude: tryRenameFile :: FilePath -> FilePath -> IO ()
+ Logging.Prelude: zonedTimeToPOSIXSeconds :: ZonedTime -> NominalDiffTime
+ Logging.TH: debug :: ExpQ
+ Logging.TH: error :: ExpQ
+ Logging.TH: fatal :: ExpQ
+ Logging.TH: info :: ExpQ
+ Logging.TH: logv :: ExpQ
+ Logging.TH: warn :: ExpQ
+ Logging.Types: data Filter
- Logging.Types: Filter :: Logger -> Filter
+ Logging.Types: Filter :: Logger -> Maybe (LogRecord -> Bool) -> Filter

Files

ChangeLog.md view
@@ -5,3 +5,15 @@  ## V0.3.1 - Fix `defaultRoot` format bug++## V0.4.0+- Add `Config` module to decode `Manager` from json and yaml+- Mark `Logging.Aeson` as deprecated+- Remove `Logging.Deprecated` module++## V0.5.0+- Mark `stderrHandler` `stdoutHandler` `defaultRoot` as deprecated+- Remove `Logging.Aeson` module+- Fix `RotatingFileHandler` bug+- Refine tests+- Fix `callHandler` redundant check
log4hs.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: e7973c763207cddf6697fcd36825081d9f793242e5dc25a07c6b80d710174aa8+-- hash: 8bbb99f215db2c4adcdb2ab401f23a78b09e5a05c3c874cbb6102403fa6d387d  name:           log4hs-version:        0.4.0.0+version:        0.5.0.0 synopsis:       A python logging style log library description:    Please see the http://hackage.haskell.org/package/log4hs category:       logging@@ -23,15 +23,15 @@ library   exposed-modules:       Logging-      Logging.Aeson       Logging.Config.Json       Logging.Config.Type       Logging.Config.Yaml+      Logging.Prelude       Logging.Types+      Logging.TH       Logging.Utils   other-modules:       Logging.Internal-      Logging.TH       Logging.Types.Class       Logging.Types.Class.Filterable       Logging.Types.Class.Handler@@ -45,7 +45,6 @@       Logging.Types.Manager       Logging.Types.Record       Logging.Types.Sink-      System.IO.Extra       Paths_log4hs   hs-source-dirs:       src@@ -68,12 +67,12 @@  test-suite log4hs-test   type: exitcode-stdio-1.0-  main-is: Spec.hs+  main-is: Main.hs   other-modules:-      Logging.AesonSpec-      Logging.ConfigSpec-      Logging.TypesSpec-      LoggingSpec+      LoggingTest.ConfigSpec+      LoggingTest.InternalSpec+      LoggingTest.Prelude+      LoggingTest.TypesSpec       Paths_log4hs   hs-source-dirs:       test
src/Logging.hs view
@@ -16,11 +16,10 @@    module Main ( main ) where -  import           Data.Aeson-  import           Data.Maybe-  import           Logging-  import           Logging.Config.Json-  import           Prelude    hiding (error)+  import           Logging             (run)+  import           Logging.Config.Json (getManager)+  import           Logging.TH          (debug, error, fatal, info, logv, warn)+  import           Prelude             hiding (error)    main :: IO ()   main = getManager "{}" >>= flip run app@@ -39,13 +38,11 @@  See "Logging.Config.Json" and "Logging.Config.Yaml" to lean more about decoding 'Manager' from json or yaml- -} module Logging   ( module Logging.Types-  , module Logging.Internal-  -- ** Logging THs   , module Logging.TH+  , module Logging.Internal   ) where  import           Logging.Internal hiding (log)
− src/Logging/Aeson.hs
@@ -1,235 +0,0 @@-{-# OPTIONS_HADDOCK prune, ignore-exports #-}--{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE FlexibleInstances     #-}-{-# LANGUAGE OverloadedStrings     #-}-{-# LANGUAGE ScopedTypeVariables   #-}-{-# LANGUAGE TypeApplications      #-}--module Logging.Aeson {-# DEPRECATED "Use Logging.Config.Json instead" #-}-  (-  -- * Aeson Instances-  ---  -- $aesondoc-  ) where--import           Control.Applicative         (pure)-import           Control.Lens                (set)-import           Data.Aeson-import           Data.Aeson.Types            (Parser, typeMismatch)-import           Data.Default-import           Data.Generics.Product.Typed-import           Data.IORef-import qualified Data.Map.Lazy               as M-import           Data.String-import           System.Directory-import           System.FilePath-import           System.IO-import           Text.Format--import           Logging.Internal-import           Logging.Types-import           Logging.Utils---{- $aesondoc--By using 'Data.Aeson', we can decode json string into 'Manager'.--__A basic 'Manager' json format:__--@-  {-    \"loggers\": {\"root\": {}, \"MyLogger\": {}},-    \"handlers\": {\"console\": {}, \"file\": {}},-    \"formatters\": {\"default\": "format", \"simple\": "format"},-    \"disabled\": false,-    \"catchUncaughtException\": true-  }-@---In practice, a set of handlers share a formatter, and other handlers share-another one, so we define all the formatters in a map, handler refernces-the formatter throught its key.--So as handlers, sinks may share same handlers.--__Examples of Formatter json__--See 'Text.Format' of vformat package for more information about formatting.--@-  "{message}"-  "{logger} {level}: {message}"-  "{logger:<20.20s} {level:<8s}: {message}"-  "{asctime:%Y-%m-%dT%H:%M:%S%6Q%z} - {level} - {logger}] {message}"-@--__Examples of 'Handler' json__--Note: Besides some common field, handler's other fields depend on its type.--@-  -- StreamHandler-  {-    \"type\": \"StreamHandler\",-    \"stream\": \"stderr\",-    \"level\": \"DEBUG\",-    \"filterer\": [\"Package.Module.Submodule\"],-    \"formatter\": \"default\",-  }--  -- FileHandler-  {-    \"type\": \"FileHandler\",-    \"file\": \"./default.log\",-    \"level\": \"INFO\",-    \"filterer\": [],-    \"formatter\": \"simple\",-  }-@--__Examples of 'Logger' ('Sink') json__--@-  -- a standard format-  {-    \"level\": \"DEBUG\",-    \"filterer\": [\"Package.Module.Submodule\"],-    \"handlers\": [\"console\"],-    \"propagate\": true,-    \"disabled\": false-  }--  -- this example is equivalent to the first-  {-    \"level\": \"DEBUG\",-    \"filterer\": [\"Package.Module.Submodule\"],-    \"handlers\": ["console"]-  }--  -- another example-  {-    \"level\": \"INFO\",-    \"handlers\": [\"console\", \"file\"],-    \"propagate\": false-  }-@--}--instance FromJSON Level where-  parseJSON v = read <$> parseJSON v---instance FromJSON Filter where-  parseJSON v = Filter <$> parseJSON v---instance FromJSON Format1 where-  parseJSON v = fromString <$> parseJSON v---instance FromJSON StreamHandler where-  parseJSON = withObject "StreamHandler" $ \v ->-      StreamHandler <$> v .:? "level" .!= def-                    <*> v .:? "filterer" .!= []-                    <*> v .:? "formatter" .!= "{message}"-                    <*> (parseStream <$> (v .:? "stream" .!= "stderr"))-    where-      parseStream :: String -> Handle-      parseStream "stderr" = stderr-      parseStream "stdout" = stdout-      parseStream _        = error "Logging.Aeson: no parse (stream)"---instance FromJSON (IO FileHandler) where-  parseJSON = withObject "FileHandler" $ \v -> do-    level <- v .:? "level" .!= def-    filterer <- v .:? "filterer" .!= []-    formatter <- v .:? "formatter" .!= "{message}"-    file <- v .:? "file" .!= "log4hs.log"-    encoding <- v .:? "encoding" .!= (show utf8)-    return $ do-      stream <- newIORef undefined-      encoding' <- mkTextEncoding encoding-      return $ FileHandler level filterer formatter file encoding' stream---instance FromJSON (IO RotatingFileHandler) where-  parseJSON = withObject "RotatingFileHandler" $ \v -> do-    level <- v .:? "level" .!= def-    filterer <- v .:? "filterer" .!= []-    formatter <- v .:? "formatter" .!= ""-    file <- v .:? "file" .!= "log4hs.log"-    encoding <- v .:? "encoding" .!= (show utf8)-    maxBytes <- v .:? "maxBytes" .!= 102400000-    backupCount <- v .:? "maxBytes" .!= 5-    return $ do-      stream <- newIORef undefined-      encoding' <- mkTextEncoding encoding-      return $ RotatingFileHandler level filterer formatter file encoding'-                                   maxBytes backupCount stream---instance FromJSON (IO SomeHandler) where-  parseJSON = withObject "Handler" $ \v -> (v .: "type") >>= (`parseHandler` v)-    where-      parseHandler :: String -> Object -> Parser (IO SomeHandler)-      parseHandler "StreamHandler" v = do-        hdl <- parseJSON (Object v)-        return $ return $ toHandler (hdl :: StreamHandler)-      parseHandler "FileHandler" v = do-        hdlIo <- parseJSON (Object v)-        return $ toHandler <$> (hdlIo :: IO FileHandler)-      parseHandler "RotatingFileHandler" v = do-        hdlIo <- parseJSON (Object v)-        return $ toHandler <$> (hdlIo :: IO RotatingFileHandler)---instance FromJSON (M.Map String Format1 -> IO SomeHandler) where-  parseJSON = withObject "Handler" $ \v -> do-    hdlio <- parseJSON (Object v)-    key <- v .:? "formatter" .!= "{message}"-    return $ \fs -> hdlio >>= \hdl -> return $-      set (typed @Format1) (M.findWithDefault "" key fs) hdl---instance FromJSON (Sink) where-  parseJSON = withObject "Sink" $ \v ->-    Sink <$> v .:? "logger" .!= "placeholder"-         <*> v .:? "level" .!= def-         <*> v .:? "filterer" .!= []-         <*> (return [])-         <*> v .:? "disabled" .!= False-         <*> v .:? "propagate" .!= False---instance FromJSON (String -> M.Map String SomeHandler -> Sink) where-  parseJSON = withObject "Sink" $ \v -> do-    sink <- parseJSON (Object v)-    keys <- v .:? "handlers" .!= []-    return $ \lgr hs -> sink { logger = if lgr == "root" then "" else lgr-                             , handlers = [hs M.! k | k <- keys]-                             }---type Formatters = M.Map String Format1-type HandlersMakerIO = M.Map String (Formatters -> IO SomeHandler)-type SinksMaker = M.Map String (String -> M.Map String SomeHandler -> Sink)--instance FromJSON (IO Manager) where-  parseJSON = withObject "Manager" $ \v -> do-    fmts :: Formatters <- v .:? "formatters" .!= (object []) >>= parseJSON-    hdls :: HandlersMakerIO <- v .:? "handlers" .!= (object []) >>= parseJSON-    sinks :: SinksMaker <- v .:? "loggers" .!= (object []) >>= parseJSON--    disabled <- v .:? "disabled" .!= False-    catchUncaughtException <- v .:? "catchUncaughtException" .!= False--    return $ do-      hdls' <- sequence $ M.map (\f -> f fmts) hdls-      let sinks' = M.mapWithKey (\k f -> f k hdls') sinks-          root = M.findWithDefault defaultRoot "root" sinks'-          sinks'' = M.delete "root" sinks'-      return $ Manager root sinks'' disabled catchUncaughtException
src/Logging/Config/Json.hs view
@@ -9,7 +9,7 @@  1) 'List' type field will use '[]' as default -2) 'Bool' type filed will use False as default+2) 'Purelude.Bool' type filed will use False as default  3) if the field is an instance of 'Data.Default.Default', use 'def' 
src/Logging/Config/Type.hs view
@@ -26,9 +26,8 @@ import qualified Prelude           as P import           Text.Format -import           Logging.Internal+import           Logging.Prelude import qualified Logging.Types     as T-import           Logging.Utils   data Handler = StreamHandler { level     :: Maybe String@@ -41,14 +40,20 @@                            , formatter :: Maybe String                            , file      :: FilePath                            , encoding  :: Maybe String+                             -- ^ See 'System.IO.mkTextEncoding',+                             -- default is utf8.                            }              | RotatingFileHandler { level       :: Maybe String                                    , filterer    :: Maybe [String]                                    , formatter   :: Maybe String                                    , file        :: FilePath                                    , encoding    :: Maybe String+                                     -- ^ See 'System.IO.mkTextEncoding',+                                     -- default is utf8.                                    , maxBytes    :: Maybe Int+                                     -- ^ Default is 100 MB.                                    , backupCount :: Maybe Int+                                     -- ^ Default is 10.                                    }               deriving (Generic, Show) @@ -100,6 +105,12 @@   where     formatters' :: Map String Format1     formatters' = maybe (singleton "" "{message}") (map fromString) formatters++    stderrHandler :: T.SomeHandler+    stderrHandler = T.toHandler $ T.StreamHandler def [] "{message}" stderr++    defaultRoot :: T.Sink+    defaultRoot = T.Sink "" "DEBUG" [] [stderrHandler] False False      createSink :: Map String T.SomeHandler -> String -> Sink -> IO T.Sink     createSink hs logger Sink{..} = do
src/Logging/Internal.hs view
@@ -30,8 +30,8 @@ import           System.IO                   (stderr, stdout) import           System.IO.Unsafe            (unsafePerformIO) +import           Logging.Prelude import           Logging.Types-import           Logging.Utils  {-# NOINLINE _mgr #-} _mgr :: IORef Manager@@ -108,7 +108,7 @@      callHandlers :: [SomeHandler] -> LogRecord -> IO ()     callHandlers handlers rcd = forM_ handlers $ \hdl ->-      when (isHandlerEnableFor hdl rcd) $ void $ Logging.Types.handle hdl rcd+      Logging.Types.handle hdl rcd      isSinkEnabledFor :: Sink -> LogRecord -> Bool     isSinkEnabledFor sink@Sink{..} rcd@LogRecord{level=level'}@@ -116,11 +116,8 @@       | level' < level = False       | otherwise = filter sink rcd -    isHandlerEnableFor :: SomeHandler -> LogRecord -> Bool-    isHandlerEnableFor hdl rcd@LogRecord{level=level'}-      | level' < (view (typed @Level) hdl) = False-      | otherwise = filter (view (typed @Filterer) hdl) rcd +{-# DEPRECATED stderrHandler, stdoutHandler, defaultRoot "Will be removed" #-}  -- |A 'StreamHandler' bound to 'stderr' stderrHandler :: StreamHandler@@ -130,9 +127,8 @@ stdoutHandler :: StreamHandler stdoutHandler = StreamHandler def [] "{message}" stdout -{-# NOINLINE defaultRoot #-}--- |Default root sink which is used by 'jsonToManager' when __root__ is missed.+-- |Default root sink which is used by "Logging.Config.Json" and+-- "Logging.Config.Yaml" when __root__ sink is omitted. ----- You can use it when you make 'Manager' manually. defaultRoot :: Sink defaultRoot = Sink "" "DEBUG" [] [toHandler stderrHandler] False False
+ src/Logging/Prelude.hs view
@@ -0,0 +1,86 @@+{-|  This module is used internally in log4hs.++Note: don't use this module in your own projects, it may be changed at any time.+-}++module Logging.Prelude+  ( addZonedTime+  , diffZonedTime+  , zonedTimeToPOSIXSeconds+  , timestamp+  , seconds+  , milliseconds+  , microseconds+  , openLogFile+  , tryRenameFile+  , modifyBaseName+  , appendBaseName+  ) where++import           Control.Monad+import           Data.Time.Clock+import           Data.Time.Clock.POSIX+import           Data.Time.LocalTime+import           System.Directory+import           System.Environment+import           System.FilePath+import           System.IO+++instance Eq TextEncoding where+  e1 == e2 = show e1 == show e2+++addZonedTime :: NominalDiffTime -> ZonedTime -> ZonedTime+addZonedTime ndt zt@(ZonedTime _ tz) =+  utcToZonedTime tz $ addUTCTime ndt $ zonedTimeToUTC zt+++diffZonedTime :: ZonedTime -> ZonedTime -> NominalDiffTime+diffZonedTime zt1 zt2 = diffUTCTime (zonedTimeToUTC zt1) (zonedTimeToUTC zt2)+++zonedTimeToPOSIXSeconds :: ZonedTime -> NominalDiffTime+zonedTimeToPOSIXSeconds = utcTimeToPOSIXSeconds . zonedTimeToUTC+++timestamp :: NominalDiffTime -> Double+timestamp = fromRational . toRational+++seconds :: NominalDiffTime -> Integer+seconds = truncate+++milliseconds :: NominalDiffTime -> Integer+milliseconds = truncate . (* 1000)+++microseconds :: NominalDiffTime -> Integer+microseconds = truncate . (* 1000000)+++openLogFile :: FilePath -> TextEncoding -> IO Handle+openLogFile path encoding = do+  absPath <- makeAbsolute path+  progName <- getProgName+  let dir = takeDirectory absPath+      file = if dir == absPath then dir </> (progName ++ ".log") else absPath+  createDirectoryIfMissing True dir+  stream <- openFile file ReadWriteMode+  hSeek stream SeekFromEnd 0+  hSetEncoding stream encoding+  hSetBuffering stream LineBuffering+  return stream+++tryRenameFile :: FilePath -> FilePath -> IO ()+tryRenameFile src dest = doesFileExist src >>= (flip when $ renameFile src dest)+++modifyBaseName :: FilePath -> (String -> String) -> FilePath+modifyBaseName file modify = replaceBaseName file $ modify $ takeBaseName file+++appendBaseName :: FilePath -> String -> FilePath+appendBaseName file suffix = modifyBaseName file (++ suffix)
src/Logging/TH.hs view
@@ -2,6 +2,17 @@ {-# LANGUAGE QuasiQuotes       #-} {-# LANGUAGE TemplateHaskell   #-} +{-| This module provides a series of log routines that create a 'LogRecord'+ and then emit a log event.++The log routines use "Language.Haskell.TH" to obtain some fields related to+where they are called,+e.g. __filename__, __pkgname__, __modulename__, __lineno__++When use these log routines, you should enable __TemplateHaskell__+language extension.+-}+ module Logging.TH   ( logv   , debug
src/Logging/Types/Class/Filterable.hs view
@@ -11,4 +11,4 @@  instance Filterable a => Filterable [a] where   filter [] _       = True-  filter (f:fs) rcd = (filter f) rcd && (filter fs rcd)+  filter (f:fs) rcd = (filter f rcd) && (filter fs rcd)
src/Logging/Types/Class/Handler.hs view
@@ -67,10 +67,11 @@   close _ = return ()    handle :: a -> LogRecord -> IO Bool-  handle hdl rcd = do-    let rv = filter (view (typed @Filterer) hdl) rcd-    when rv $ emit hdl rcd-    return rv+  handle hdl rcd@LogRecord{level=level'} = do+    let level = view (typed @Level) hdl+        res = level' >= level && filter (view (typed @Filterer) hdl) rcd+    when res $ emit hdl rcd+    return res    fromHandler :: SomeHandler -> Maybe a   fromHandler (SomeHandler h) = cast h
src/Logging/Types/Filter.hs view
@@ -14,24 +14,41 @@  -- | 'Filter's are used to perform arbitrary filtering of 'LogRecord's. ----- 'Sink's and 'Handler's can optionally use 'Filter' to filter records--- as desired. It allows events which are below a certain point in the--- sink hierarchy. For example, a filter initialized with "A.B" will allow+-- 'Sink's and 'Handler's can optionally use 'Filter' to filter 'LogRecord's+-- as desired.+-- It allows events which are below a certain point in the+-- sink hierarchy.+-- For example, a 'Filter' initialized with "A.B" will allow -- events logged by loggers "A.B", "A.B.C", "A.B.C.D", "A.B.D" etc. -- but not "A.BB", "B.A.B" etc.--- If initialized name with the empty string, all events are passed.-newtype Filter = Filter Logger deriving (Read, Show, Eq)+--+-- If initialized with the empty string, all events are passed.+--+-- If initialized with a predicate function, the 'Logger' will be considered as+-- the 'Filter''s name, the function will be used to filter 'LogRecord's.+data Filter = Filter Logger (Maybe (LogRecord -> Bool))  instance IsString Filter where-  fromString = Filter+  fromString = flip Filter Nothing +instance Read Filter where+  readsPrec _ s = [(fromString s, "")]++instance Show Filter where+  show (Filter logger _)         = "Filter " ++ (show logger)++-- | If two 'Filter's have same 'Logger' (name), they are equal.+instance Eq Filter where+  (Filter logger _) == (Filter logger' _) = logger == logger'+ instance Filterable Filter where-  filter (Filter self) rcd@LogRecord{..}-    | self == "" = True-    | otherwise = case stripPrefix self logger of-                    Just ""      -> True   -- self == logger-                    Just ('.':_) -> True   -- self == parent logger+  filter (Filter logger' Nothing) rcd@LogRecord{..}+    | logger' == "" = True+    | otherwise = case stripPrefix logger' logger of+                    Just ""      -> True+                    Just ('.':_) -> True                     _            -> False+  filter (Filter _ (Just f)) rcd = f rcd   -- |List of Filter
src/Logging/Types/Handlers/FileHandler.hs view
@@ -11,11 +11,10 @@ import           System.IO import           Text.Format +import           Logging.Prelude import           Logging.Types.Class import           Logging.Types.Filter import           Logging.Types.Level-import           Logging.Utils-import           System.IO.Extra   -- | A handler type which writes logging records, appropriately formatted,
src/Logging/Types/Handlers/RotatingFileHandler.hs view
@@ -12,18 +12,15 @@ import           System.IO import           Text.Format +import           Logging.Prelude import           Logging.Types.Class import           Logging.Types.Filter import           Logging.Types.Level-import           Logging.Utils-import           System.IO.Extra   -- | A handler type which writes logging records, appropriately formatted, -- to a file, it will rotate when file is too large. ----- UNTESTED--- -- Since 0.3.0 -- data RotatingFileHandler = RotatingFileHandler { level       :: Level@@ -32,6 +29,9 @@                                                , file        :: FilePath                                                , encoding    :: TextEncoding                                                , maxBytes    :: Int+                                                 -- ^ Actual file size may be+                                                 -- slightly larger than this+                                                 -- value.                                                , backupCount :: Int                                                , stream      :: IORef Handle                                                } deriving (Generic, Eq)@@ -40,29 +40,24 @@   open RotatingFileHandler{..} =     atomicWriteIORef stream =<< openLogFile file encoding -  emit RotatingFileHandler{..} rcd = do+  emit handler@RotatingFileHandler{..} rcd = do       let msg = format1 formatter rcd-      rollover $ length msg       stream' <- readIORef stream       hPutStrLn stream' msg       hFlush stream'-    where-      rollover :: Int -> IO ()-      rollover mlen = do-        stream' <- readIORef stream-        pos <- hTell =<< readIORef stream-        when (fromEnum pos + mlen >= maxBytes && backupCount > 0)  $ do+      when (backupCount > 0) $ do+        pos <- hTell stream'+        when (fromEnum pos >= maxBytes) $ do           hClose stream'-          rotateNext $ backupCount - 1-          rotateFile file $ modifyBaseName file (++ ".1")--      addSuffix :: String -> Int -> String-      addSuffix src suffix = src ++ "." ++ (show suffix)--      rotateNext :: Int -> IO ()-      rotateNext n = when (n > 0) $ do-        let src = modifyBaseName file $ flip addSuffix n-            dest = modifyBaseName file $ flip addSuffix $ n + 1-        rotateFile src dest+          rotate $ backupCount - 1+          tryRenameFile file $ appendBaseName file ".1"+          open handler+    where+      rotate :: Int -> IO ()+      rotate n = when (n > 0) $ do+        let src = appendBaseName file $ '.' : (show n)+            dest = appendBaseName file $ '.' : (show (n + 1))+        tryRenameFile src dest+        rotate (n - 1)    close RotatingFileHandler{..} = hClose =<< readIORef stream
src/Logging/Types/Level.hs view
@@ -26,9 +26,6 @@ -- newtype Level = Level Int deriving (Eq, Ord) -instance FormatArg Level where-  formatArg = formatArg . show- instance Show Level where   show (Level 0)  = "NOTSET"   show (Level 10) = "DEBUG"@@ -58,3 +55,6 @@  instance Default Level where   def = "NOTSET"++instance FormatArg Level where+  formatArg = formatArg . show
src/Logging/Types/Record.hs view
@@ -61,6 +61,6 @@                            , utctime    :: UTCTime                            , created    :: Double                            , msecs      :: Integer-                           } deriving Generic+                           } deriving (Generic, Show)  instance FormatArg LogRecord
src/Logging/Utils.hs view
@@ -1,70 +1,6 @@-module Logging.Utils-  ( addZonedTime-  , diffZonedTime-  , zonedTimeToPOSIXSeconds-  , timestamp-  , seconds-  , milliseconds-  , microseconds-  , openLogFile-  , rotateFile-  , modifyBaseName+module Logging.Utils {-# DEPRECATED "Renamed as Logging.Prelude" #-}+  ( module Logging.Prelude   ) where -import           Control.Monad-import           Data.Time.Clock-import           Data.Time.Clock.POSIX-import           Data.Time.LocalTime-import           System.Directory-import           System.Environment-import           System.FilePath-import           System.IO --addZonedTime :: NominalDiffTime -> ZonedTime -> ZonedTime-addZonedTime ndt zt@(ZonedTime _ tz) =-  utcToZonedTime tz $ addUTCTime ndt $ zonedTimeToUTC zt---diffZonedTime :: ZonedTime -> ZonedTime -> NominalDiffTime-diffZonedTime zt1 zt2 = diffUTCTime (zonedTimeToUTC zt1) (zonedTimeToUTC zt2)---zonedTimeToPOSIXSeconds :: ZonedTime -> NominalDiffTime-zonedTimeToPOSIXSeconds = utcTimeToPOSIXSeconds . zonedTimeToUTC---timestamp :: NominalDiffTime -> Double-timestamp = fromRational . toRational---seconds :: NominalDiffTime -> Integer-seconds = truncate---milliseconds :: NominalDiffTime -> Integer-milliseconds = truncate . (* 1000)---microseconds :: NominalDiffTime -> Integer-microseconds = truncate . (* 1000000)---openLogFile :: FilePath -> TextEncoding -> IO Handle-openLogFile path encoding = do-  absPath <- makeAbsolute path-  progName <- getProgName-  let dir = takeDirectory absPath-      file = if dir == absPath then dir </> (progName ++ ".log") else absPath-  createDirectoryIfMissing True dir-  stream <- openFile file AppendMode-  hSetEncoding stream encoding-  return stream---rotateFile :: FilePath -> FilePath -> IO ()-rotateFile src dest = doesFileExist src >>= (flip when $ renameFile src dest)---modifyBaseName :: FilePath -> (String -> String) -> FilePath-modifyBaseName file modify = replaceBaseName file $ modify $ takeBaseName file+import           Logging.Prelude
− src/System/IO/Extra.hs
@@ -1,7 +0,0 @@-module System.IO.Extra  where--import           System.IO---instance Eq TextEncoding where-  e1 == e2 = show e1 == show e2
− test/Logging/AesonSpec.hs
@@ -1,222 +0,0 @@-{-# LANGUAGE CPP               #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes       #-}-{-# LANGUAGE RecordWildCards   #-}-{-# LANGUAGE TemplateHaskell   #-}-{-# LANGUAGE TypeApplications  #-}--module Logging.AesonSpec ( spec ) where--import           Control.Lens                (view)-import           Control.Monad-import           Data.Aeson-#if MIN_VERSION_aeson(1, 4, 3)-import           Data.Aeson.QQ.Simple        (aesonQQ)-#else-import           Data.Aeson.QQ               (aesonQQ)-#endif-import           Data.Default                (def)-import           Data.Generics.Product.Typed-import           Data.List                   (intercalate)-import qualified Data.Map                    as M-import           Data.Maybe                  (fromJust)-import           System.IO-import           System.IO.Unsafe-import           Test.Hspec-import           Test.Hspec.QuickCheck-import           Text.Format--import           Logging-import           Logging.Aeson--spec :: Spec-spec = levelSpec >> filterSpec >> handlerSpec >> sinkSpec >> managerSpec---levelSpec :: Spec-levelSpec = describe "Level" $ modifyMaxSize (const 1000) $ do-  prop "decode" $ \x -> let v = Level x in (decode $ encode $ show v) == Just v---filterSpec :: Spec-filterSpec = describe "Filter" $ modifyMaxSize (const 1000) $ do-  prop "decode" $ \fs ->-    let logger = intercalate "." fs-    in (decode $ encode logger) == Just (Filter logger)---handlerSpec :: Spec-handlerSpec = describe "Handler" $ modifyMaxSize (const 1000) $ do-  it "decode StreamHandler simple" $ do-    let StreamHandler{..} = fromJust $ decode $ encode $-          [aesonQQ|{"type": "StreamHandler"}|]-    stream == stderr `shouldBe` True-    level == def `shouldBe` True-    filterer == [] `shouldBe` True-    formatter == "{message}" `shouldBe` True--  it "decode StreamHandler standard" $ do-    let StreamHandler{..} = fromJust $ decode $ encode $-          [aesonQQ|-            {-              "type": "StreamHandler",-              "stream": "stdout",-              "level": "DEBUG",-              "filterer": ["Module.Submodule"],-              "formatter": "default"-            }-          |]--    stream == stdout `shouldBe` True-    level == "DEBUG" `shouldBe` True-    filterer == [Filter "Module.Submodule"] `shouldBe` True-    formatter == "default" `shouldBe` True--  it "decode FileHandler" $ do-    FileHandler{..} <- fromJust $ decode $ encode $-          [aesonQQ|-            {-              "type": "FileHandler",-              "file": "myapp.log",-              "encoding": "UTF-16",-              "level": "INFO",-              "filterer": ["Module.Submodule"],-              "formatter": "default"-            }-          |]--    file `shouldBe` "myapp.log"-    encoding `shouldBe` utf16-    level `shouldBe` "INFO"-    filterer == [Filter "Module.Submodule"] `shouldBe` True-    formatter == "default" `shouldBe` True--  it "decode (formatters -> SomeHandler)" $ do-    let simple = "{logger}: {message}"-        func = fromJust $ decode $ encode-          [aesonQQ|-            {-              "type": "StreamHandler",-              "level": "DEBUG",-              "filterer": ["Module.Submodule"],-              "formatter": "simple"-            }-          |]--    handler@(SomeHandler _) <- func $ M.singleton ("simple" :: String) simple-    view (typed @Level) handler == "DEBUG" `shouldBe` True-    view (typed @Filterer) handler == ["Module.Submodule"] `shouldBe` True-    view (typed @Format1) handler == simple `shouldBe` True---sinkSpec :: Spec-sinkSpec = describe "Sink" $ modifyMaxSize (const 1000) $ do-  it "decode" $ do-    let Sink{..} = fromJust $ decode $ encode $-          [aesonQQ|-            {-              "level": "DEBUG",-              "filterer": ["Module.Submodule"],-              "handlers": ["console"],-              "disabled": false,-              "propagate": true-            }-          |]--    logger `shouldBe` "placeholder"-    level `shouldBe` "DEBUG"-    filterer == ["Module.Submodule"] `shouldBe` True-    length handlers `shouldBe` 0-    disabled `shouldBe` False-    propagate `shouldBe` True--  it "decode (logger -> handlers -> sink)" $ do-    let handlerMap = M.fromList [ ("console", toHandler stderrHandler)-                                , ("file" :: String, toHandler stderrHandler)-                                ]-        func = fromJust $ decode $ encode $-              [aesonQQ|-                {-                  "level": "INFO",-                  "filterer": ["Module.Submodule"],-                  "handlers": ["console", "file"]-                }-              |]-        Sink{..} = func ("MyLogger" :: String) handlerMap--    logger `shouldBe` "MyLogger"-    level `shouldBe` "INFO"-    filterer == ["Module.Submodule"] `shouldBe` True-    length handlers `shouldBe` 2 -- deep test handlers-    disabled `shouldBe` False-    propagate `shouldBe` False---managerSpec :: Spec-managerSpec = describe "Manager" $ modifyMaxSize (const 1000) $ do-  it "decode" $ do-    let Manager{..} = manager_--    length sinks `shouldBe` 1-    M.member "MyLogger" sinks `shouldBe` True-    disabled `shouldBe` False-    catchUncaughtException `shouldBe` True--  it "decode root" $ do-    let Sink{..} = root manager_--    logger `shouldBe` ""-    level `shouldBe` "DEBUG"-    disabled `shouldBe` False-    propagate `shouldBe` False-    length filterer `shouldBe` 0-    length handlers `shouldBe` 1--  it "decode sinks" $ do-    let Sink{..} = (sinks manager_) M.! "MyLogger"--    logger `shouldBe` "MyLogger"-    level `shouldBe` "INFO"-    propagate `shouldBe` False-    disabled `shouldBe` False-    length filterer `shouldBe` 1-    filterer == ["MyLogger.Main"] `shouldBe` True-    length handlers `shouldBe` 2----- Decoding manager globally, or it will throw file resouse busy error-manager_ :: Manager-{-# NOINLINE manager_ #-}-manager_ = unsafePerformIO $ fromJust $ decode $ encode [aesonQQ|{-  "catchUncaughtException": true,-  "loggers": {-      "root": {-        "level": "DEBUG",-        "handlers": ["console"],-        "propagate": false-      },-      "MyLogger": {-        "level": "INFO",-        "filterer": ["MyLogger.Main"],-        "handlers": ["console", "file"],-        "propagate": false-      }-    },-    "handlers": {-      "console": {-        "type": "StreamHandler",-        "stream": "stderr",-        "level": "DEBUG",-        "formatter": "default"-      },-      "file": {-        "type": "FileHandler",-        "level": "INFO",-        "formatter": "default",-        "file": "./default.log"-      }-    },-    "formatters": {-      "default": "{asctime} - {level} - {logger}:{lineno}] {message}"-    }-}|]
− test/Logging/ConfigSpec.hs
@@ -1,158 +0,0 @@-{-# LANGUAGE CPP               #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes       #-}-{-# LANGUAGE RecordWildCards   #-}--module Logging.ConfigSpec ( spec ) where---import           Data.Aeson            as Json-#if MIN_VERSION_aeson(1, 4, 3)-import           Data.Aeson.QQ.Simple  (aesonQQ)-#else-import           Data.Aeson.QQ         (aesonQQ)-#endif-import           Data.ByteString       (ByteString, pack, writeFile)-import           Data.ByteString.Lazy  (unpack)-import qualified Data.Map              as M-import           Data.Yaml             as Yaml-import           Data.Yaml.TH-import           Prelude               hiding (writeFile)-import           System.Directory-import           Test.Hspec-import           Test.Hspec.QuickCheck--import           Logging-import           Logging.Config.Json   as Json-import           Logging.Config.Yaml   as Yaml---spec :: Spec-spec = beforeAll_ prepare $-  describe "Config & Manager" $ modifyMaxSize (const 1000) $ do-    it "json" $ do-      Manager{..} <- Json.getManager jsonRaw-      length sinks `shouldBe` 1-      M.member "MyLogger" sinks `shouldBe` True-      disabled `shouldBe` False-      catchUncaughtException `shouldBe` True--    it "yaml" $ do-      Manager{..} <- Yaml.getManager yamlRaw-      length sinks `shouldBe` 1-      M.member "MyLogger" sinks `shouldBe` True-      disabled `shouldBe` False-      catchUncaughtException `shouldBe` True--    it "json file" $ do-      Manager{..} <- Json.getManagerFile "/tmp/log4hs/config.json"-      length sinks `shouldBe` 1-      M.member "MyLogger" sinks `shouldBe` True-      disabled `shouldBe` False-      catchUncaughtException `shouldBe` True--    it "yaml file" $ do-      Manager{..} <- Yaml.getManagerFile "/tmp/log4hs/config.yaml"-      length sinks `shouldBe` 1-      M.member "MyLogger" sinks `shouldBe` True-      disabled `shouldBe` False-      catchUncaughtException `shouldBe` True--    it "root sink" $ do-      Sink{..} <- root <$> (Json.getManager jsonRaw)--      logger `shouldBe` ""-      level `shouldBe` "DEBUG"-      disabled `shouldBe` False-      propagate `shouldBe` False-      length filterer `shouldBe` 0-      length handlers `shouldBe` 1--    it "sinks" $ do-      Sink{..} <- (flip (M.!) "MyLogger" . sinks) <$> (Json.getManager jsonRaw)--      logger `shouldBe` "MyLogger"-      level `shouldBe` "INFO"-      propagate `shouldBe` False-      disabled `shouldBe` False-      length filterer `shouldBe` 1-      filterer == ["MyLogger.Main"] `shouldBe` True-      length handlers `shouldBe` 2---prepare :: IO ()-prepare = do-  createDirectoryIfMissing False "/tmp/log4hs"-  writeFile "/tmp/log4hs/config.json" jsonRaw-  writeFile "/tmp/log4hs/config.yaml" yamlRaw---jsonRaw :: ByteString-jsonRaw = pack $ unpack $ Json.encode-  [aesonQQ|{-    "sinks": {-      "root": {-        "level": "DEBUG",-        "handlers": ["console"],-        "propagate": false-      },-      "MyLogger": {-        "level": "INFO",-        "filterer": ["MyLogger.Main"],-        "handlers": ["console", "file"],-        "propagate": false-      }-    },-    "handlers": {-      "console": {-        "type": "StreamHandler",-        "stream": "stderr",-        "level": "DEBUG",-        "formatter": "default"-      },-      "file": {-        "type": "FileHandler",-        "level": "INFO",-        "formatter": "default",-        "file": "./default.log"-      }-    },-    "formatters": {-      "default": "{asctime} - {level} - {logger}:{lineno}] {message}"-    },-    "catchUncaughtException": true-  }|]---yamlRaw :: ByteString-yamlRaw = Yaml.encode-  [yamlQQ|-    sinks:-      root:-        level: DEBUG-        handlers:-        - console-        propagate: false-      MyLogger:-        level: INFO-        filterer:-        - MyLogger.Main-        handlers:-        - console-        - file-        propagate: false-    handlers:-      console:-        type: StreamHandler-        stream: stderr-        level: DEBUG-        formatter: default-      file:-        type: FileHandler-        level: INFO-        formatter: default-        file: "./default.log"-    formatters:-      default: "{asctime} - {level} - {logger}:{lineno}] {message}"-    catchUncaughtException: true-  |]
− test/Logging/TypesSpec.hs
@@ -1,21 +0,0 @@-{-# LANGUAGE RecordWildCards    #-}-{-# LANGUAGE StandaloneDeriving #-}--module Logging.TypesSpec ( spec ) where--import           Data.String           (fromString)-import           Test.Hspec-import           Test.Hspec.QuickCheck-import           Test.QuickCheck--import           Logging.Types-import           Logging.Utils--spec :: Spec-spec = levelSpec---levelSpec :: Spec-levelSpec = describe "Level" $ modifyMaxSize (const 1000) $ do-  prop "read and show" $ \x -> (read . show) (Level x) == (Level x)-  prop "overload string" $ \x -> fromString (show (Level x)) == (Level x)
− test/LoggingSpec.hs
@@ -1,176 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes       #-}-{-# LANGUAGE RecordWildCards   #-}-{-# LANGUAGE TemplateHaskell   #-}--module LoggingSpec ( spec ) where--import           Control.Monad-import           Data.Default            (def)-import           Data.List               (intercalate)-import qualified Data.Map                as M-import           Data.Word-import           Prelude                 hiding (error)-import           System.IO-import           System.IO.Unsafe        (unsafePerformIO)-import           System.Process          (createPipe)-import           Test.Hspec-import           Test.Hspec.QuickCheck-import           Test.QuickCheck         hiding (run)-import qualified Test.QuickCheck.Monadic as Q-import           Text.Format--import           Logging--spec :: Spec-spec = basicSpec >> formatSpec >> filterSpec >> propagateSpec---basicSpec :: Spec-basicSpec = describe "Basic" $ modifyMaxSize (const 1000) $ do-  prop "log to root" $ \(PrintableString msg) ->-    runLog "" "DEBUG" msg "debug" (== msg)-  prop "log to Log4hs" $ \(PrintableString msg) ->-    runLog "Log4hs" "INFO" msg "debug" (== msg)-  prop "log to Log4hs.Json" $ \(PrintableString msg) ->-    runLog "Log4hs.Json" "WARN" msg "debug" (== msg)-  prop "disabled some" $ \(PrintableString msg) level ->-    runLog "Log10000hs" level msg "debug" (== "")-  prop "disabled all" $ \(PrintableString msg) loggers level ->-    let logger = intercalate "." $ [getPrintableString x | x <- loggers]-    in runDisabledLog logger level msg---formatSpec :: Spec-formatSpec = describe "Format" $ modifyMaxSize (const 1000) $ do-  prop "simple" $ \(PrintableString msg) ->-    runLog "" "DEBUG" msg "debug" (== msg)-  prop "logger" $ \(PrintableString msg) (Log4hsLogger logger) -> do-    runLog logger "WARN" msg "info" (== logger ++ ": " ++ msg)-  prop "level" $ \(PrintableString msg) (Log4hsLogger logger) fmtlevel ->-    let (Log4hsHighLevel level) = fmtlevel-        out = logger ++ " " ++ (show level) ++ "] " ++ msg-    in runLog logger level msg "error" (== out)---filterSpec :: Spec-filterSpec = describe "Filter" $ modifyMaxSize (const 1000) $ do-  prop "root sink && debug handler" $ \(PrintableString msg) level ->-    runLog "" level msg "debug" $ (== if level >= "DEBUG" then msg else "")-  prop "Log4hs sink && info handler" $ \(PrintableString msg) level ->-    let out = if level >= "INFO" then "Log4hs: " ++ msg else ""-    in runLog "Log4hs" level msg "info" $ (== out)-  prop "Log4hs.Json sink && error handler" $ \(PrintableString msg) level ->-    let out = if level >= "ERROR"-                then "Log4hs.Json " ++ (show level) ++ "] " ++ msg-                else ""-    in runLog "Log4hs.Json" level msg "error" $ (== out)-  prop "handler's filterer" $ \(PrintableString msg) (Small v)->-    let logger = "Log4hs.V" ++ (show v)-        out = if v == (3 :: Word8) then logger ++ ": " ++ msg else ""-    in runLog logger "INFO" msg "infov3" (== out)---propagateSpec :: Spec-propagateSpec = describe "Propagate" $ modifyMaxSize (const 1000) $ do-  prop "to root (real)" $ \(PrintableString msg) ->-    runLog "Log8hs" "WARN" msg "debug" $ (== msg)-  prop "to root (virtual)" $ \(PrintableString msg) ->-    runLog "Log0hs" "WARN" msg "debug" $ (== msg)-  prop "to parent (real)" $ \(PrintableString msg) ->-    runLog "Log4hs.Json" "WARN" msg "info" $ (== "Log4hs.Json: " ++ msg)-  prop "to parent (virtual)" $ \(PrintableString msg) ->-    runLog "Log4hs.Ini" "WARN" msg "info" $ (== "Log4hs.Ini: " ++ msg)-  prop "to grandparent (real)" $ \(PrintableString msg) ->-    runLog "Log8hs.Json" "WARN" msg "debug" $ (== msg)-  prop "to grandparent (virtual)" $ \(PrintableString msg) ->-    runLog "Log0hs.Json" "WARN" msg "debug" $ (== msg)-  prop "disabled" $ \(PrintableString msg) ->-    runLog "Log4hs" "FATAL" msg "fatal" $ (== "")---runLog :: Logger -> Level -> String -> String -> (String -> Bool) -> Property-runLog logger level msg stream test = Q.monadicIO $ do-  out <- Q.run $ createManager False >>= \(mgr, streams) -> run mgr $-    $(logv) logger level msg  >> hTryGetLine (streams M.! stream)-  Q.assert $ test out---runDisabledLog :: Logger -> Level -> String -> Property-runDisabledLog logger level msg = Q.monadicIO $ do-  outs <- Q.run $ createManager True >>= \(mgr, streams) -> run mgr $-    $(logv) logger level msg  >> sequence (M.map hTryGetLine streams)-  Q.assert $ M.empty == M.filter (/= "") outs---createManager :: Bool -> IO (Manager, M.Map String Handle)-createManager disabled = do-  debugPh <- createPipeHandler "DEBUG" [] (formatters M.! "simple")-  infoPh <- createPipeHandler "INFO" [] (formatters M.! "logger")-  infov3Ph <- createPipeHandler "INFO" ["Log4hs.V3"] (formatters M.! "logger")-  errorPh <- createPipeHandler "ERROR" [] (formatters M.! "level")-  fatalPh <- createPipeHandler "FATAL" [] (formatters M.! "logger")--  let root = Sink "" "DEBUG" [] [fst debugPh, fst fatalPh] False False-      handlers = [fst debugPh, fst infoPh, fst infov3Ph, fst errorPh]-      log4hs = Sink "Log4hs" "INFO" [] handlers False False-      log4hsJson = Sink "Log4hs.Json" "WARN" [] [] False True-      log4hsYaml = Sink "Log4hs.Yaml" "WARN" [] [] False True-      log8hs = Sink "Log8hs" "INFO" [] [] False True-      log8hsJson = Sink "Log8hs.Json" "INFO" [] [] False True-      log10000hs = Sink "Log8hs.Json" "INFO" [] [fst debugPh] True False-      sinks = M.fromList [ ("Log4hs", log4hs)-                         , ("Log4hs.Json", log4hsJson)-                         , ("Log4hs.Yaml", log4hsYaml)-                         , ("Log8hs", log8hs)-                         , ("Log8hs.Json", log8hsJson)-                         , ("Log10000hs", log10000hs)-                         ]-      streams = M.fromList $ [ ("debug", snd debugPh)-                             , ("info", snd infoPh)-                             , ("infov3", snd infov3Ph)-                             , ("error", snd errorPh)-                             , ("fatal", snd fatalPh)-                             ]-  return (Manager root sinks disabled False, streams)---formatters :: M.Map String Format1-formatters = M.fromList-  [ ("simple", "{message}")-  , ("logger", "{logger}: {message}")-  , ("level", "{logger} {level}] {message}")-  ]---createPipeHandler :: Level -> Filterer -> Format1 -> IO (SomeHandler, Handle)-createPipeHandler level filterer formatter = do-  (read, write) <- createPipe-  hSetEncoding read utf8-  hSetEncoding write utf8-  return $ ( toHandler $ StreamHandler level filterer formatter write-           , read-           )---hTryGetLine :: Handle -> IO String-hTryGetLine hdl = do-  ready <- hReady hdl-  if ready then hGetLine hdl else return ""---instance Arbitrary Level where-  arbitrary = Level <$> arbitrary---newtype Log4hsLogger = Log4hsLogger Logger deriving Show--instance Arbitrary Log4hsLogger where-  arbitrary = elements $-    map Log4hsLogger ["Log4hs", "Log4hs.Json", "Log4hs.Yaml"]---newtype Log4hsHighLevel = Log4hsHighLevel Level deriving Show--instance Arbitrary Log4hsHighLevel where-  arbitrary = elements $ map Log4hsHighLevel ["ERROR", "FATAL", Level 100]
+ test/LoggingTest/ConfigSpec.hs view
@@ -0,0 +1,158 @@+{-# LANGUAGE CPP               #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes       #-}+{-# LANGUAGE RecordWildCards   #-}++module LoggingTest.ConfigSpec ( spec ) where+++import           Data.Aeson            as Json+#if MIN_VERSION_aeson(1, 4, 3)+import           Data.Aeson.QQ.Simple  (aesonQQ)+#else+import           Data.Aeson.QQ         (aesonQQ)+#endif+import           Data.ByteString       (ByteString, pack, writeFile)+import           Data.ByteString.Lazy  (unpack)+import qualified Data.Map              as M+import           Data.Yaml             as Yaml+import           Data.Yaml.TH+import           Prelude               hiding (writeFile)+import           System.Directory+import           Test.Hspec+import           Test.Hspec.QuickCheck++import           Logging+import           Logging.Config.Json   as Json+import           Logging.Config.Yaml   as Yaml+++spec :: Spec+spec = beforeAll_ prepare $+  describe "Config & Manager" $ modifyMaxSize (const 1000) $ do+    it "json" $ do+      Manager{..} <- Json.getManager jsonRaw+      length sinks `shouldBe` 1+      M.member "MyLogger" sinks `shouldBe` True+      disabled `shouldBe` False+      catchUncaughtException `shouldBe` True++    it "yaml" $ do+      Manager{..} <- Yaml.getManager yamlRaw+      length sinks `shouldBe` 1+      M.member "MyLogger" sinks `shouldBe` True+      disabled `shouldBe` False+      catchUncaughtException `shouldBe` True++    it "json file" $ do+      Manager{..} <- Json.getManagerFile "/tmp/log4hs/config.json"+      length sinks `shouldBe` 1+      M.member "MyLogger" sinks `shouldBe` True+      disabled `shouldBe` False+      catchUncaughtException `shouldBe` True++    it "yaml file" $ do+      Manager{..} <- Yaml.getManagerFile "/tmp/log4hs/config.yaml"+      length sinks `shouldBe` 1+      M.member "MyLogger" sinks `shouldBe` True+      disabled `shouldBe` False+      catchUncaughtException `shouldBe` True++    it "root sink" $ do+      Sink{..} <- root <$> (Json.getManager jsonRaw)++      logger `shouldBe` ""+      level `shouldBe` "DEBUG"+      disabled `shouldBe` False+      propagate `shouldBe` False+      length filterer `shouldBe` 0+      length handlers `shouldBe` 1++    it "sinks" $ do+      Sink{..} <- (flip (M.!) "MyLogger" . sinks) <$> (Json.getManager jsonRaw)++      logger `shouldBe` "MyLogger"+      level `shouldBe` "INFO"+      propagate `shouldBe` False+      disabled `shouldBe` False+      length filterer `shouldBe` 1+      filterer == ["MyLogger.Main"] `shouldBe` True+      length handlers `shouldBe` 2+++prepare :: IO ()+prepare = do+  createDirectoryIfMissing False "/tmp/log4hs"+  writeFile "/tmp/log4hs/config.json" jsonRaw+  writeFile "/tmp/log4hs/config.yaml" yamlRaw+++jsonRaw :: ByteString+jsonRaw = pack $ unpack $ Json.encode+  [aesonQQ|{+    "sinks": {+      "root": {+        "level": "DEBUG",+        "handlers": ["console"],+        "propagate": false+      },+      "MyLogger": {+        "level": "INFO",+        "filterer": ["MyLogger.Main"],+        "handlers": ["console", "file"],+        "propagate": false+      }+    },+    "handlers": {+      "console": {+        "type": "StreamHandler",+        "stream": "stderr",+        "level": "DEBUG",+        "formatter": "default"+      },+      "file": {+        "type": "FileHandler",+        "level": "INFO",+        "formatter": "default",+        "file": "./default.log"+      }+    },+    "formatters": {+      "default": "{asctime} - {level} - {logger}:{lineno}] {message}"+    },+    "catchUncaughtException": true+  }|]+++yamlRaw :: ByteString+yamlRaw = Yaml.encode+  [yamlQQ|+    sinks:+      root:+        level: DEBUG+        handlers:+        - console+        propagate: false+      MyLogger:+        level: INFO+        filterer:+        - MyLogger.Main+        handlers:+        - console+        - file+        propagate: false+    handlers:+      console:+        type: StreamHandler+        stream: stderr+        level: DEBUG+        formatter: default+      file:+        type: FileHandler+        level: INFO+        formatter: default+        file: "./default.log"+    formatters:+      default: "{asctime} - {level} - {logger}:{lineno}] {message}"+    catchUncaughtException: true+  |]
+ test/LoggingTest/InternalSpec.hs view
@@ -0,0 +1,107 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes       #-}+{-# LANGUAGE RecordWildCards   #-}+{-# LANGUAGE TemplateHaskell   #-}++module LoggingTest.InternalSpec ( spec ) where++import           Control.Monad+import           Data.Default            (def)+import           Data.List               (intercalate)+import qualified Data.Map                as M+import           Data.Word+import           Prelude                 hiding (error)+import           System.IO+import           System.IO.Unsafe        (unsafePerformIO)+import           System.Process          (createPipe)+import           Test.Hspec+import           Test.Hspec.QuickCheck+import           Test.QuickCheck+import           Test.QuickCheck.Monadic+import           Text.Format++import           Logging                 hiding (run)+import           Logging.TH+import           LoggingTest.Prelude++spec :: Spec+spec = describe "run & log" $ do+  let levels = ["DEBUG", "INFO", "WARN", "ERROR", "FATAL"]+      loggers = [ ("root", "DEBUG", [], ["DEBUG"], False, False)+                , ("Disabled", "DEBUG", [], ["DEBUG"], True, False)+                , ("Debug", "DEBUG", [], ["DEBUG", "INFO"], False, False)+                , ("Info", "INFO", ["Info.A"], ["DEBUG", "INFO"], False, False)+                , ("Warn", "WARN", [], ["WARN"], False, False)+                , ("Warn.Error", "ERROR", [], ["ERROR"], False, False)+                , ("Error", "ERROR", [], ["ERROR"], False, True)+                , ("Error.Fatal", "FATAL", [], ["FATAL"], False, True)+                ]+  handlers <- fmap M.fromList $ runIO $ forM levels $ \level -> do+    (read, write) <- createPipe+    hSetEncoding read utf8 >> hSetEncoding write utf8+    let handler = toHandler $ StreamHandler level [] "{message}" write+    return $ (level, (handler, read))+  sinks <- fmap M.fromList $ runIO $ forM loggers $ \item -> do+    let (logger, level, fs, hs, disabled, propagate) = item+        logger' = if logger == "root" then "" else logger+        hs' = [fst (handlers M.! h) | h <- hs]+        sink = Sink logger' level fs hs' disabled propagate+    return (logger, sink)++  let root = sinks M.! "root"+      sinks' = M.delete "root" sinks+      disabledManager = Manager root sinks' True False+      manager = Manager root sinks' False False++  prop "filter" $ \(MessageString message) -> monadicIO $ do+    -- manager disabled+    run $ runLog disabledManager $ $(info) "Debug" message+    msgs <- run $ forM ["DEBUG", "INFO"] $ \level ->+      hTryGetLine $ snd $ handlers M.! level+    assert $ ["", ""] ==  msgs+    -- sink disabled+    run $ runLog manager $ $(debug) "Disabled" message+    msg1 <- run $ hTryGetLine $ snd $ handlers M.! "DEBUG"+    assert $ msg1 == ""+    -- sink level reject+    run $ runLog manager $ $(debug) "Info.A" message+    msgs1 <- run $ forM ["DEBUG", "INFO"] $ \level ->+      hTryGetLine $ snd $ handlers M.! level+    assert $ ["", ""] == msgs1+    -- sink filterer reject+    run $ runLog manager $ $(info) "Info.B" message+    msgs2 <- run $ forM ["DEBUG", "INFO"] $ \level ->+      hTryGetLine $ snd $ handlers M.! level+    assert $ ["", ""] ==  msgs2+    -- pass 1+    run $ runLog manager $ $(info) "Debug" message+    msgs3 <- run $ forM ["DEBUG", "INFO"] $ \level ->+      hTryGetLine $ snd $ handlers M.! level+    assert $ [message, message] == msgs3+    -- pass 2+    run $ runLog manager $ $(info) "Info.A" message+    msgs4 <- run $ forM ["DEBUG", "INFO"] $ \level ->+      hTryGetLine $ snd $ handlers M.! level+    assert $ [message, message] == msgs4++  prop "propagate" $ \(MessageString message) -> monadicIO $ do+    -- propagation disabled 1+    run $ runLog manager $ $(warn) "Warn" message+    msgs <- run $ forM ["DEBUG", "WARN"] $ \level ->+      hTryGetLine $ snd $ handlers M.! level+    assert $ ["", message] == msgs+    -- propagation disabled 2+    run $ runLog manager $ $(error) "Warn.Error" message+    msgs1 <- run $ forM ["DEBUG", "WARN", "ERROR"] $ \level ->+      hTryGetLine $ snd $ handlers M.! level+    assert $ ["", "", message] == msgs1+    -- propagation enabled 1+    run $ runLog manager $ $(error) "Error" message+    msgs2 <- run $ forM ["DEBUG", "ERROR"] $ \level ->+      hTryGetLine $ snd $ handlers M.! level+    assert $ [message, message] == msgs2+    -- propagation enabled 2+    run $ runLog manager $ $(fatal) "Error.Fatal" message+    msgs3 <- run $ forM ["DEBUG", "ERROR", "FATAL"] $ \level ->+      hTryGetLine $ snd $ handlers M.! level+    assert $ [message, message, message] == msgs3
+ test/LoggingTest/Prelude.hs view
@@ -0,0 +1,82 @@+{-# LANGUAGE NamedFieldPuns  #-}+{-# LANGUAGE RecordWildCards #-}++module LoggingTest.Prelude+  ( Alphabet(..)+  , LoggerString(..)+  , MessageString(..)+  , hTryGetLine+  , parent+  , runLog+  ) where+++import           Control.Monad+import           Data.List           (dropWhileEnd, intersperse)+import           Data.String+import           Data.Time.LocalTime+import           System.IO+import           Test.QuickCheck++import qualified Logging+import           Logging.Prelude+import           Logging.Types+++data Alphabet = Alphabet { getAlphabet :: Char } deriving Show++instance Arbitrary Alphabet where+  arbitrary = Alphabet <$> elements ['a'..'z']+++instance Arbitrary Level where+  arbitrary = (Level . getNonNegative) <$> arbitrary+++newtype LoggerString = LoggerString { getLoggerString :: Logger }++instance Arbitrary LoggerString where+  arbitrary = do+    ls <- listOf (elements ['A'..'Z'])+    return $ LoggerString $ intersperse '.' $ ls+++newtype MessageString = MessageString { getMessageString :: String+                                      } deriving Show++instance Arbitrary MessageString where+  arbitrary = do+    ms <- forM [1..99] $ \_ -> arbitraryUnicodeChar `suchThat` (/= '\n')+    return $ MessageString ms+++instance Arbitrary LogRecord where+  arbitrary = do+    logger <- getLoggerString <$> arbitrary+    level <- arbitrary+    message <- getMessageString <$> arbitrary+    pathname <- arbitrary+    filename <- arbitrary+    pkgname <- arbitrary+    modulename <- arbitrary+    lineno <- arbitrary+    let asctime = read "1970-01-01 00:00:00"+        utctime = zonedTimeToUTC asctime+        difftime = zonedTimeToPOSIXSeconds asctime+        created = timestamp difftime+        msecs   = milliseconds difftime+    return $ LogRecord{..}+++hTryGetLine :: Handle -> IO String+hTryGetLine hdl = do+  ready <- hReady hdl+  if ready then hGetLine hdl else return ""+++parent :: Logger -> Logger+parent = dropWhileEnd (== '.') . dropWhileEnd (/= '.')+++runLog :: Manager -> IO a -> IO a+runLog = Logging.run
+ test/LoggingTest/TypesSpec.hs view
@@ -0,0 +1,133 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards   #-}+{-# LANGUAGE TypeApplications  #-}++module LoggingTest.TypesSpec ( spec ) where++import           Control.Lens                hiding (Level (..))+import           Control.Monad+import           Data.Default                (def)+import           Data.Generics.Product.Typed+import           Data.IORef+import           Data.String                 (fromString)+import           Prelude                     hiding (filter)+import           System.IO+import           System.Process              (createPipe)+import           Test.Hspec+import           Test.Hspec.QuickCheck+import           Test.QuickCheck+import           Test.QuickCheck.Monadic++import           Logging.Prelude+import           Logging.Types+import           LoggingTest.Prelude++spec :: Spec+spec = do+  levelSpec+  filterSpec+  handlerSpec+++levelSpec :: Spec+levelSpec = describe "Level" $ do+  prop "read & show" $ \v ->+    let level = Level v in read (show level) == level++  prop "overload string" $ \v ->+    let level = Level v in fromString (show level) == level++filterSpec :: Spec+filterSpec = describe "Filter" $ do+  prop "pass" $ \rcd@LogRecord{..} ->+    let filterer = map fromString $+                    [logger, parent logger, parent $ parent logger, ""]+    in filter (filterer :: Filterer) rcd++  prop "reject" $ \rcd@LogRecord{..} ->+    let filters = map (fromString . (++ "Test")) $+                    [logger, parent logger, parent $ parent logger, ""]+    in all (== False) [filter (f :: Filter) rcd | f <- filters]+++handlerSpec :: Spec+handlerSpec = describe "Handler" $ do+  (read, write) <- runIO createPipe+  fileRef <- runIO $ newIORef undefined+  rotateFileRef <- runIO $ newIORef undefined+  runIO $ hSetEncoding read utf8 >> hSetEncoding write utf8+  let handler = toHandler $ StreamHandler def [] "{message}" write+      fileHandler = toHandler $+        FileHandler def [] "{message}" "/tmp/log4hs/file.log" utf8 fileRef+      rotateHandler = toHandler $+        RotatingFileHandler def [] "{message}" "/tmp/log4hs/rotate.log" utf8+          1000 5 rotateFileRef+  runIO $ open handler >> open fileHandler >> open rotateHandler++  prop "filter" $ \(rcd@LogRecord{..}) -> monadicIO $ do+    let loggers = [logger, parent logger, parent $ parent logger, ""]+    -- level reject+    forM_ logger $ \logger -> do+      let handler' = handler & typed @Level .~ (succ level)+      res <- run $ handle handler' rcd+      assert $ not res+    -- filterer reject+    forM_ loggers $ \logger -> do+      let handler' = handler & typed @Filterer .~ [fromString $ logger ++ "Test"]+      res <- run $ handle handler' rcd+      assert $ not res+    -- filterer pass+    forM_ loggers $ \logger -> do+      let handler' = handler & typed @Filterer .~ [fromString logger]+      res <- run $ handle handler' rcd+      msg <- run $ hTryGetLine read+      assert res+      assert (msg == message)+    -- empty filterer+    res <- run $ handle handler rcd+    msg <- run $ hTryGetLine read+    assert res+    assert (msg == message)++  prop "file" $ \(rcd@LogRecord{..}) -> monadicIO $ do+    file <- run $ readIORef fileRef+    run $ hSetFileSize file 0 >> hSeek file AbsoluteSeek 0+    run $ handle fileHandler rcd+    run $ hSeek file AbsoluteSeek 0+    msg <- run $ hTryGetLine file+    assert (msg == message)++  prop "rotate" $ \(rcd, Alphabet c) -> monadicIO $ do+    file <- run $ readIORef rotateFileRef+    run $ hSetFileSize file 0 >> hSeek file AbsoluteSeek 0+    let message = replicate 99 c+        rcd' = rcd { message = message }+    -- not rollover yet+    run $ forM [1..9] $ \_ -> handle rotateHandler rcd'+    run $ hSeek file AbsoluteSeek 0+    msgs <- run $ forM [1..9] $ \_ -> hTryGetLine file+    assert $ all (== message) msgs+    -- rollover+    run $ handle rotateHandler rcd'+    closed <- run $ hIsClosed file+    assert closed+    pos <- run $ readIORef rotateFileRef >>= hTell+    assert (pos == 0)+    -- rotate.1.log+    file1 <- run $ openLogFile "/tmp/log4hs/rotate.1.log" utf8+    run $ hSeek file1 AbsoluteSeek 0+    msgs1 <- run $ forM [1..10] $ \_ -> hTryGetLine file1+    run $ hClose file1+    assert $ all (== message) msgs1+    -- rollover again+    file' <- run $ readIORef rotateFileRef+    run $ forM [1..10] $ \_ -> handle rotateHandler rcd'+    closed1 <- run $ hIsClosed file'+    assert closed1+    -- rotate.2.log+    file2 <- run $ openLogFile "/tmp/log4hs/rotate.2.log" utf8+    run $ hSeek file2 AbsoluteSeek 0+    msgs2 <- run $ forM [1..10] $ \_ -> hTryGetLine file2+    run $ hClose file2+    -- rotate.2.log+    assert $ all (== message) msgs2
+ test/Main.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
− test/Spec.hs
@@ -1,1 +0,0 @@-{-# OPTIONS_GHC -F -pgmF hspec-discover #-}