log-warper 0.3.1 → 0.4
raw patch · 6 files changed
+252/−135 lines, 6 filesdep ~data-defaultdep ~lens
Dependency ranges changed: data-default, lens
Files
- examples/Playground.hs +5/−5
- log-warper.cabal +3/−3
- src/System/Wlog/Launcher.hs +42/−46
- src/System/Wlog/LoggerConfig.hs +170/−39
- src/System/Wlog/Wrapper.hs +16/−29
- test/Test/Wlog/RollingSpec.hs +16/−13
examples/Playground.hs view
@@ -7,10 +7,10 @@ import Data.Monoid ((<>)) import qualified Data.Text as T (pack) -import System.Wlog (CanLog, dispatchEvents, initLoggingFromYaml, logDebug,- logError, logInfo, logNotice, logWarning,- modifyLoggerName, releaseAllHandlers, runPureLog,- usingLoggerName)+import System.Wlog (CanLog, buildAndSetupYamlLogging, dispatchEvents,+ logDebug, logError, logInfo, logNotice, logWarning,+ modifyLoggerName, prefixB, productionB,+ releaseAllHandlers, runPureLog, usingLoggerName) testLogging :: (CanLog m) => m () testLogging = usingLoggerName "node" $ do@@ -35,6 +35,6 @@ main :: IO () main = bracket_- (initLoggingFromYaml "logger-config-example.yaml" $ Just "logs")+ (buildAndSetupYamlLogging (productionB <> prefixB "logs") "logger-config-example.yaml") releaseAllHandlers (testLogging >> showPureLog)
log-warper.cabal view
@@ -1,5 +1,5 @@ name: log-warper-version: 0.3.1+version: 0.4 synopsis: Flexible, configurable, monadic and pretty logging homepage: https://github.com/serokell/log-warper license: MIT@@ -37,7 +37,6 @@ , ansi-terminal >= 0.6.2.3 , base >= 4.7 && < 5 , bytestring >= 0.10.8.1- , data-default >= 0.7.1 , directory >= 1.2.6.2 , dlist >= 0.8.0.2 , errors >= 2.1.2@@ -62,7 +61,7 @@ hs-source-dirs: src default-language: Haskell2010- ghc-options: -Wall -fno-warn-orphans -fprint-potential-instances+ ghc-options: -Wall -fno-warn-orphans default-extensions: GeneralizedNewtypeDeriving DeriveDataTypeable@@ -108,6 +107,7 @@ , filepath , hspec , HUnit+ , lens , log-warper , QuickCheck , universum
src/System/Wlog/Launcher.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE ViewPatterns #-}+{-# LANGUAGE NoImplicitPrelude #-} -- | -- Module : System.Wlog.Parser@@ -26,12 +26,14 @@ -- @node@ and @node.comm@. module System.Wlog.Launcher- ( initLoggingFromYaml- , initLoggingFromYamlWithMapper+ ( buildAndSetupYamlLogging+ , initLoggingFromYaml , parseLoggerConfig- , traverseLoggerConfig+ , setupLogging ) where +import Universum+ #if PatakDebugSkovorodaBARDAQ import qualified Data.ByteString.Char8 as BS (putStrLn) import Data.Yaml.Pretty (defConfig, encodePretty)@@ -39,13 +41,10 @@ import Control.Error.Util ((?:)) import Control.Exception (throwIO)-import Control.Monad (join)-import Control.Monad.Extra (whenJust)+import Control.Monad (join, when) import Control.Monad.IO.Class (MonadIO (liftIO)) -import Data.Foldable (for_) import qualified Data.HashMap.Strict as HM hiding (HashMap)-import Data.Maybe (fromMaybe) import Data.Monoid ((<>)) import Data.Text (unpack) import Data.Yaml (decodeFileEither)@@ -58,50 +57,50 @@ import System.Log.Logger (addHandler, updateGlobalLogger) import System.Wlog.Formatter (setStdoutFormatter)-import System.Wlog.LoggerConfig (LoggerConfig (..), LoggerTree (..),- RotationParameters)+import System.Wlog.LoggerConfig (LoggerConfig (..), LoggerTree (..)) import System.Wlog.LoggerName (LoggerName (..)) import System.Wlog.Roller (rotationFileHandler)-import System.Wlog.Wrapper (Severity (Debug, Warning), convertSeverity,- initLogging, setSeverityMaybe)+import System.Wlog.Wrapper (Severity (Debug), convertSeverity,+ initTerminalLogging, setSeverityMaybe) data HandlerFabric = forall h . LogHandler h => HandlerFabric (FilePath -> Priority -> IO h) -- | This function traverses 'LoggerConfig' initializing all subloggers--- with 'Severity' and redirecting output in file handlers. Also takes--- 'FilePath' prefix to create directory for file handlers output. Create--- logging rotation if appropriate arguments are passed.-traverseLoggerConfig- :: MonadIO m- => (LoggerName -> LoggerName) -- ^ mapper to transform logger names during traversal- -> Maybe RotationParameters -- ^ rotation parameters for roller handle- -> LoggerTree -- ^ given 'LoggerConfig' to traverse- -> Maybe FilePath -- ^ prefix for file handlers- -> m ()-traverseLoggerConfig logMapper mrot tree (fromMaybe "." -> handlerPrefix) = do+-- with 'Severity' and redirecting output in file handlers.+-- See 'LoggerConfig' for more details.+setupLogging :: MonadIO m => LoggerConfig -> m ()+setupLogging LoggerConfig{..} = do liftIO $ createDirectoryIfMissing True handlerPrefix- initLogging Warning- processLoggers mempty tree+ when consoleOutput $ initTerminalLogging isShowTime _lcTermSeverity+ processLoggers mempty _lcTree where+ handlerPrefix = _lcFilePrefix ?: "."+ logMapper = appEndo _lcMapper+ isShowTime = getAny _lcShowTime+ consoleOutput = getAny _lcConsoleOutput+ handlerFabric :: HandlerFabric- handlerFabric = case mrot of+ handlerFabric = case _lcRotation of Nothing -> HandlerFabric fileHandler Just rot -> HandlerFabric $ rotationFileHandler rot processLoggers :: MonadIO m => LoggerName -> LoggerTree -> m () processLoggers parent LoggerTree{..} = do- setSeverityMaybe parent ltSeverity+ -- This prevents logger output to appear in terminal+ unless (parent == mempty && not consoleOutput) $+ setSeverityMaybe parent _ltSeverity - whenJust ltFile $ \fileName -> liftIO $ do- let filePriority = convertSeverity $ ltSeverity ?: Debug+ whenJust _ltFile $ \fileName -> liftIO $ do+ let filePriority = convertSeverity $ _ltSeverity ?: Debug let handlerPath = handlerPrefix </> fileName case handlerFabric of- HandlerFabric fabric -> do- thisLoggerHandler <- setStdoutFormatter True <$> fabric handlerPath filePriority- updateGlobalLogger (loggerName parent) $ addHandler thisLoggerHandler+ HandlerFabric fabric -> do+ let handlerCreator = fabric handlerPath filePriority+ thisLoggerHandler <- setStdoutFormatter isShowTime <$> handlerCreator+ updateGlobalLogger (loggerName parent) $ addHandler thisLoggerHandler - for_ (HM.toList ltSubloggers) $ \(name, loggerConfig) -> do+ for_ (HM.toList _ltSubloggers) $ \(name, loggerConfig) -> do let thisLoggerName = LoggerName $ unpack name let thisLogger = parent <> logMapper thisLoggerName processLoggers thisLogger loggerConfig@@ -111,22 +110,19 @@ parseLoggerConfig loggerConfigPath = liftIO $ join $ either throwIO return <$> decodeFileEither loggerConfigPath --- | Initialize logger hierarchy from configuration file.--- See this module description.-initLoggingFromYamlWithMapper- :: MonadIO m- => (LoggerName -> LoggerName)- -> FilePath- -> Maybe FilePath- -> m ()-initLoggingFromYamlWithMapper loggerMapper loggerConfigPath handlerPrefix = do+-- | Applies given builder to parsed logger config and initializes logging.+buildAndSetupYamlLogging :: MonadIO m => LoggerConfig -> FilePath -> m ()+buildAndSetupYamlLogging configBuilder loggerConfigPath = do cfg@LoggerConfig{..} <- parseLoggerConfig loggerConfigPath+ let builtConfig = cfg <> configBuilder #if PatakDebugSkovorodaBARDAQ- liftIO $ BS.putStrLn $ encodePretty defConfig cfg+ liftIO $ BS.putStrLn $ encodePretty defConfig builtConfig #endif - traverseLoggerConfig loggerMapper lcRotation lcTree handlerPrefix+ setupLogging builtConfig -initLoggingFromYaml :: MonadIO m => FilePath -> Maybe FilePath -> m ()-initLoggingFromYaml = initLoggingFromYamlWithMapper id+-- | Initialize logger hierarchy from configuration file.+-- See this module description.+initLoggingFromYaml :: MonadIO m => FilePath -> m ()+initLoggingFromYaml = buildAndSetupYamlLogging mempty
src/System/Wlog/LoggerConfig.hs view
@@ -1,3 +1,7 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE TemplateHaskell #-}+ -- | -- Module : System.Wlog.LoggerConfig -- Copyright : (c) Serokell, 2016@@ -9,27 +13,58 @@ -- Logger configuration. module System.Wlog.LoggerConfig- ( LoggerConfig (..)- , LoggerTree (..)- , LoggerMap+ ( LoggerMap , RotationParameters (..)+ , fromScratch , isValidRotation++ -- * Hierarchical tree of loggers (with lenses)+ , LoggerTree (..)+ , ltFile+ , ltSeverity+ , ltSubloggers++ -- * Global logger configuration+ , LoggerConfig (..)++ -- ** Lenses+ , lcConsoleOutput+ , lcFilePrefix+ , lcMapper+ , lcRotation+ , lcShowTime+ , lcTermSeverity+ , lcTree+ , zoomLogger++ -- ** Builders for 'LoggerConfig'+ , consoleOutB+ , mapperB+ , prefixB+ , productionB+ , showTimeB ) where -import Data.Aeson (withObject)-import Data.Default (Default (def))-import Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as HM hiding (HashMap)-import Data.Text (Text)-import qualified Data.Text.Buildable as Buildable-import Data.Traversable (for)-import Data.Word (Word64)-import Data.Yaml (FromJSON (..), ToJSON, Value (Object), (.:), (.:?))-import Formatting (bprint, shown)-import GHC.Generics (Generic)+import Universum -import System.Wlog.Wrapper (Severity)+import Control.Lens (at, makeLenses, zoom, _Just)+import Control.Monad.State (put)+import Data.Aeson (withObject)+import qualified Data.HashMap.Strict as HM hiding (HashMap)+import Data.List (notElem)+import Data.Monoid (Any (..))+import Data.Text (Text)+import qualified Data.Text.Buildable as Buildable+import Data.Traversable (for)+import Data.Word (Word64)+import Data.Yaml (FromJSON (..), ToJSON (..), Value (Object),+ object, (.!=), (.:), (.:?), (.=))+import Formatting (bprint, shown)+import GHC.Generics (Generic) +import System.Wlog.LoggerName (LoggerName)+import System.Wlog.Wrapper (Severity)+ ---------------------------------------------------------------------------- -- Utilites & helpers ----------------------------------------------------------------------------@@ -37,6 +72,10 @@ filterObject :: [Text] -> HashMap Text a -> HashMap Text a filterObject excluded = HM.filterWithKey $ \k _ -> k `notElem` excluded +-- | Useful lens combinator to be used for logging initialization.+fromScratch :: Monoid m => State m a -> m+fromScratch = executingState mempty+ ---------------------------------------------------------------------------- -- LoggerTree ----------------------------------------------------------------------------@@ -45,30 +84,44 @@ -- | Stores configuration for hierarchical loggers. data LoggerTree = LoggerTree- { ltSubloggers :: !LoggerMap- , ltFile :: !(Maybe FilePath)- , ltSeverity :: !(Maybe Severity)+ { _ltSubloggers :: !LoggerMap+ , _ltFile :: !(Maybe FilePath)+ , _ltSeverity :: !(Maybe Severity) } deriving (Generic, Show) -instance ToJSON LoggerTree+makeLenses ''LoggerTree -instance Default LoggerTree where- def = LoggerTree- { ltFile = Nothing- , ltSeverity = Nothing- , ltSubloggers = mempty- }+-- TODO: QuickCheck tests on monoid laws+instance Monoid LoggerTree where+ mempty = LoggerTree+ { _ltFile = Nothing+ , _ltSeverity = Nothing+ , _ltSubloggers = mempty+ } + lt1 `mappend` lt2 = LoggerTree+ { _ltFile = _ltFile lt1 <|> _ltFile lt2+ , _ltSeverity = _ltSeverity lt1 <|> _ltSeverity lt2+ , _ltSubloggers = _ltSubloggers lt1 <> _ltSubloggers lt2+ }+ nonLoggers :: [Text] nonLoggers = ["file", "severity"] +instance ToJSON LoggerTree instance FromJSON LoggerTree where parseJSON = withObject "loggers tree" $ \o -> do- ltFile <- o .:? "file"- ltSeverity <- o .:? "severity"- ltSubloggers <- for (filterObject nonLoggers o) parseJSON+ _ltFile <- o .:? "file"+ _ltSeverity <- o .:? "severity"+ _ltSubloggers <- for (filterObject nonLoggers o) parseJSON return LoggerTree{..} +-- | Zooming into logger name with putting specific key.+zoomLogger :: Text -> State LoggerTree () -> State LoggerTree ()+zoomLogger loggerName initializer = zoom (ltSubloggers.at loggerName) $ do+ put $ Just mempty+ zoom _Just initializer+ ---------------------------------------------------------------------------- -- Logger rotattion ----------------------------------------------------------------------------@@ -90,6 +143,7 @@ rpKeepFiles <- o .: "keepFiles" return RotationParameters{..} +-- | Checks if logger rotation parameters are valid. isValidRotation :: RotationParameters -> Bool isValidRotation RotationParameters{..} = rpLogLimit > 0 && rpKeepFiles > 0 @@ -99,20 +153,97 @@ -- | Logger configuration which keeps 'RotationParameters' and 'LoggerTree'. data LoggerConfig = LoggerConfig- { lcRotation :: !(Maybe RotationParameters)- , lcTree :: !LoggerTree- } deriving (Generic, Show)+ { -- | Rotation parameters for logger config. See 'System.Wlog.Roller'.+ _lcRotation :: Maybe RotationParameters -instance Default LoggerConfig where- def = LoggerConfig- { lcRotation = Nothing- , lcTree = def- }+ -- | Severity for terminal output. If @Nothing@ then 'Warning' is used.+ , _lcTermSeverity :: Maybe Severity + -- | Show time for non-error messages.+ -- Note that error messages always have timestamp.+ , _lcShowTime :: Any++ -- | @True@ if we should also print output into console.+ , _lcConsoleOutput :: Any++ -- | Defines how to transform logger names in config.+ , _lcMapper :: Endo LoggerName++ -- | Path prefix to add for each logger file+ , _lcFilePrefix :: Maybe FilePath++ -- | Hierarchical tree of loggers.+ , _lcTree :: LoggerTree+ }++makeLenses ''LoggerConfig++-- TODO: QuickCheck tests on monoid laws+instance Monoid LoggerConfig where+ mempty = LoggerConfig+ { _lcRotation = Nothing+ , _lcTermSeverity = Nothing+ , _lcShowTime = mempty+ , _lcConsoleOutput = mempty+ , _lcMapper = mempty+ , _lcFilePrefix = mempty+ , _lcTree = mempty+ }++ lc1 `mappend` lc2 = LoggerConfig+ { _lcRotation = _lcRotation lc1 <|> _lcRotation lc2+ , _lcTermSeverity = _lcTermSeverity lc1 <|> _lcTermSeverity lc2+ , _lcShowTime = _lcShowTime lc1 <> _lcShowTime lc2+ , _lcConsoleOutput = _lcConsoleOutput lc1 <> _lcConsoleOutput lc2+ , _lcMapper = _lcMapper lc1 <> _lcMapper lc2+ , _lcFilePrefix = _lcFilePrefix lc1 <|> _lcFilePrefix lc2+ , _lcTree = _lcTree lc1 <> _lcTree lc2+ }++topLevelParams :: [Text]+topLevelParams = ["rotation", "showTime", "printOutput", "filePrefix"]+ instance FromJSON LoggerConfig where parseJSON = withObject "rotation params" $ \o -> do- lcRotation <- o .:? "rotation"- lcTree <- parseJSON $ Object $ filterObject ["rotation"] o+ _lcRotation <- o .:? "rotation"+ _lcTermSeverity <- o .:? "termSeverity"+ _lcShowTime <- Any <$> o .:? "showTime" .!= False+ _lcConsoleOutput <- Any <$> o .:? "printOutput" .!= False+ _lcFilePrefix <- o .:? "filePrefix"+ _lcTree <- parseJSON $ Object $ filterObject topLevelParams o+ let _lcMapper = mempty return LoggerConfig{..} -instance ToJSON LoggerConfig+-- | This instances violates @fromJSON . toJSON = identity@ rule but doesn't matter+-- because it is used only for debugging.+instance ToJSON LoggerConfig where+ toJSON LoggerConfig{..} = object+ [ "rotation" .= _lcRotation+ , "termSeverity" .= _lcTermSeverity+ , "showTime" .= getAny _lcShowTime+ , "printOutput" .= getAny _lcConsoleOutput+ , "filePrefix" .= _lcFilePrefix+ , ("logTree", toJSON _lcTree)+ ]++-- Builders for 'LoggerConfig'.++-- | Setup 'lcShowTime' inside 'LoggerConfig'.+showTimeB :: Bool -> LoggerConfig+showTimeB isShowTime = mempty { _lcShowTime = Any isShowTime }++-- | Setup 'lcConsoleOutput' inside 'LoggerConfig'.+consoleOutB :: Bool -> LoggerConfig+consoleOutB printToConsole = mempty { _lcConsoleOutput = Any printToConsole }++-- | Adds sensible predefined set of parameters to logger.+productionB :: LoggerConfig+productionB = showTimeB True <> consoleOutB True++-- | Setup 'lcMapper' inside 'LoggerConfig'.+mapperB :: (LoggerName -> LoggerName) -> LoggerConfig+mapperB loggerNameMapper = mempty { _lcMapper = Endo loggerNameMapper }++-- | Setup 'lcFilePrefix' inside 'LoggerConfig'.+prefixB :: FilePath -> LoggerConfig+prefixB filePrefix = mempty { _lcFilePrefix = Just filePrefix }
src/System/Wlog/Wrapper.hs view
@@ -1,3 +1,6 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE ViewPatterns #-}+ -- | -- Module : System.Wlog.Wrapper -- Copyright : (c) Serokell, 2016@@ -11,19 +14,17 @@ -- which allows to keep logger name in monadic context. module System.Wlog.Wrapper- ( LoggingFormat (..)- , Severity (..)+ ( Severity (..) , convertSeverity- , initLogging- , initLoggingWith+ , initTerminalLogging , releaseAllHandlers , setSeverity , setSeverityMaybe ) where +import Universum+ import Control.Concurrent.MVar (MVar, newMVar, withMVar)-import Control.Monad.Trans (MonadIO (liftIO))-import Data.Default (Default (def)) import System.IO (Handle, stderr, stdout) import System.Log.Handler.Simple (GenericHandler (..), streamHandler) import System.Log.Logger (Priority (DEBUG, ERROR), clearLevel,@@ -34,17 +35,6 @@ import System.Wlog.LoggerName (LoggerName (..)) import System.Wlog.Severity (Severity (..), convertSeverity) ---- | Options determining formatting of messages.-data LoggingFormat = LoggingFormat- { -- | Show time for non-error messages.- -- Note that error messages always have timestamp.- lfShowTime :: !Bool- } deriving (Show)--instance Default LoggingFormat where- def = LoggingFormat {lfShowTime = True}- -- | Like `streamHandler`, but syncronized using given `MVar` as lock -- (it should be filled before this function call). streamHandlerWithLock :: MVar () -> Handle -> Priority -> IO (GenericHandler Handle)@@ -58,10 +48,13 @@ , closeFunc = closeFunc } --- | This function initializes global logging system. At high level, it sets--- severity which will be used by all loggers by default, sets default--- formatters and sets custom severity for given loggers (if any).+-- | This function initializes global logging system for terminal output.+-- At high level, it sets severity which will be used by all loggers by default,+-- sets default formatters and sets custom severity for given loggers (if any). --+-- NOTE: you probably don't want to use this function.+-- Consider 'System.Wlog.Launcher.setupLogging'.+-- -- On a lower level it does the following: -- 1. Removes default handler from root logger, sets two handlers such that: -- 1.1. All messages are printed to /stdout/.@@ -71,14 +64,12 @@ -- descendant loggers by default. -- 3. Applies `setSeverity` to given loggers. It can be done later using -- `setSeverity` directly.-initLoggingWith- :: MonadIO m- => LoggingFormat -> Severity -> m ()-initLoggingWith LoggingFormat {..} defaultSeverity = liftIO $ do+initTerminalLogging :: MonadIO m => Bool -> Maybe Severity -> m ()+initTerminalLogging isShowTime (fromMaybe Warning -> defaultSeverity) = liftIO $ do lock <- liftIO $ newMVar () -- We set DEBUG here, to allow all messages by stdout handler. -- They will be filtered by loggers.- stdoutHandler <- setStdoutFormatter lfShowTime <$>+ stdoutHandler <- setStdoutFormatter isShowTime <$> streamHandlerWithLock lock stdout DEBUG stderrHandler <- setStderrFormatter <$> streamHandlerWithLock lock stderr ERROR@@ -86,10 +77,6 @@ setHandlers [stderrHandler, stdoutHandler] updateGlobalLogger rootLoggerName $ setLevel (convertSeverity defaultSeverity)---- | Version of initLoggingWith without any predefined loggers.-initLogging :: MonadIO m => Severity -> m ()-initLogging = initLoggingWith def -- | Set severity for given logger. By default parent's severity is used. setSeverity :: MonadIO m => LoggerName -> Severity -> m ()
test/Test/Wlog/RollingSpec.hs view
@@ -7,8 +7,7 @@ import Universum import Control.Concurrent.Async (mapConcurrently)-import Data.Default (def)-import qualified Data.HashMap.Strict as HM (fromList)+import Control.Lens (zoom, (.=), (?=)) import qualified Prelude (read) import System.Directory (doesFileExist, removeFile) import System.FilePath (takeExtension)@@ -21,11 +20,12 @@ import Test.QuickCheck.Monadic (PropertyM, monadicIO, run) import System.Wlog (InvalidRotation (..), LoggerConfig (..),- LoggerTree (..), RotationParameters (..),- Severity (..), isValidRotation, logDebug,- logIndex, releaseAllHandlers,- rotationFileHandler, traverseLoggerConfig,- usingLoggerName, whenExist)+ RotationParameters (..), Severity (..),+ fromScratch, isValidRotation, lcFilePrefix,+ lcRotation, lcTree, logDebug, logIndex, ltFile,+ ltSeverity, releaseAllHandlers,+ rotationFileHandler, setupLogging,+ usingLoggerName, whenExist, zoomLogger) spec :: Spec spec = do@@ -67,11 +67,13 @@ testLogFile = "patak.log" testLoggerConfig :: RotationParameters -> LoggerConfig-testLoggerConfig (Just -> lcRotation) = LoggerConfig{..}- where- lcTree = def { ltSeverity = Just Debug- , ltSubloggers = HM.fromList [("test", def { ltFile = Just testLogFile })]- }+testLoggerConfig (Just -> rotParam ) = fromScratch $ do+ lcRotation .= rotParam+ lcFilePrefix ?= "logs"+ zoom lcTree $ do+ ltSeverity ?= Debug+ zoomLogger "test" $+ ltFile ?= testLogFile logThreadsNum :: Word logThreadsNum = 4@@ -80,7 +82,7 @@ writeConcurrentLogs :: RotationParameters -> LinesToLog -> IO () writeConcurrentLogs rp@RotationParameters{..} (getNumberOfLinesToLog -> linesNum) = bracket_- (traverseLoggerConfig identity lcRotation lcTree $ Just "logs")+ (setupLogging $ testLoggerConfig rp) releaseAllHandlers concurrentWriting where@@ -96,6 +98,7 @@ -- * it works when multiple threads trying to write to log file -- * size of each file is not very big -- * number of files is not bigger than allowed+-- * at least one (root) file with logs is created -- TODO: more properties? verifyLoggerRotation :: RotationParameters -> LinesToLog -> Property verifyLoggerRotation rp@RotationParameters{..} linesNum = isValidRotation rp ==> monadicIO $ do