packages feed

log-base (empty) → 0.7

raw patch · 12 files changed

+820/−0 lines, 12 filesdep +aesondep +aeson-prettydep +basesetup-changed

Dependencies added: aeson, aeson-pretty, base, bytestring, deepseq, exceptions, monad-control, monad-time, mtl, semigroups, stm, text, time, transformers-base, unordered-containers

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2015, Scrive AB++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Andrzej Rybczak nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ log-base.cabal view
@@ -0,0 +1,72 @@+name:                log-base+version:             0.7+synopsis:            Structured logging solution (base package)++description:         A library that provides a way to record structured log+                     messages. Use this package in conjunction with+                     'log-elasticsearch' or 'log-postgres', depending+                     on which back end you need. Use the 'log' library+                     if you need all back ends.+++homepage:            https://github.com/scrive/log+license:             BSD3+license-file:        LICENSE+author:              Scrive AB+maintainer:          Andrzej Rybczak <andrzej@rybczak.net>,+                     Jonathan Jouty <jonathan@scrive.com>,+                     Mikhail Glushenkov <mikhail@scrive.com>,+                     Oleg Grenrus <oleg.grenrus@iki.fi>+copyright:           Scrive AB+category:            System+build-type:          Simple+cabal-version:       >=1.10+tested-with:         GHC == 7.8.4, GHC == 7.10.3, GHC == 8.0.1++Source-repository head+  Type:     git+  Location: https://github.com/scrive/log.git++library+  exposed-modules:     Log,+                       Log.Backend.StandardOutput,+                       Log.Backend.StandardOutput.Bulk,+                       Log.Backend.Text,+                       Log.Class,+                       Log.Data,+                       Log.Internal.Logger,+                       Log.Logger,+                       Log.Monad+  build-depends:       base <5,+                       aeson >=0.11.0.0,+                       aeson-pretty >=0.8.2,+                       bytestring,+                       deepseq,+                       exceptions >=0.6,+                       monad-control >=0.3,+                       monad-time >= 0.2,+                       mtl,+                       semigroups,+                       stm >=2.4,+                       text,+                       time >= 1.5,+                       transformers-base,+                       unordered-containers+  hs-source-dirs:      src++  ghc-options:         -O2 -Wall -funbox-strict-fields++  default-language:   Haskell2010+  default-extensions: BangPatterns+                    , FlexibleContexts+                    , FlexibleInstances+                    , GeneralizedNewtypeDeriving+                    , LambdaCase+                    , MultiParamTypeClasses+                    , NoImplicitPrelude+                    , OverloadedStrings+                    , RankNTypes+                    , RecordWildCards+                    , ScopedTypeVariables+                    , TypeFamilies+                    , UndecidableInstances
+ src/Log.hs view
@@ -0,0 +1,45 @@+-- | Structured logging solution with multiple backends.+--+-- @+-- {-\# LANGUAGE OverloadedStrings #-}+--+-- module Main where+--+-- import Log+-- import Log.Backend.ElasticSearch+--+-- import System.Random+--+-- main :: IO ()+-- main = do+--   let config = 'Log.Backend.ElasticSearch.ElasticSearchConfig' {+--         esServer  = "http://localhost:9200",+--         esIndex   = "logs",+--         esMapping = "log"+--         }+--   'Log.Backend.ElasticSearch.withElasticSearchLogger' config randomIO $ \\logger -> do+--     'runLogT' "main" logger $ do+--       'logTrace_' "foo"+-- @+module Log (+    -- * Documentation+    -- | 'Log.Class.MonadLog' type class of monads with logging capabilities.+    module Log.Class+    -- | 'Log.Data.LogMessage' and 'Log.Data.LogLevel' data definitions.+  , module Log.Data+    -- | 'Log.Logger.Logger' objects used to perform logging operations in ''Log.Monad.LogT'.+  , module Log.Logger+    -- | 'Log.Monad.LogT' monad transformer that adds logging capabilities to+    -- the underlying monad.+  , module Log.Monad+   -- * Aeson re-exports+  , object+  , (.=)+  ) where++import Data.Aeson++import Log.Class+import Log.Data+import Log.Logger+import Log.Monad
+ src/Log/Backend/StandardOutput.hs view
@@ -0,0 +1,37 @@+-- | Stdout logging back-end.+module Log.Backend.StandardOutput (+    simpleStdoutLogger+  , stdoutLogger+  , withSimpleStdOutLogger+  ) where++import Prelude+import qualified Data.Text.IO as T+import System.IO++import Log.Data+import Log.Internal.Logger+import Log.Logger++-- | Create a 'simpleStdoutlogger' for the duration of the given+-- action, making sure that stdout is flushed afterwards.+withSimpleStdOutLogger :: (Logger -> IO r) -> IO r+withSimpleStdOutLogger act = do+  logger <- stdoutLogger+  withLogger logger act++{-# DEPRECATED simpleStdoutLogger "Use 'withSimpleStdOutLogger'" #-}++-- | Simple, synchronous logger that prints messages to standard output.+simpleStdoutLogger :: Logger+simpleStdoutLogger = Logger {+    loggerWriteMessage = T.putStrLn . showLogMessage Nothing+  , loggerWaitForWrite = hFlush stdout+  , loggerShutdown     = return ()+  }++{-# DEPRECATED stdoutLogger "Use 'withSimpleStdOutLogger'" #-}++-- | Create a logger that prints messages to standard output.+stdoutLogger :: IO Logger+stdoutLogger = mkLogger "stdout" $ T.putStrLn . showLogMessage Nothing
+ src/Log/Backend/StandardOutput/Bulk.hs view
@@ -0,0 +1,32 @@+-- | Bulk stdout logging back-end, useful mainly for testing.+module Log.Backend.StandardOutput.Bulk (+    withBulkStdOutLogger,+    bulkStdoutLogger+  ) where++import Prelude+import qualified Data.Text.IO as T++import Log.Data+import Log.Logger+import Log.Internal.Logger++-- | Create a 'bulkStdoutLogger' for the duration of the given action,+-- and shut it down afterwards, making sure that all buffered messages+-- are actually written to stdout.+withBulkStdOutLogger :: (Logger -> IO r) -> IO r+withBulkStdOutLogger act = do+  logger <- bulkStdoutLogger+  withLogger logger act++{-# DEPRECATED bulkStdoutLogger "Use 'withBulkStdOutLogger' instead!" #-}++-- | Start an asynchronous logger thread that prints messages to+-- standard output.+--+-- Please use 'withBulkStdOutLogger'' instead, which is more exception-safe+-- (see the note attached to 'mkBulkLogger').+bulkStdoutLogger :: IO Logger+bulkStdoutLogger = mkBulkLogger "stdout-bulk"+                   (mapM_ $ T.putStrLn . showLogMessage Nothing)+                   (return ())
+ src/Log/Backend/Text.hs view
@@ -0,0 +1,31 @@+-- | A logger that produces in-memory 'Text' values. Mainly useful for+-- testing.+module Log.Backend.Text ( withSimpleTextLogger ) where++import Control.Applicative+import Data.IORef+import Data.Monoid+import Prelude+import qualified Data.Text as T+import qualified Data.Text.Lazy as L+import qualified Data.Text.Lazy.Builder as B++import Log.Data+import Log.Internal.Logger++-- | Create an in-memory logger for the duration of the given action,+-- returning both the result of the action and the logger's output as+-- a 'Text' value afterwards.+withSimpleTextLogger :: (Logger -> IO r) -> IO (T.Text, r)+withSimpleTextLogger act = do+  builderRef <- newIORef mempty+  let logger = Logger+        { loggerWriteMessage = \msg -> do+            let msg' = B.fromText $ showLogMessage Nothing msg+            modifyIORef' builderRef (<> msg' <> B.fromText "\n")+        , loggerWaitForWrite = return ()+        , loggerShutdown     = return ()+        }+  r <- act logger+  txt <- L.toStrict . B.toLazyText <$> readIORef builderRef+  return (txt, r)
+ src/Log/Class.hs view
@@ -0,0 +1,93 @@+-- | The 'MonadLog' type class of monads with logging capabilities.+{-# OPTIONS_GHC -fno-warn-deprecated-flags #-}+{-# LANGUAGE OverlappingInstances #-}+module Log.Class (+    UTCTime+  , MonadTime(..)+  , MonadLog(..)+  , logAttention+  , logInfo+  , logTrace+  , logAttention_+  , logInfo_+  , logTrace_+  ) where++import Control.Monad.Time+import Control.Monad.Trans+import Control.Monad.Trans.Control+import Data.Aeson+import Data.Aeson.Types+import Data.Time+import Prelude+import qualified Data.Text as T++import Log.Data++-- | Represents the family of monads with logging capabilities. Each+-- 'MonadLog' carries with it some associated state (the logging+-- environment) that can be modified locally with 'localData' and+-- 'localDomain'.+class MonadTime m => MonadLog m where+  -- | Write a message to the log.+  logMessage  :: UTCTime  -- ^ Time of the event.+              -> LogLevel -- ^ Log level.+              -> T.Text   -- ^ Log message.+              -> Value    -- ^ Additional data associated with the message.+              -> m ()+  -- | Extend the additional data associated with each log message locally.+  localData   :: [Pair] -> m a -> m a+  -- | Extend the current application domain locally.+  localDomain :: T.Text -> m a -> m a++-- | Generic, overlapping instance.+instance (+    MonadLog m+  , Monad (t m)+  , MonadTransControl t+  ) => MonadLog (t m) where+    logMessage time level message = lift . logMessage time level message+    localData data_ m = controlT $ \run -> localData data_ (run m)+    localDomain domain m = controlT $ \run -> localDomain domain (run m)++controlT :: (MonadTransControl t, Monad (t m), Monad m)+         => (Run t -> m (StT t a)) -> t m a+controlT f = liftWith f >>= restoreT . return++----------------------------------------++-- | Log a message and its associated data using current time as the+-- event time and the 'LogAttention' log level.+logAttention :: (MonadLog m, ToJSON a) => T.Text -> a -> m ()+logAttention msg = logNow LogAttention msg . toJSON++-- | Log a message and its associated data using current time as the+-- event time and the 'LogInfo' log level.+logInfo :: (MonadLog m, ToJSON a) => T.Text -> a -> m ()+logInfo msg = logNow LogInfo msg . toJSON++-- | Log a message and its associated data using current time as the+-- event time and the 'LogTrace' log level.+logTrace :: (MonadLog m, ToJSON a) => T.Text -> a -> m ()+logTrace msg = logNow LogTrace msg . toJSON++-- | Like 'logAttention', but without any additional associated data.+logAttention_ :: MonadLog m => T.Text -> m ()+logAttention_ = (`logAttention` emptyObject)++-- | Like 'logInfo', but without any additional associated data.+logInfo_ :: MonadLog m => T.Text -> m ()+logInfo_ = (`logInfo` emptyObject)++-- | Like 'logTrace', but without any additional associated data.+logTrace_ :: MonadLog m => T.Text -> m ()+logTrace_ = (`logTrace` emptyObject)++----------------------------------------++-- | Write a log message using the given log level, message text and+-- additional data. Current time is used as the event time.+logNow :: MonadLog m => LogLevel -> T.Text -> Value -> m ()+logNow level message data_ = do+  time <- currentTime+  logMessage time level message data_
+ src/Log/Data.hs view
@@ -0,0 +1,128 @@+-- | Basic data types used throughout the package.+module Log.Data (+    LogLevel(..)+  , showLogLevel+  , readLogLevel+  , LogMessage(..)+  , showLogMessage+  ) where++import Control.DeepSeq+import Control.Applicative+import Data.Aeson+import Data.Aeson.Encode.Pretty+import Data.Aeson.Types+import Data.ByteString.Lazy (toStrict)+import Data.Time+import Prelude+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import qualified Data.Monoid as Monoid++-- | Available log levels.+data LogLevel = LogAttention | LogInfo | LogTrace+  deriving (Bounded, Eq, Ord, Show)++-- | This function is partial.+readLogLevel :: T.Text -> LogLevel+readLogLevel = either error id . readLogLevelEither+{-# INLINE readLogLevel #-}++readLogLevelEither :: T.Text -> Either String LogLevel+readLogLevelEither "attention" = Right LogAttention+readLogLevelEither "info"      = Right LogInfo+readLogLevelEither "trace"     = Right LogTrace+readLogLevelEither level       = Left $ "readLogLevel: unknown level: "+                                 ++ T.unpack level++showLogLevel :: LogLevel -> T.Text+showLogLevel LogAttention = "attention"+showLogLevel LogInfo      = "info"+showLogLevel LogTrace     = "trace"++instance ToJSON LogLevel where+  toJSON = toJSON . showLogLevel+  toEncoding = toEncoding . showLogLevel++instance FromJSON LogLevel where+  parseJSON = withText "LogLevel" $+    either fail pure . readLogLevelEither++instance NFData LogLevel where+  rnf = (`seq` ())++-- | Represents message to be logged.+data LogMessage = LogMessage {+  -- | Component of an application.+  lmComponent :: !T.Text+  -- | Aplication log domain.+, lmDomain    :: ![T.Text]+  -- | Time of the logged event.+, lmTime      :: !UTCTime+  -- | Log level.+, lmLevel     :: !LogLevel+  -- | Message to be logged.+, lmMessage   :: !T.Text+  -- | Additional data associated with the message.+, lmData      :: !Value+} deriving (Eq, Show)++-- | Render a 'LogMessage' to 'Text'.+showLogMessage :: Maybe UTCTime -- ^ The time that message was added to the log.+               -> LogMessage    -- ^ The actual message.+               -> T.Text+showLogMessage mInsertionTime LogMessage{..} = T.concat $ [+    T.pack $ formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S" lmTime+  , case mInsertionTime of+      Nothing -> " "+      Just it -> T.pack $ formatTime defaultTimeLocale " (%H:%M:%S) " it+  , T.toUpper $ showLogLevel lmLevel+  , " "+  , T.intercalate "/" $ lmComponent : lmDomain+  , ": "+  , lmMessage+  ] ++ if lmData == emptyObject+    then []+    else [" ", textifyData lmData]+  where+    textifyData :: Value -> T.Text+    textifyData = T.decodeUtf8 . toStrict . encodePretty' defConfig {+      confIndent = Spaces 2+    }++instance ToJSON LogMessage where+  toJSON LogMessage{..} = object [+      "component" .= lmComponent+    , "domain"    .= lmDomain+    , "time"      .= lmTime+    , "level"     .= lmLevel+    , "message"   .= lmMessage+    , "data"      .= lmData+    ]++  toEncoding LogMessage{..} = pairs $ Monoid.mconcat [+      "component" .= lmComponent+    , "domain"    .= lmDomain+    , "time"      .= lmTime+    , "level"     .= lmLevel+    , "message"   .= lmMessage+    , "data"      .= lmData+    ]++instance FromJSON LogMessage where+  parseJSON = withObject "LogMessage" $ \obj -> LogMessage+    -- to suppress warnings+    Control.Applicative.<$> obj .: "component"+    <*> obj .: "domain"+    <*> obj .: "time"+    <*> obj .: "level"+    <*> obj .: "message"+    <*> obj .: "data"++instance NFData LogMessage where+  rnf LogMessage{..} = rnf lmComponent+    `seq` rnf lmDomain+    `seq` rnf lmTime+    `seq` rnf lmLevel+    `seq` rnf lmMessage+    `seq` rnf lmData
+ src/Log/Internal/Logger.hs view
@@ -0,0 +1,70 @@+-- | The 'Logger' type - implementation details.+{-# OPTIONS_HADDOCK hide #-}+module Log.Internal.Logger (+    Logger(..)+  , execLogger+  , waitForLogger+  , shutdownLogger+  , withLogger+  ) where++import Data.Semigroup+import Control.Exception+import Prelude++import Log.Data++-- | An object used for communication with a logger thread that+-- outputs 'LogMessage's using e.g. PostgreSQL, Elasticsearch or+-- stdout (depending on the back-end chosen).+data Logger = Logger {+  loggerWriteMessage :: !(LogMessage -> IO ()) -- ^ Output a 'LogMessage'.+, loggerWaitForWrite :: !(IO ())+                     -- ^ Wait for the logger to output all messages+                     -- in its input queue (in the case logging is+                     -- done asynchronously).+, loggerShutdown     :: !(IO ())+                     -- ^ Kill the logger thread. Subsequent attempts+                     -- to write messages to the logger will raise an+                     -- exception.+}++-- | Execute logger to serialize a 'LogMessage'.+execLogger :: Logger -> LogMessage -> IO ()+execLogger Logger{..} = loggerWriteMessage++-- | Wait until all 'LogMessage's stored in the internal queue are+-- serialized.+waitForLogger :: Logger -> IO ()+waitForLogger Logger{..} = loggerWaitForWrite++-- | Shutdown the logger thread associated with this 'Logger'+-- object. Subsequent attempts to write messages via this 'Logger'+-- will result in an exception.+shutdownLogger :: Logger -> IO ()+shutdownLogger Logger{..} = loggerShutdown++-- | 'bracket'-like execution of an 'IO' action, verifying all messages+-- are properly logged. See 'mkBulkLogger'.+withLogger :: Logger -> (Logger -> IO r) -> IO r+withLogger logger act = act logger `finally` cleanup+  where+    cleanup = waitForLogger logger >> shutdownLogger logger++instance Semigroup Logger where+  l1 <> l2 = Logger {+    loggerWriteMessage = \msg -> do+      loggerWriteMessage l1 msg+      loggerWriteMessage l2 msg+  , loggerWaitForWrite = do+      loggerWaitForWrite l1+      loggerWaitForWrite l2+  , loggerShutdown     = do+      loggerShutdown l1+      loggerShutdown l2+  }++-- | Composition of 'Logger' objects.+instance Monoid Logger where+  mempty  = Logger (const $ return ()) (return ()) (return ())+  mappend = (<>)
+ src/Log/Logger.hs view
@@ -0,0 +1,165 @@+-- | The 'Logger' type of logging back-ends.+module Log.Logger (+    Logger+  , mkLogger+  , mkBulkLogger+  , execLogger+  , waitForLogger+  , shutdownLogger+  ) where++import Control.Applicative+import Control.Concurrent+import Control.Concurrent.STM+import Control.Exception+import Control.Monad+import Data.Semigroup+import Prelude+import qualified Data.Text as T+import qualified Data.Text.IO as T++import Log.Data+import Log.Internal.Logger++-- | Start a logger thread that consumes one queued message at a time.+mkLogger :: T.Text -> (LogMessage -> IO ()) -> IO Logger+mkLogger name exec = mkLoggerImpl+  newTQueueIO isEmptyTQueue readTQueue writeTQueue (return ())+  name exec (return ())++-- | Start an asynchronous logger thread that consumes all queued+-- messages once per second. To make sure that the messages get+-- written out in the presence of exceptions, use high-level wrappers+-- like 'withLogger', 'Log.Backend.ElasticSearch.withElasticSearchLogger' or+-- 'Log.Backend.StandardOutput.Bulk.withBulkStdOutLogger'+-- instead of this function directly.+--+-- Note: some messages can be lost when the main thread shuts down+-- without making sure that all logger threads have written out all+-- messages, because in that case child threads are not given a chance+-- to clean up by the RTS. This is apparently a feature:+-- <https://mail.haskell.org/pipermail/haskell-cafe/2014-February/112754.html>+--+-- To work around this issue, make sure that the main thread doesn't+-- exit until all its children have terminated. The 'async' package+-- makes this easy.+--+-- Problematic example:+--+-- @+-- import Control.Concurrent.Async+--+-- main :: IO ()+-- main = do+--    logger \<- 'Log.Backend.ElasticSearch.elasticSearchLogger'+--    a \<- 'Control.Concurrent.Async.async' ('Log.Backend.ElasticSearch.withElasticSearchLogger' $ \\logger ->+--                'Log.Monad.runLogT' "main" logger $ 'Log.Class.logTrace_' "foo")+--    -- Main thread exits without waiting for the child+--    -- to finish and without giving the child a chance+--    -- to do proper cleanup.+-- @+--+-- Fixed example:+--+-- @+-- import Control.Concurrent.Async+--+-- main :: IO ()+-- main = do+--    logger \<- 'Log.Backend.ElasticSearch.elasticSearchLogger'+--    a \<- 'Control.Concurrent.Async.async' ('Log.Backend.ElasticSearch.withElasticSearchLogger' $ \\logger ->+--                'Log.Monad.runLogT' "main" logger $ 'Log.Class.logTrace_' "foo")+--    'Control.Concurrent.Async.wait' a+--    -- Main thread waits for the child to finish, giving+--    -- it a chance to shut down properly. This works even+--    -- in the presence of exceptions in the child thread.+-- @+mkBulkLogger :: T.Text -> ([LogMessage] -> IO ()) -> IO () -> IO Logger+mkBulkLogger = mkLoggerImpl+  newSQueueIO isEmptySQueue readSQueue writeSQueue (threadDelay 1000000)++----------------------------------------++-- | A simple STM based queue.+newtype SQueue a = SQueue (TVar [a])++-- | Create an instance of 'SQueue'.+newSQueueIO :: IO (SQueue a)+newSQueueIO = SQueue <$> newTVarIO []++-- | Check if an 'SQueue' is empty.+isEmptySQueue :: SQueue a -> STM Bool+isEmptySQueue (SQueue queue) = null <$> readTVar queue++-- | Read all the values stored in an 'SQueue'.+readSQueue :: SQueue a -> STM [a]+readSQueue (SQueue queue) = do+  elems <- readTVar queue+  when (null elems) retry+  writeTVar queue []+  return $ reverse elems++-- | Write a value to an 'SQueue'.+writeSQueue :: SQueue a -> a -> STM ()+writeSQueue (SQueue queue) a = modifyTVar queue (a :)++----------------------------------------++mkLoggerImpl :: IO queue+             -> (queue -> STM Bool)+             -> (queue -> STM msgs)+             -> (queue -> LogMessage -> STM ())+             -> IO ()+             -> T.Text+             -> (msgs -> IO ())+             -> IO ()+             -> IO Logger+mkLoggerImpl newQueue isQueueEmpty readQueue writeQueue afterExecDo+  name exec sync = do+  queue      <- newQueue+  inProgress <- newTVarIO False+  isRunning  <- newTVarIO True+  tid <- forkFinally (forever $ loop queue inProgress)+                     (\_ -> cleanup queue inProgress)+  return Logger {+    loggerWriteMessage = \msg -> atomically $ do+        checkIsRunning isRunning+        writeQueue queue msg,+    loggerWaitForWrite = do+        atomically $ waitForWrite queue inProgress+        sync,+    loggerShutdown     = do+        killThread tid+        atomically $ writeTVar isRunning False+    }+  where+    checkIsRunning isRunning' = do+      isRunning <- readTVar isRunning'+      when (not isRunning) $+        throwSTM (AssertionFailed $ "Log.Logger.mkLoggerImpl: "+                   ++ "attempt to write to a shut down logger")++    loop queue inProgress = do+      step queue inProgress+      afterExecDo++    step queue inProgress = do+      msgs <- atomically $ do+        writeTVar inProgress True+        readQueue queue+      exec msgs+      atomically $ writeTVar inProgress False++    cleanup queue inProgress = do+      step queue inProgress+      sync+      -- Don't call afterExecDo, since it's either a no-op or a+      -- threadDelay.+      printLoggerTerminated++    waitForWrite queue inProgress = do+      isEmpty <- isQueueEmpty queue+      isInProgress <- readTVar inProgress+      when (not isEmpty || isInProgress) retry++    printLoggerTerminated = T.putStrLn $ name <> ": logger thread terminated"
+ src/Log/Monad.hs view
@@ -0,0 +1,115 @@+-- | The 'LogT' monad transformer for adding logging capabilities to any monad.+{-# LANGUAGE CPP #-}+module Log.Monad (+    Logger+  , LoggerEnv(..)+  , InnerLogT+  , LogT(..)+  , runLogT+  , mapLogT+  ) where++import Control.Applicative+import Control.DeepSeq+import Control.Monad.Base+import Control.Monad.Catch+import Control.Monad.Reader+import Control.Monad.Trans.Control+import Data.Aeson+import Data.Aeson.Types+import Data.Text (Text)+import Prelude+import qualified Control.Exception as E+import qualified Data.HashMap.Strict as H++import Log.Class+import Log.Data+import Log.Logger++-- | The state that every 'LogT' carries around.+data LoggerEnv = LoggerEnv {+  leLogger    :: !Logger -- ^ The 'Logger' to use.+, leComponent :: !Text   -- ^ Current application component.+, leDomain    :: ![Text] -- ^ Current application domain.+, leData      :: ![Pair] -- ^ Additional data to be merged with the+                         -- log message\'s data.+}++type InnerLogT = ReaderT LoggerEnv++-- | Monad transformer that adds logging capabilities to the underlying monad.+newtype LogT m a = LogT { unLogT :: InnerLogT m a }+  deriving (Alternative, Applicative, Functor, Monad, MonadBase b, MonadCatch+           ,MonadIO, MonadMask, MonadPlus, MonadThrow, MonadTrans)++-- | Run a 'LogT' computation.+--+-- Note that in the case of asynchronous/bulk loggers 'runLogT'+-- doesn't guarantee that all messages are actually written to the log+-- once it finishes. Use 'withPGLogger' or 'withElasticSearchLogger'+-- for that.+runLogT :: Text     -- ^ Application component name to use.+        -> Logger   -- ^ The logging back-end to use.+        -> LogT m a -- ^ The 'LogT' computation to run.+        -> m a+runLogT component logger m = runReaderT (unLogT m) LoggerEnv {+  leLogger = logger+, leComponent = component+, leDomain = []+, leData = []+} -- We can't do synchronisation here, since 'runLogT' can be invoked+  -- quite often from the application (e.g. on every request).++-- | Transform the computation inside a 'LogT'.+mapLogT :: (m a -> n b) -> LogT m a -> LogT n b+mapLogT f = LogT . mapReaderT f . unLogT++instance MonadTransControl LogT where+#if MIN_VERSION_monad_control(1,0,0)+  type StT LogT m = StT InnerLogT m+  liftWith = defaultLiftWith LogT unLogT+  restoreT = defaultRestoreT LogT+#else+  newtype StT LogT m = StLogT { unStLogT :: StT InnerLogT m }+  liftWith = defaultLiftWith LogT unLogT StLogT+  restoreT = defaultRestoreT LogT unStLogT+#endif+  {-# INLINE liftWith #-}+  {-# INLINE restoreT #-}++instance MonadBaseControl b m => MonadBaseControl b (LogT m) where+#if MIN_VERSION_monad_control(1,0,0)+  type StM (LogT m) a = ComposeSt LogT m a+  liftBaseWith = defaultLiftBaseWith+  restoreM     = defaultRestoreM+#else+  newtype StM (LogT m) a = StMLogT { unStMLogT :: ComposeSt LogT m a }+  liftBaseWith = defaultLiftBaseWith StMLogT+  restoreM     = defaultRestoreM unStMLogT+#endif+  {-# INLINE liftBaseWith #-}+  {-# INLINE restoreM #-}++instance (MonadBase IO m, MonadTime m) => MonadLog (LogT m) where+  logMessage time level message data_ = LogT $ ReaderT logMsg+    where+      logMsg LoggerEnv{..} = liftBase $ do+        execLogger leLogger =<< E.evaluate (force lm)+        where+          lm = LogMessage {+            lmComponent = leComponent+          , lmDomain = leDomain+          , lmTime = time+          , lmLevel = level+          , lmMessage = message+          , lmData = case data_ of+            Object obj -> Object . H.union obj $ H.fromList leData+            _ | null leData -> data_+              | otherwise -> object $ ("_data", data_) : leData+          }++  localData data_ =+    LogT . local (\e -> e { leData = data_ ++ leData e }) . unLogT++  localDomain domain =+    LogT . local (\e -> e { leDomain = leDomain e ++ [domain] }) . unLogT