log-warper 1.7.1 → 1.7.2
raw patch · 8 files changed
+497/−30 lines, 8 filesdep +markdown-unlitdep ~textnew-component:exe:how-to
Dependencies added: markdown-unlit
Dependency ranges changed: text
Files
- CHANGES.md +10/−0
- examples/HowTo.lhs +190/−0
- examples/HowTo.md +190/−0
- log-warper.cabal +19/−3
- src/System/Wlog.hs +16/−14
- src/System/Wlog/Exception.hs +52/−0
- src/System/Wlog/Formatter.hs +15/−10
- src/System/Wlog/LoggerConfig.hs +5/−3
CHANGES.md view
@@ -1,3 +1,13 @@+1.7.2+=====++* [#57](https://github.com/serokell/log-warper/issues/57):+ Add `Exception` module with `logException` and `catchLog` functions.+* [#60](https://github.com/serokell/log-warper/issues/60):+ Fix documentation for `termSeveritiesOut` and `termSeveritiesErr`.+* [#63](https://github.com/serokell/log-warper/issues/63):+ Timestamp rounding by powers of 10.+ 1.7.1 =====
+ examples/HowTo.lhs view
@@ -0,0 +1,190 @@+# `log-warper` how-to++If you want to see the working example first, please, check out this+[very simple example](https://github.com/serokell/log-warper/blob/master/examples/Playground.hs).++Here we will try to explain step by step how to integrate `log-warper` into our very small project,+specifically how to replace `putStrLn` with simple naive logging.++All code below can be compiled and run with these commands++```bash+stack build log-warper+stack exec how-to+```+and you can see how it's working both with and without logging.++## Preamble: imports and language extensions++Since this is a literate haskell file, we need to specify all our language extensions+and imports up front.++``` haskell+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where+++import Control.Monad.IO.Class (MonadIO, liftIO)+import Data.Semigroup ((<>))+import Data.Text (Text)+import System.Wlog (WithLoggerIO, buildAndSetupYamlLogging, logError,+ logInfo, productionB, usingLoggerName)++import qualified Data.Text as Text+import qualified Data.Text.IO as TextIO++```++## Adding a new logger++First of all, you may ask what are we doing? What the hell is logger?++**Logger** is the [output handler](https://hackage.haskell.org/package/log-warper-1.7.1/docs/System-Wlog-LogHandler.html#t:LogHandler)+labeled by the unique [logger name](https://hackage.haskell.org/package/log-warper-1.7.1/docs/System-Wlog-LoggerName.html#t:LoggerName).++When you log new messages you need to specify logger which is responsible for logging this message.+Fortunately, log-warper has monadic interfaces and logger will be taken explicitly from current context+if possible, thus you need to specify logger only once for some block of actions.+Don't worry, this will be covered later!++So, if you're adding a new logger for your needs you'll have to consider several things.+Firstly, you should choose some reasonable name for the logger you are creating+according to the actions that it would cover with logging. From now on let's assume+that you need to have the logger with the name `new-logger` in your project.++### Configuration file++There are two ways of configuring the logger you creating. One option+is to configure everything directly in the code, which will make the process+of writing code less convenient. Here we will use another configuration possibility+— we will write configurations in special `.yaml` file.+In our example we will use very simple one with the minimal settings needed for the file to work.+You can have a look at [`how-to-log-config.yaml`](https://github.com/serokell/log-warper/blob/master/examples/how-to-log-config.yaml),+it contains the following lines:++```yaml+loggerTree:+ severity: Warning+ # severities for «root» logger+ new-logger: # logger named «new-logger»+ severity: Debug+ # severities for logger «new-logger»+```+You don't need to worry about root logger for now. This part is explained in section devoted to hierarchical logging.++For full information about all configuration file's features and options please see [this section](#link-to-readme-config).++### Setting up++Before using your logging subsystem you need to set it up. To do that there are functions in `log-warper` library.+In our example we will run all functions with our logger from `main` function of `Main` module.++Let's consider that this is what we have:++```haskell+mainWithoutLogging :: IO ()+mainWithoutLogging = inputLength+```+ where `inputLength` is the function where we will add some logging messages.++So to make it possible we should transform this function to the following:++```haskell+mainWithLogging :: IO ()+mainWithLogging = do+ buildAndSetupYamlLogging productionB "examples/how-to-log-config.yaml"+ usingLoggerName "new-logger" inputLengthWithLog+```+where we set up the config from the file and run `inputLengthWithLog` under the logger with name `new-logger`.+Note that we use `inputLengthWithLog` now instead of `inputLength` because we can not just add logging+to the function that works under `IO` monad automatically. We need to change our faction.+And `inputLengthWithLog` is a version of `inputLength` with logging added.++Let's have a closer look at how we transform the `inputLength` function+to the `inputLengthWithLog` that would work with logging in the next paragraph.++## Add logging messages to function in `IO`++If you want to use logging in a function that already runs in the `IO`,+replace concrete `IO` type with polymorphic constraint+[`WithLoggerIO`](https://hackage.haskell.org/package/log-warper-1.7.1/docs/System-Wlog-CanLog.html#t:WithLoggerIO)` m => m`+and add [`liftIO`](https://www.stackage.org/haddock/lts-9.14/base-4.9.1.0/Control-Monad-IO-Class.html#v:liftIO) all your `IO` actions.++For example, our `inputLength` function works this way:+```haskell++inputLength :: IO ()+inputLength = do+ input <- TextIO.getLine+ case Text.length input of+ 0 -> do+ putStrLn "Should not be empty"+ inputLength+ -- v ^ you need to log these+ _ -> do+ putStrLn "Well done"+ funWithInput input++```++_**Note:**_ All logging functions take [`Text`](https://hackage.haskell.org/package/text-1.2.2.2/docs/Data-Text.html#t:Text)+messages instead of `String`. So users might convert their `String`s to `Text`s with+[`Text.pack`](https://hackage.haskell.org/package/text-1.2.2.2/docs/Data-Text.html#v:pack)+or migrate to the `text` package or use some custom prelude which makes the work with `Text` more+convenient, e.g. [`universum`](https://hackage.haskell.org/package/universum).++Okay, it's very easy function. We have some input from console and write some messages depending of the length of user's input.+What we are going to do is to log these messages instead of just `putStrLn` them.++First that we should consider is that logging can not be done in `IO`, so we need make+the function work in some polymorphic monad, which implements `WithLoggerIO` constraint,+so that we are able to do any logging stuff inside.+After this is done you can easily use `logDebug`, `logInfo`, `logWarning` etc.+(see others [here](https://hackage.haskell.org/package/log-warper-1.7.1/docs/System-Wlog-CanLog.html#v:logDebug)) to log messages.+As it's not `IO` after our transformations, we shouldn't forget to `liftIO` functions that work in `IO` and doesn't have logging inside.+As result we will get++```haskell++inputLengthWithLog :: WithLoggerIO m => m ()+inputLengthWithLog = do+ input <- liftIO TextIO.getLine+ case Text.length input of+ 0 -> do+ -- `putStrLn` is replaced with `logError` now.+ logError "Should not be empty"+ inputLengthWithLog+ _ -> do+ -- `putStrLn` is replaced with `logInfo` now.+ logInfo "Well done"+ funWithInputInMonad input++```++Another moment, let's have a closer look at the `funWithInput` function from the example above.+We see that its type is `IO ()` and it doesn't have logging inside.++```haskell+funWithInput :: Text -> IO ()+funWithInput input = do+ TextIO.putStrLn $ "Wow! You wrote " <> input++```+So we also can rewrite it in this way+```haskell+funWithInputInMonad :: MonadIO m => Text -> m ()+funWithInputInMonad input =+ liftIO $ TextIO.putStrLn $ "Wow! You wrote " <> input++```+or usage of `liftIO` is also the solution.++And as a conclusion, let's write the `main` function to check how we managed to apply logging in very simple example+```haskell+main :: IO ()+main = do+ mainWithoutLogging+ mainWithLogging+```++You can see an example of the work of the programm we've just wrote:+
+ examples/HowTo.md view
@@ -0,0 +1,190 @@+# `log-warper` how-to++If you want to see the working example first, please, check out this+[very simple example](https://github.com/serokell/log-warper/blob/master/examples/Playground.hs).++Here we will try to explain step by step how to integrate `log-warper` into our very small project,+specifically how to replace `putStrLn` with simple naive logging.++All code below can be compiled and run with these commands++```bash+stack build log-warper+stack exec how-to+```+and you can see how it's working both with and without logging.++## Preamble: imports and language extensions++Since this is a literate haskell file, we need to specify all our language extensions+and imports up front.++``` haskell+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where+++import Control.Monad.IO.Class (MonadIO, liftIO)+import Data.Semigroup ((<>))+import Data.Text (Text)+import System.Wlog (WithLoggerIO, buildAndSetupYamlLogging, logError,+ logInfo, productionB, usingLoggerName)++import qualified Data.Text as Text+import qualified Data.Text.IO as TextIO++```++## Adding a new logger++First of all, you may ask what are we doing? What the hell is logger?++**Logger** is the [output handler](https://hackage.haskell.org/package/log-warper-1.7.1/docs/System-Wlog-LogHandler.html#t:LogHandler)+labeled by the unique [logger name](https://hackage.haskell.org/package/log-warper-1.7.1/docs/System-Wlog-LoggerName.html#t:LoggerName).++When you log new messages you need to specify logger which is responsible for logging this message.+Fortunately, log-warper has monadic interfaces and logger will be taken explicitly from current context+if possible, thus you need to specify logger only once for some block of actions.+Don't worry, this will be covered later!++So, if you're adding a new logger for your needs you'll have to consider several things.+Firstly, you should choose some reasonable name for the logger you are creating+according to the actions that it would cover with logging. From now on let's assume+that you need to have the logger with the name `new-logger` in your project.++### Configuration file++There are two ways of configuring the logger you creating. One option+is to configure everything directly in the code, which will make the process+of writing code less convenient. Here we will use another configuration possibility+— we will write configurations in special `.yaml` file.+In our example we will use very simple one with the minimal settings needed for the file to work.+You can have a look at [`how-to-log-config.yaml`](https://github.com/serokell/log-warper/blob/master/examples/how-to-log-config.yaml),+it contains the following lines:++```yaml+loggerTree:+ severity: Warning+ # severities for «root» logger+ new-logger: # logger named «new-logger»+ severity: Debug+ # severities for logger «new-logger»+```+You don't need to worry about root logger for now. This part is explained in section devoted to hierarchical logging.++For full information about all configuration file's features and options please see [this section](#link-to-readme-config).++### Setting up++Before using your logging subsystem you need to set it up. To do that there are functions in `log-warper` library.+In our example we will run all functions with our logger from `main` function of `Main` module.++Let's consider that this is what we have:++```haskell+mainWithoutLogging :: IO ()+mainWithoutLogging = inputLength+```+ where `inputLength` is the function where we will add some logging messages.++So to make it possible we should transform this function to the following:++```haskell+mainWithLogging :: IO ()+mainWithLogging = do+ buildAndSetupYamlLogging productionB "examples/how-to-log-config.yaml"+ usingLoggerName "new-logger" inputLengthWithLog+```+where we set up the config from the file and run `inputLengthWithLog` under the logger with name `new-logger`.+Note that we use `inputLengthWithLog` now instead of `inputLength` because we can not just add logging+to the function that works under `IO` monad automatically. We need to change our faction.+And `inputLengthWithLog` is a version of `inputLength` with logging added.++Let's have a closer look at how we transform the `inputLength` function+to the `inputLengthWithLog` that would work with logging in the next paragraph.++## Add logging messages to function in `IO`++If you want to use logging in a function that already runs in the `IO`,+replace concrete `IO` type with polymorphic constraint+[`WithLoggerIO`](https://hackage.haskell.org/package/log-warper-1.7.1/docs/System-Wlog-CanLog.html#t:WithLoggerIO)` m => m`+and add [`liftIO`](https://www.stackage.org/haddock/lts-9.14/base-4.9.1.0/Control-Monad-IO-Class.html#v:liftIO) all your `IO` actions.++For example, our `inputLength` function works this way:+```haskell++inputLength :: IO ()+inputLength = do+ input <- TextIO.getLine+ case Text.length input of+ 0 -> do+ putStrLn "Should not be empty"+ inputLength+ -- v ^ you need to log these+ _ -> do+ putStrLn "Well done"+ funWithInput input++```++_**Note:**_ All logging functions take [`Text`](https://hackage.haskell.org/package/text-1.2.2.2/docs/Data-Text.html#t:Text)+messages instead of `String`. So users might convert their `String`s to `Text`s with+[`Text.pack`](https://hackage.haskell.org/package/text-1.2.2.2/docs/Data-Text.html#v:pack)+or migrate to the `text` package or use some custom prelude which makes the work with `Text` more+convenient, e.g. [`universum`](https://hackage.haskell.org/package/universum).++Okay, it's very easy function. We have some input from console and write some messages depending of the length of user's input.+What we are going to do is to log these messages instead of just `putStrLn` them.++First that we should consider is that logging can not be done in `IO`, so we need make+the function work in some polymorphic monad, which implements `WithLoggerIO` constraint,+so that we are able to do any logging stuff inside.+After this is done you can easily use `logDebug`, `logInfo`, `logWarning` etc.+(see others [here](https://hackage.haskell.org/package/log-warper-1.7.1/docs/System-Wlog-CanLog.html#v:logDebug)) to log messages.+As it's not `IO` after our transformations, we shouldn't forget to `liftIO` functions that work in `IO` and doesn't have logging inside.+As result we will get++```haskell++inputLengthWithLog :: WithLoggerIO m => m ()+inputLengthWithLog = do+ input <- liftIO TextIO.getLine+ case Text.length input of+ 0 -> do+ -- `putStrLn` is replaced with `logError` now.+ logError "Should not be empty"+ inputLengthWithLog+ _ -> do+ -- `putStrLn` is replaced with `logInfo` now.+ logInfo "Well done"+ funWithInputInMonad input++```++Another moment, let's have a closer look at the `funWithInput` function from the example above.+We see that its type is `IO ()` and it doesn't have logging inside.++```haskell+funWithInput :: Text -> IO ()+funWithInput input = do+ TextIO.putStrLn $ "Wow! You wrote " <> input++```+So we also can rewrite it in this way+```haskell+funWithInputInMonad :: MonadIO m => Text -> m ()+funWithInputInMonad input =+ liftIO $ TextIO.putStrLn $ "Wow! You wrote " <> input++```+or usage of `liftIO` is also the solution.++And as a conclusion, let's write the `main` function to check how we managed to apply logging in very simple example+```haskell+main :: IO ()+main = do+ mainWithoutLogging+ mainWithLogging+```++You can see an example of the work of the programm we've just wrote:+
log-warper.cabal view
@@ -1,5 +1,5 @@ name: log-warper-version: 1.7.1+version: 1.7.2 synopsis: Flexible, configurable, monadic and pretty logging homepage: https://github.com/serokell/log-warper license: MIT@@ -8,7 +8,7 @@ maintainer: Serokell <hi@serokell.io> copyright: 2016-2017 Serokell category: Logging-extra-source-files: CHANGES.md+extra-source-files: CHANGES.md, examples/HowTo.md, examples/HowTo.lhs stability: experimental build-type: Simple cabal-version: >=1.18@@ -18,6 +18,7 @@ library exposed-modules: System.Wlog System.Wlog.CanLog+ System.Wlog.Exception System.Wlog.FileUtils System.Wlog.Formatter System.Wlog.HasLoggerName@@ -94,7 +95,8 @@ hs-source-dirs: examples default-language: Haskell2010- ghc-options: -threaded -Wall -fno-warn-orphans+ ghc-options: -threaded -Wall+ -fno-warn-orphans default-extensions: GeneralizedNewtypeDeriving DeriveDataTypeable@@ -103,6 +105,20 @@ OverloadedStrings TypeApplications RecordWildCards++executable how-to+ main-is: HowTo.lhs++ build-depends: base >= 4.7 && < 5+ , log-warper+ , markdown-unlit >= 0.4.0+ , text >= 1.2.2.1++ hs-source-dirs: examples+ default-language: Haskell2010+ ghc-options: -threaded -Wall+ -fno-warn-orphans+ -pgmL markdown-unlit test-suite log-test type: exitcode-stdio-1.0
src/System/Wlog.hs view
@@ -13,6 +13,7 @@ module System.Wlog ( module System.Wlog.CanLog+ , module System.Wlog.Exception , module System.Wlog.FileUtils , module System.Wlog.HasLoggerName , module System.Wlog.IOLogger@@ -28,17 +29,18 @@ , module System.Wlog.Terminal ) where -import System.Wlog.CanLog-import System.Wlog.FileUtils-import System.Wlog.HasLoggerName-import System.Wlog.IOLogger-import System.Wlog.Launcher-import System.Wlog.LoggerConfig-import System.Wlog.LoggerName-import System.Wlog.LoggerNameBox-import System.Wlog.LogHandler.Roller-import System.Wlog.LogHandler.Simple-import System.Wlog.LogHandler.Syslog-import System.Wlog.PureLogging-import System.Wlog.Severity-import System.Wlog.Terminal+import System.Wlog.CanLog+import System.Wlog.Exception+import System.Wlog.FileUtils+import System.Wlog.HasLoggerName+import System.Wlog.IOLogger+import System.Wlog.Launcher+import System.Wlog.LoggerConfig+import System.Wlog.LoggerName+import System.Wlog.LoggerNameBox+import System.Wlog.LogHandler.Roller+import System.Wlog.LogHandler.Simple+import System.Wlog.LogHandler.Syslog+import System.Wlog.PureLogging+import System.Wlog.Severity+import System.Wlog.Terminal
+ src/System/Wlog/Exception.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ExplicitForAll #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | This module deals with Exception logging.++module System.Wlog.Exception+ ( logException+ , catchLog+ ) where++import Universum++import Control.Exception.Base (Exception)++import System.Wlog.CanLog (WithLogger, WithLoggerIO, logError)++-- | Logs exception's description with ''System.Wlog.Severity.Error' 'System.Wlog.Severity.Severity'+logException :: forall e m . (WithLogger m, Exception e) => e -> m ()+logException = logError . show++{- | Runs the action, if an exception is raised the 'logException' is executed.++==== __Example__+Here is very simple example of usage 'catchLog' on IO functions:++@+main :: IO ()+main = do+ buildAndSetupYamlLogging productionB "log-config.yaml"+ usingLoggerName "new-logger" runWithExceptionLog++runWithExceptionLog :: (WithLoggerIO m, MonadCatch m) => m ()+runWithExceptionLog = catchLog @IOException (liftIO simpleIOfun)++simpleIOfun :: IO ()+simpleIOfun = getLine >>= readFile >>= putStrLn++@++and when run you will get:++>>> run-main-from-this-example+> not-existing-filename.txt+[new-logger:ERROR] [2017-12-01 13:07:33.21 UTC] asd: openFile: does not exist (No such file or directory)++-}+catchLog :: forall e m . (WithLoggerIO m, MonadCatch m, Exception e) => m () -> m ()+catchLog a = a `catch` logE+ where+ logE :: e -> m ()+ logE = logException
src/System/Wlog/Formatter.hs view
@@ -33,7 +33,7 @@ import Data.Monoid (mconcat) import Data.Text.Lazy.Builder as B import Data.Time (formatTime, getCurrentTime, getZonedTime)-import Data.Time.Clock (UTCTime (..))+import Data.Time.Clock (UTCTime (..), diffTimeToPicoseconds, picosecondsToDiffTime) import Data.Time.Format (FormatTime) import Fmt (fmt, padRightF, (+|), (|+), (|++|)) import Fmt.Time (dateDashF, hmsF, subsecondF, tzNameF)@@ -169,14 +169,19 @@ where centiSecondF = padRightF 3 '0' . T.take 3 . fmt . subsecondF +-- | Get the current time rounded to 10^n picoseconds.+-- n = 3 rounds to the nearest nanosecond.+-- n = 6 rounds to the nearest microsecond.+-- n = 9 will round to the nearest millisecond.+-- n = 12 will round to the nearest second. getRoundedTime :: Int -> IO UTCTime-getRoundedTime roundN = do- UTCTime{..} <- liftIO $ getCurrentTime- let newSec = fromIntegral $ roundBy (round $ toRational utctDayTime :: Int)- pure $ UTCTime { utctDayTime = newSec, .. }- where- roundBy :: (Num a, Integral a) => a -> a- roundBy x = let y = x `div` fromIntegral roundN in y * fromIntegral roundN+getRoundedTime n = do+ UTCTime{..} <- getCurrentTime+ let m = if n < 0 then 0 else (fromIntegral n :: Integer)+ multiplier = 10 ^ m+ picoseconds = diffTimeToPicoseconds utctDayTime+ roundedPicoseconds = (picoseconds `div` multiplier) * multiplier+ pure $ UTCTime { utctDayTime = picosecondsToDiffTime roundedPicoseconds, .. } stdoutFormatter :: (UTCTime -> Text) -> Bool -> Bool -> LogFormatter a stdoutFormatter timeF isShowTime isShowTid handle record message = do@@ -184,8 +189,8 @@ createLogFormatter isShowTime isShowTid timeF time handle record message stdoutFormatterTimeRounded :: (UTCTime -> Text) -> Int -> LogFormatter a-stdoutFormatterTimeRounded timeF roundN handle record message = do- time <- getRoundedTime roundN+stdoutFormatterTimeRounded timeF n handle record message = do+ time <- getRoundedTime n createLogFormatter True True timeF time handle record message createLogFormatter
src/System/Wlog/LoggerConfig.hs view
@@ -114,7 +114,9 @@ { _hwFilePath :: !FilePath -- ^ Path to the file to be handled. , _hwRounding :: !(Maybe Int)- -- ^ Amount of seconds to round time on (if set).+ -- ^ Round timestamps to this power of 10 picoseconds.+ -- Just 3 would round to nanoseconds.+ -- Just 12 would round to seconds. } deriving (Generic,Show) makeLenses ''HandlerWrap@@ -306,11 +308,11 @@ -- Builders for 'LoggerConfig'. ---------------------------------------------------------------------------- --- | Setup 'lcTermSeverity' to specified severity inside 'LoggerConfig'.+-- | Setup 'lcTermSeverityOut' to specified severity inside 'LoggerConfig'. termSeveritiesOutB :: Severities -> LoggerConfig termSeveritiesOutB severities = mempty { _lcTermSeverityOut = Just severities } --- | Setup 'lcTermSeverity' to specified severity inside 'LoggerConfig'.+-- | Setup 'lcTermSeverityErr' to specified severity inside 'LoggerConfig'. termSeveritiesErrB :: Severities -> LoggerConfig termSeveritiesErrB severities = mempty { _lcTermSeverityErr = Just severities }