packages feed

lager (empty) → 0.1.0.0

raw patch · 6 files changed

+371/−0 lines, 6 filesdep +asyncdep +basedep +lager

Dependencies added: async, base, lager, prettyprinter, prettyprinter-ansi-terminal, stm, text

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for lager++## 0.1.0.0 -- 3-26-2026++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2026 dopamane++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ README.md view
@@ -0,0 +1,16 @@+# Concurrent Lager :beer:++```hs+import Lager++main =+  withLager "APP" [defConsole] $ \l -> do+    logDebug   l "Cheers! 🍻"+    logWarning l "Warning!"+```++Build & Docs+```+cabal build+cabal haddock+```
+ lager.cabal view
@@ -0,0 +1,39 @@+cabal-version:      3.0+name:               lager+version:            0.1.0.0+synopsis:           Concurrent logging+license:            MIT+license-file:       LICENSE+author:             dopamane+maintainer:         dwc1295@gmail.com+copyright:          David Cox+category:           Logger+build-type:         Simple+extra-doc-files:    CHANGELOG.md+                    README.md++source-repository head+  type:     git+  location: https://github.com/dopamane/lager++library+  hs-source-dirs:   lib+  default-language: Haskell2010+  ghc-options:      -Wall -O2+  exposed-modules:  Lager+  build-depends:    async+                  , base < 5.0+                  , prettyprinter+                  , prettyprinter-ansi-terminal+                  , stm+                  , text++test-suite test+  hs-source-dirs:   test+  default-language: Haskell2010+  ghc-options:      -Wall -O2 -threaded+  type:             exitcode-stdio-1.0+  main-is:          Main.hs+  build-depends:    async+                  , base+                  , lager
+ lib/Lager.hs view
@@ -0,0 +1,276 @@+{-# LANGUAGE DeriveGeneric     #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Concurrent Lager 🍺+--+-- @+-- import Lager+--+-- main =+--   'withLager' \"APP\" ['File' 'Info' \"log.txt\"] $ \\l -> do+--     'logDebug'   l "Cheers! 🍻"+--     'logWarning' l "Warning!"+-- @+module Lager+  ( -- * Logging+    Lager+  , withLager+  , newLager+  , newLagerSTM+  , runLager+  , drinkLager+  , logDebug+  , logInfo+  , logNotice+  , logWarning+  , logErr+  , logCrit+  , logAlert+  , logEmerg+  , logSTM+  , logStream+  , logSub+  , Msg(..)+  , -- * Target+    Target(..)+  , defConsole+  , Level(..)+  , defLevelRGB+  , -- * 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 Data.Text.Lazy (Text)+import qualified Data.Text.Lazy    as T+import qualified Data.Text.Lazy.IO as T+import GHC.Generics+import Prettyprinter+import Prettyprinter.Render.Terminal+import System.IO++-- | Logging handle+data Lager = Lager+  { nm :: Text        -- 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 :: Text -- ^ message+  , src :: Text -- ^ logger source+  }++-- | Acquire a new handle+newLager :: Text -> [Target] -> IO Lager+newLager nm' = atomically . newLagerSTM nm'++-- | Acquire a new handle in STM+newLagerSTM :: Text -> [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 :: Text -> [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++logDebug :: Lager -> Text -> IO ()+logDebug l = lager l Debug++logInfo :: Lager -> Text -> IO ()+logInfo l = lager l Info++logNotice :: Lager -> Text -> IO ()+logNotice l = lager l Notice++logWarning :: Lager -> Text -> IO ()+logWarning l = lager l Warning++logErr :: Lager -> Text -> IO ()+logErr l = lager l Err++logCrit :: Lager -> Text -> IO ()+logCrit l = lager l Crit++logAlert :: Lager -> Text -> IO ()+logAlert l = lager l Alert++logEmerg :: Lager -> Text -> IO ()+logEmerg l = lager l Emerg++-- | Log text in IO+lager :: Lager -> Level -> Text -> IO ()+lager l lvl' = atomically . logSTM l lvl'++-- | Log text in STM+logSTM :: Lager -> Level -> Text -> STM ()+logSTM l lvl' msg = do+  throwIfDrunk l+  writeTChan (wc l) $ Msg lvl' msg (nm l)++-- | Extend the logger name+logSub :: Text -> Lager -> Lager+logSub nm' l = Lager nm'' [] (wc l) [] (drink l) (drunk l)+  where+    nm'' | T.null nm'    = nm l+         | T.null (nm l) = nm'+         | otherwise     = nm l <> "|" <> nm'++-- | Stream log messages+logStream :: Lager -> (IO Msg -> IO a) -> IO a+logStream l k = do+  r <- atomically $ throwIfDrunk l *> dupTChan (wc l)+  k  $ atomically $ throwIfDrunk l *> readTChan  r++throwIfDrunk :: Lager -> STM ()+throwIfDrunk l = do+  isDrunk <- readTVar $ drunk l+  when isDrunk $ throwSTM LagerDaemonTerminated++-- | Log level+data Level+  = Emerg -- ^ emergency+  | Alert+  | Crit  -- ^ critcal+  | Err   -- ^ error+  | 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+defLevelRGB :: [(Level, Color)]+defLevelRGB =+  [ (Emerg, Red), (Alert, Red), (Crit, Red), (Err, Red)+  , (Warning, Yellow), (Notice, Green), (Debug, Cyan)+  ]++-- | Log output+data Target+  = Console Level [(Level, Color)] -- ^ stdout, optional RGB+  | Journal Level                  -- ^ journald stdout+  | File Level FilePath+  deriving (Eq, Generic, Show)++-- | Default 'Console' target with 'Info' log level+-- and 'defLevelRGB' color schema.+defConsole :: Target+defConsole = Console Info defLevelRGB++-- | Run logging daemon+runLager :: Lager -> IO ()+runLager l =+  mapConcurrently_ (runTarget l) (zip (tg l) (rc l))+    `finally` atomically (writeTVar (drunk l) True)++runTarget :: Lager -> (Target, TChan Msg) -> IO ()+runTarget lgr (t, c) = case t of+  Console l m -> runHandle lgr stdout (renderConsoleRGB 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 -> Text+renderJournal msg =+  "<" <> T.pack (show $ fromEnum $ lvl msg) <> "> " <> renderConsole msg++renderConsole :: Msg -> Text+renderConsole msg+  | T.null (src msg) = txt msg+  | otherwise        = "[" <> src msg <> "] " <> txt msg++renderConsoleRGB :: [(Level, Color)] -> Msg -> Text+renderConsoleRGB m msg =+  renderLazy $+  layoutPretty defaultLayoutOptions $+  annLevelColor (lvl msg) m $+  pretty $+  renderConsole msg++runHandle+  :: Lager+  -> Handle+  -> (Msg -> Text)+  -> 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 -> Text) -> Level -> Msg -> IO ()+logHandle hndl render l msg+  | not (visible l msg) = return ()+  | otherwise           = T.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'++instance Show LagerException where+  show LagerDaemonTerminated = "lager: daemon terminated"++instance Exception LagerException
+ test/Main.hs view
@@ -0,0 +1,15 @@+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where++import Lager++main :: IO ()+main = withLager "APP" [Console Debug defLevelRGB, File Info "log.txt"] $ \l -> do+  logNotice l "Hello World!"+  logDebug l "Invisible"+  logErr l "NOO"+  logInfo l "YES"+  logWarning  l "HI"+  let l' = logSub "SUB" l+  logInfo l' "SUB!"