packages feed

lager-1.0.0.0: lib/Lager.hs

{-# LANGUAGE DeriveGeneric #-}

-- | Concurrent Lager 🍺
--
-- @
-- import Lager
--
-- main =
--   'withLager' \"APP\" ['File' 'Info' \"log.txt\"] $ \\l -> do
--     'logDebug' l "Cheers! 🍻"
--     'logWarn'  l "Warning!"
-- @
module Lager
  ( -- * Logging
    Lager
  , withLager
  , newLager
  , newLagerSTM
  , runLager
  , drinkLager
  , streamLager
  , extendLager
  , logDebug
  , logDebugSTM
  , logInfo
  , logInfoSTM
  , logNotice
  , logNoticeSTM
  , logWarn
  , logWarnSTM
  , logErr
  , logErrSTM
  , logCrit
  , logCritSTM
  , logAlert
  , logAlertSTM
  , logEmerg
  , logEmergSTM
  , Msg(..)
  , -- * Target
    Target(..)
  , defConsole
  , Level(..)
  , defLevelColor
  , Color(..)
  , -- * Exception
    LagerException(..)
  ) where

import Control.Applicative
import Control.Concurrent
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Exception
import Control.Monad
import Data.Functor
import Data.Ord
import qualified Data.Text.Lazy as T
import GHC.Generics
import Prettyprinter
import Prettyprinter.Render.Terminal
import System.IO

-- | Logging handle
data Lager = Lager
  { nm :: String      -- source name
  , tg :: [Target]
  , wc :: TChan Msg   -- write chan
  , rc :: [TChan Msg] -- read chans
  , drink :: TVar Bool
  , drunk :: TVar Bool
  }

-- | Log message
data Msg = Msg
  { lvl :: Level
  , txt :: String -- ^ message
  , src :: String -- ^ logger source
  } deriving (Eq, Generic, Read, Show)

-- | Acquire a new handle
newLager :: String -> [Target] -> IO Lager
newLager nm' = atomically . newLagerSTM nm'

-- | Acquire a new handle in STM
newLagerSTM :: String -> [Target] -> STM Lager
newLagerSTM nm' tgt' = do
  wc' <- newBroadcastTChan
  Lager nm' tgt' wc'
    <$> replicateM (length tgt') (dupTChan wc')
    <*> newTVar False
    <*> newTVar False

-- | Acquire a 'newLager', concurrently 'runLager',
-- then finally 'drinkLager' if the body terminates
-- or throws an exception.
withLager :: String -> [Target] -> (Lager -> IO a) -> IO a
withLager nm' tgt' k = do
  l <- newLager nm' tgt'
  either id id
    <$> race (k l `finally` drinkLager l)
             (runLager l >> forever (threadDelay maxBound))

-- | Finish logging. Blocks until all outputs are written by 'runLager'.
drinkLager :: Lager -> IO ()
drinkLager l = do
  atomically $ writeTVar (drink l) True
  atomically $ checkDrunk l

checkDrink :: Lager -> STM ()
checkDrink = check <=< readTVar . drink

checkDrunk :: Lager -> STM ()
checkDrunk = check <=< readTVar . drunk

-- | Stream log messages
streamLager :: Lager -> (IO Msg -> IO a) -> IO a
streamLager l k = do
  r <- atomically $ throwIfDrunk l *> dupTChan (wc l)
  k  $ atomically $ throwIfDrunk l *> readTChan  r

-- | Extend the logger name
extendLager :: String -> Lager -> Lager
extendLager nm' l = Lager nm'' [] (wc l) [] (drink l) (drunk l)
  where
    nm'' | null nm'    = nm l
         | null (nm l) = nm'
         | otherwise   = nm l <> "|" <> nm'

-- | Log text in IO
lager :: Lager -> Level -> String -> IO ()
lager l lvl' = atomically . lagerSTM l lvl'

-- | Log text in STM
lagerSTM :: Lager -> Level -> String -> STM ()
lagerSTM l lvl' msg = do
  throwIfDrunk l
  writeTChan (wc l) $ Msg lvl' msg (nm l)

logDebug :: Lager -> String -> IO ()
logDebug l = lager l Debug

logDebugSTM :: Lager -> String -> STM ()
logDebugSTM l = lagerSTM l Debug

logInfo :: Lager -> String -> IO ()
logInfo l = lager l Info

logInfoSTM :: Lager -> String -> STM ()
logInfoSTM l = lagerSTM l Info

logNotice :: Lager -> String -> IO ()
logNotice l = lager l Notice

logNoticeSTM :: Lager -> String -> STM ()
logNoticeSTM l = lagerSTM l Notice

-- | warning
logWarn :: Lager -> String -> IO ()
logWarn l = lager l Warn

logWarnSTM :: Lager -> String -> STM ()
logWarnSTM l = lagerSTM l Warn

-- | error
logErr :: Lager -> String -> IO ()
logErr l = lager l Err

logErrSTM :: Lager -> String -> STM ()
logErrSTM l = lagerSTM l Err

-- | critical
logCrit :: Lager -> String -> IO ()
logCrit l = lager l Crit

logCritSTM :: Lager -> String -> STM ()
logCritSTM l = lagerSTM l Crit

logAlert :: Lager -> String -> IO ()
logAlert l = lager l Alert

logAlertSTM :: Lager -> String -> STM ()
logAlertSTM l = lagerSTM l Alert

-- | emergency
logEmerg :: Lager -> String -> IO ()
logEmerg l = lager l Emerg

logEmergSTM :: Lager -> String -> STM ()
logEmergSTM l = lagerSTM l Emerg

-- | Log level
data Level
  = Emerg -- ^ emergency
  | Alert
  | Crit  -- ^ critcal
  | Err   -- ^ error
  | Warn  -- ^ warning
  | Notice
  | Info
  | Debug
  deriving (Enum, Eq, Generic, Read, Show)

-- | 'Debug' < 'Emerg'
instance Ord Level where
  compare = comparing $ negate . fromEnum

annLevelColor :: Level -> [(Level, Color)] -> Doc AnsiStyle -> Doc AnsiStyle
annLevelColor l m = case lookup l m of
  Just c  -> annotate $ color c
  Nothing -> id

-- | Default 'Console' color schema
defLevelColor :: [(Level, Color)]
defLevelColor =
  [ (Emerg, Red), (Alert, Red), (Crit, Red), (Err, Red)
  , (Warn, Yellow), (Notice, Green), (Debug, Cyan)
  ]

-- | Log output
data Target
  = Console Level [(Level, Color)] -- ^ stdout, color schema
  | Journal Level                  -- ^ journald stdout
  | File Level FilePath
  deriving (Eq, Generic, Show)

-- | Default 'Console' target with 'Info' log level
-- and 'defLevelColor' color schema.
defConsole :: Target
defConsole = Console Info defLevelColor

-- | Run logging daemon
runLager :: Lager -> IO ()
runLager l = run `finally` atomically (writeTVar (drunk l) True)
  where
    run = case zip (tg l) (rc l) of
      [] -> atomically $ checkDrink l
      ts -> mapConcurrently_ (runTarget l) ts

runTarget :: Lager -> (Target, TChan Msg) -> IO ()
runTarget lgr (t, c) = case t of
  Console l m -> runHandle lgr stdout (renderConsoleColor m) l c
  Journal l -> runHandle lgr stdout renderJournal l c
  File l path ->
    withFile path WriteMode $ \hndl ->
      runHandle lgr hndl renderConsole l c

renderJournal :: Msg -> String
renderJournal msg =
  "<" <> show (fromEnum $ lvl msg) <> "> " <> renderConsole msg

renderConsole :: Msg -> String
renderConsole msg
  | null (src msg) = txt msg
  | otherwise      = "[" <> src msg <> "] " <> txt msg

renderConsoleColor :: [(Level, Color)] -> Msg -> String
renderConsoleColor m msg =
  T.unpack $
  renderLazy $
  layoutPretty defaultLayoutOptions $
  annLevelColor (lvl msg) m $
  pretty $
  renderConsole msg

runHandle
  :: Lager
  -> Handle
  -> (Msg -> String)
  -> Level
  -> TChan Msg
  -> IO ()
runHandle lgr hndl render l c = loop
  where
    loop = join $ atomically $ do
      m <- Just `fmap` readTChan c <|> (checkDrink lgr $> Nothing)
      case m of
        Just a  -> return $ logHandle hndl render l a >> loop
        Nothing -> do
          msgs <- listTChan c
          return $ mapM_ (logHandle hndl render l) msgs

logHandle :: Handle -> (Msg -> String) -> Level -> Msg -> IO ()
logHandle hndl render l msg
  | not (visible l msg) = return ()
  | otherwise           = hPutStrLn hndl $ render msg

listTChan :: TChan a -> STM [a]
listTChan t = loop
  where
    loop = do
      isEmpty <- isEmptyTChan t
      if isEmpty
        then return []
        else (:) <$> readTChan t <*> loop

visible :: Level -> Msg -> Bool
visible lvl' msg = lvl msg >= lvl'

data LagerException
  = LagerDaemonTerminated
    -- ^ the daemon is already closed due to 'drinkLager'
    -- or an exception

instance Show LagerException where
  show LagerDaemonTerminated = "lager: daemon terminated"

instance Exception LagerException

throwIfDrunk :: Lager -> STM ()
throwIfDrunk l = do
  isDrunk <- readTVar $ drunk l
  when isDrunk $ throwSTM LagerDaemonTerminated