{-# LANGUAGE UndecidableInstances #-}
module System.Log.Caster
(
LogMsg(..)
, broadcastLog
, LogQueue(..)
, newLogQueue
, LogChan(..)
, newLogChan
, Formatter
, Listener
, relayLog
, stdoutListener
, stdoutListenerWith
, terminalListener
, handleListener
, handleListenerFlush
, defaultFormatter
, terminalFormatter
, LogLevel(..)
, logAs
, debug
, info
, notice
, warn
, err
, critical
, alert
, emergency
, ToBuilder(..)
, fix
, ($:)
, (<:>)
) where
import Control.Concurrent.STM
import Control.Monad
import Control.Monad.IO.Class (MonadIO (..))
import qualified Data.ByteString as SBS
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.FastBuilder as FB
import qualified Data.ByteString.Lazy as LBS
import Data.Semigroup
import qualified Data.Text as ST
import qualified Data.Text.Encoding as STE
import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.Encoding as LTE
import Data.UnixTime (Format, UnixTime (..),
formatUnixTime, getUnixTime)
import GHC.IO.Unsafe (unsafePerformIO)
import System.IO (Handle, hFlush, stdout)
class ToBuilder a where
toBuilder :: a -> FB.Builder
instance ToBuilder FB.Builder where
toBuilder = id
instance ToBuilder String where
toBuilder = FB.stringUtf8
instance ToBuilder ST.Text where
toBuilder = FB.byteString . STE.encodeUtf8
instance ToBuilder LT.Text where
toBuilder = FB.byteString . LBS.toStrict . LTE.encodeUtf8
instance ToBuilder SBS.ByteString where
toBuilder = FB.byteString
instance ToBuilder LBS.ByteString where
toBuilder = FB.byteString . LBS.toStrict
instance ToBuilder BB.Builder where
toBuilder = FB.byteString . LBS.toStrict . BB.toLazyByteString
instance {-# OVERLAPPABLE #-} Show a => ToBuilder a where
toBuilder = FB.stringUtf8 . show
fix :: FB.Builder -> FB.Builder
fix = id
infixr 0 $:
($:) :: ToBuilder b => (FB.Builder -> b) -> FB.Builder -> b
($:) = ($)
infixr 6 <:>
(<:>) :: (ToBuilder a, ToBuilder b) => a -> b -> FB.Builder
a <:> b = toBuilder a <> toBuilder b
data LogLevel
= LogDebug
| LogInfo
| LogNotice
| LogWarn
| LogError
| LogCritical
| LogAlert
| LogEmergency
deriving (Show, Eq, Ord)
data LogMsg = LogMsg
{ logMsgLevel :: LogLevel
, logMsgTime :: UnixTime
, logMsgBuilder :: FB.Builder
}
newtype LogQueue = LogQueue (TQueue LogMsg)
newtype LogChan = LogChan (TChan LogMsg)
newLogQueue :: IO LogQueue
newLogQueue = LogQueue <$> newTQueueIO
newLogChan :: IO LogChan
newLogChan = LogChan <$> newBroadcastTChanIO
broadcastLog :: LogQueue -> LogChan -> IO ()
broadcastLog (LogQueue q) (LogChan c) = forever $
atomically $ readTQueue q >>= writeTChan c
type Formatter = LogMsg -> FB.Builder
type Listener = LogMsg -> IO ()
relayLog :: LogChan -> LogLevel -> Listener -> IO ()
relayLog (LogChan bchan) logLevel listener = do
chan <- atomically $ dupTChan bchan
forever $ do
msg <- atomically $ readTChan chan
when (logMsgLevel msg >= logLevel) $ listener msg
handleListener :: Formatter -> Handle -> Listener
handleListener f h = FB.hPutBuilder h . f
handleListenerFlush :: Formatter -> Handle -> Listener
handleListenerFlush f h msg = FB.hPutBuilder h (f msg) >> hFlush h
stdoutListenerWith :: Formatter -> Listener
stdoutListenerWith f = handleListenerFlush f stdout
stdoutListener :: Listener
stdoutListener = stdoutListenerWith defaultFormatter
terminalListener :: Listener
terminalListener = stdoutListenerWith terminalFormatter
defaultFormatter :: Formatter
defaultFormatter (LogMsg lev ut str) =
formatTime ut <> " - [" <> logLevelToBuilder lev <> "] " <> str <> "\n"
terminalFormatter :: Formatter
terminalFormatter = terminalFormatterWith
"\ESC[32m"
"\ESC[36m"
"\ESC[4m\ESC[36m"
"\ESC[4m\ESC[33m"
"\ESC[4m\ESC[31m"
"\ESC[1m\ESC[31m"
"\ESC[1m\ESC[35m"
"\ESC[5m\ESC[35m"
terminalFormatterWith :: FB.Builder -> FB.Builder -> FB.Builder -> FB.Builder -> FB.Builder -> FB.Builder -> FB.Builder -> FB.Builder -> Formatter
terminalFormatterWith fDebug fInfo fNotice fWarn fError fCritical fAlert fEmergency (LogMsg lev ut str) =
formatTime ut <> " - " <> fmt <> "[" <> logLevelToBuilder lev <> "]\ESC[0m " <> str <> "\n"
where
fmt = case lev of
LogDebug -> fDebug
LogInfo -> fInfo
LogNotice -> fNotice
LogWarn -> fWarn
LogError -> fError
LogCritical -> fCritical
LogAlert -> fAlert
LogEmergency -> fEmergency
logLevelToBuilder :: LogLevel -> FB.Builder
logLevelToBuilder = \case
LogDebug -> "DEBUG"
LogInfo -> "INFO"
LogNotice -> "NOTICE"
LogWarn -> "WARN"
LogError -> "ERROR"
LogCritical -> "CRITICAL"
LogAlert -> "ALERT"
LogEmergency -> "EMERGENCY"
{-# NOINLINE formatTime #-}
formatTime :: UnixTime -> FB.Builder
formatTime ut =
let
ut' = FB.byteString . unsafePerformIO $ formatUnixTime "%Y-%m-%d %T" ut
utMilli = FB.string7 . tail . show $ utMicroSeconds ut `div` 1000 + 1000
in
ut' <> "." <> utMilli
logAs :: (MonadIO m, ToBuilder s) => LogQueue -> LogLevel -> s -> m ()
logAs (LogQueue q) l s = liftIO $ do
ut <- getUnixTime
atomically $ writeTQueue q (LogMsg l ut (toBuilder s))
debug :: (MonadIO m, ToBuilder s) => LogQueue -> s -> m ()
debug q = logAs q LogDebug
info :: (MonadIO m, ToBuilder s) => LogQueue -> s -> m ()
info q = logAs q LogInfo
notice :: (MonadIO m, ToBuilder s) => LogQueue -> s -> m ()
notice q = logAs q LogNotice
warn :: (MonadIO m, ToBuilder s) => LogQueue -> s -> m ()
warn q = logAs q LogWarn
err :: (MonadIO m, ToBuilder s) => LogQueue -> s -> m ()
err q = logAs q LogError
critical :: (MonadIO m, ToBuilder s) => LogQueue -> s -> m ()
critical q = logAs q LogCritical
alert :: (MonadIO m, ToBuilder s) => LogQueue -> s -> m ()
alert q = logAs q LogAlert
emergency :: (MonadIO m, ToBuilder s) => LogQueue -> s -> m ()
emergency q = logAs q LogEmergency