elynx-tools-0.6.1.0: src/ELynx/Tools/Logger.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
-- |
-- Module : ELynx.Tools.Logger
-- Description : Monad logger utility functions
-- Copyright : (c) Dominik Schrempf 2021
-- License : GPL-3.0-or-later
--
-- Maintainer : dominik.schrempf@gmail.com
-- Stability : unstable
-- Portability : portable
--
-- Creation date: Fri Sep 6 14:43:19 2019.
module ELynx.Tools.Logger
( Verbosity (..),
HasLock (..),
HasLogHandles (..),
HasStartingTime (..),
HasVerbosity (..),
Logger,
logOutB,
logDebugB,
logDebugS,
logWarnB,
logWarnS,
logInfoB,
logInfoS,
logHeader,
logInfoHeader,
logInfoFooter,
logInfoNewSection,
)
where
import Control.Concurrent.MVar
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Reader
import Data.Aeson.TH
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Lazy.Char8 as BL
import Data.List
import Data.Time
import Data.Version
import GHC.Generics
import Language.Haskell.TH
import Paths_elynx_tools
import System.Environment
import System.IO
-- | Verbosity levels.
data Verbosity = Quiet | Warn | Info | Debug
deriving (Bounded, Enum, Eq, Generic, Ord, Read, Show)
$(deriveJSON defaultOptions ''Verbosity)
-- | Types with an output lock for concurrent output.
class HasLock e where
getLock :: e -> MVar ()
-- | Types with logging information.
class HasLogHandles e where
getLogHandles :: e -> [Handle]
-- | Types with starting time.
class HasStartingTime s where
getStartingTime :: s -> UTCTime
-- | Types with verbosity.
class HasVerbosity s where
getVerbosity :: s -> Verbosity
-- | Reader transformer used for logging to a file and to standard output.
type Logger e a = ReaderT e IO a
msgPrepare :: BL.ByteString -> BL.ByteString -> BL.ByteString
msgPrepare pref msg = BL.intercalate "\n" $ map (BL.append pref) $ BL.lines msg
-- Make sure that concurrent output is not scrambled.
atomicAction :: HasLock e => IO () -> Logger e ()
atomicAction a = do
l <- reader getLock
liftIO $ withMVar l (const a)
-- | Write to standard output and maybe to log file.
logOutB ::
(HasLogHandles e, HasLock e) =>
-- | Prefix.
BL.ByteString ->
-- | Message.
BL.ByteString ->
Logger e ()
logOutB pref msg = do
hs <- reader getLogHandles
mapM_ (atomicAction . (`BL.hPutStrLn` msg')) hs
where
msg' = msgPrepare pref msg
-- Perform debug action.
logDebugA :: (HasLock e, HasLogHandles e, HasVerbosity e) => Logger e () -> Logger e ()
logDebugA a = reader getVerbosity >>= \v -> when (v >= Debug) a
-- | Log debug message.
logDebugB :: (HasLock e, HasLogHandles e, HasVerbosity e) => BL.ByteString -> Logger e ()
logDebugB = logDebugA . logOutB "D: "
-- | Log debug message.
logDebugS :: (HasLock e, HasLogHandles e, HasVerbosity e) => String -> Logger e ()
logDebugS = logDebugB . BL.pack
-- Perform warning action.
logWarnA :: (HasLogHandles e, HasVerbosity e) => Logger e () -> Logger e ()
logWarnA a = reader getVerbosity >>= \v -> when (v >= Warn) a
-- | Log warning message.
logWarnB :: (HasLock e, HasLogHandles e, HasVerbosity e) => BL.ByteString -> Logger e ()
logWarnB = logWarnA . logOutB "W: "
-- | Log warning message.
logWarnS :: (HasLock e, HasLogHandles e, HasVerbosity e) => String -> Logger e ()
logWarnS = logWarnB . BL.pack
-- Perform info action.
logInfoA :: (HasLogHandles e, HasVerbosity e) => Logger e () -> Logger e ()
logInfoA a = reader getVerbosity >>= \v -> when (v >= Info) a
-- | Log info message.
logInfoB :: (HasLock e, HasLogHandles e, HasVerbosity e) => BL.ByteString -> Logger e ()
logInfoB = logInfoA . logOutB " "
-- | Log info message.
logInfoS :: (HasLock e, HasLogHandles e, HasVerbosity e) => String -> Logger e ()
logInfoS = logInfoB . BL.pack
-- Be careful; it is necessary to synchronize the version numbers across packages.
versionString :: String
versionString = "ELynx Suite version " ++ showVersion version ++ "."
copyrightString :: String
copyrightString = "Developed by Dominik Schrempf."
compilationString :: String
compilationString =
"Compiled on "
++ $( stringE
=<< runIO
( formatTime defaultTimeLocale "%B %-e, %Y, at %H:%M %P, %Z."
`fmap` getCurrentTime
)
)
-- | A short header to be used in executables. 'unlines' doesn't work here
-- because it adds an additional newline at the end.
logHeader :: [String]
logHeader = [versionString, copyrightString, compilationString]
-- For a given width, align string to the right; use given fill character.
alignRightWithNoTrim :: Char -> Int -> BL.ByteString -> BL.ByteString
alignRightWithNoTrim c n s = BL.replicate (fromIntegral n - l) c <> s
where
l = BL.length s
-- Adapted from System.ProgressBar.renderDuration of package
-- [terminal-progressbar-0.4.1](https://hackage.haskell.org/package/terminal-progress-bar-0.4.1).
renderDuration :: NominalDiffTime -> BL.ByteString
renderDuration dt = hTxt <> mTxt <> sTxt
where
hTxt = renderDecimal h <> ":"
mTxt = renderDecimal m <> ":"
sTxt = renderDecimal s
(h, hRem) = ts `quotRem` 3600
(m, s) = hRem `quotRem` 60
-- Total amount of seconds
ts :: Int
ts = round dt
renderDecimal n = alignRightWithNoTrim '0' 2 $ BB.toLazyByteString $ BB.intDec n
-- Render a time stamp.
renderTime :: FormatTime t => t -> String
renderTime = formatTime defaultTimeLocale "%B %-e, %Y, at %H:%M %P, %Z."
-- | Log header.
logInfoHeader :: (HasLock e, HasLogHandles e, HasStartingTime e, HasVerbosity e) => String -> [String] -> Logger e ()
logInfoHeader cmdName cmdDsc = do
logInfoS hline
logInfoS $ intercalate "\n" logHeader
t <- renderTime <$> reader getStartingTime
p <- liftIO getProgName
as <- liftIO getArgs
logInfoS $
intercalate "\n" $
hline :
("Command name: " ++ cmdName) :
cmdDsc
++ ["Starting time: " ++ t, "Command line: " ++ p ++ " " ++ unwords as]
logInfoS hline
where
hline = replicate 78 '-'
-- | Log footer.
logInfoFooter :: (HasLock e, HasLogHandles e, HasStartingTime e, HasVerbosity e) => Logger e ()
logInfoFooter = do
ti <- reader getStartingTime
te <- liftIO getCurrentTime
let dt = te `diffUTCTime` ti
logInfoB $ "Wall clock run time: " <> renderDuration dt <> "."
logInfoS $ "End time: " <> renderTime te
-- | Unified way of creating a new section in the log.
logInfoNewSection :: (HasLock e, HasLogHandles e, HasVerbosity e) => String -> Logger e ()
logInfoNewSection s = logInfoS $ "== " <> s