on a3dbdb8 help sign in
6 files changed+207−60
since splitsnapshots
unifiedsplit
katip/katip.cabal+28−26 katip/src/Katip.hs+1 katip/src/Katip/Scribes/Handle.hs+43−26 katip/src/Katip/Scribes/RotatingHandler.hs+126 katip/test/Katip/Tests/Scribes/Handle.hs+6−8 stack.yaml+3
katip/katip.cabal+28−26
@@ -36,22 +36,23 @@ flag lib-Werror
3636 manual: True
3737
3838library
39− exposed-modules:
40− Katip
41− Katip.Core
42− Katip.Format.Time
43− Katip.Monadic
44− Katip.Scribes.Handle
39+ exposed-modules: Katip
40+ , Katip.Core
41+ , Katip.Format.Time
42+ , Katip.Monadic
43+ , Katip.Scribes.Handle
44+ , Katip.Scribes.RotatingHandler
4545
46− default-extensions:
47− DeriveGeneric
48− FlexibleContexts
49− GeneralizedNewtypeDeriving
50− MultiParamTypeClasses
51− RankNTypes
52− RecordWildCards
53− TemplateHaskell
54− OverloadedStrings
46+ default-extensions: DeriveGeneric
47+ , FlexibleContexts
48+ , GeneralizedNewtypeDeriving
49+ , LambdaCase
50+ , MultiParamTypeClasses
51+ , OverloadedStrings
52+ , RankNTypes
53+ , RecordWildCards
54+ , ScopedTypeVariables
55+ , TemplateHaskell
5556
5657 build-depends: base >=4.9.0.0 && <5
5758 , aeson >=0.6
@@ -59,27 +60,28 @@ library
5960 , auto-update >= 0.1
6061 , bytestring >= 0.9
6162 , containers >=0.4
63+ , deepseq
6264 , either >= 4
63− , safe-exceptions >= 0.1.0.0
6465 , hostname >=1.0
66+ , microlens >= 0.2.0.0
67+ , microlens-th >= 0.1.0.0
68+ , monad-control >= 1.0
69+ , mtl >= 2.0
6570 , old-locale >= 1.0
71+ , resourcet >= 1.1
72+ , safe-exceptions >= 0.1.0.0
73+ , scientific >= 0.3.3.0
74+ , semigroups
75+ , stm >= 2.4 && < 2.5
6676 , string-conv >= 0.1
6777 , template-haskell >= 2.8
6878 , text >= 0.11
6979 , time >= 1
7080 , transformers >= 0.3
71− , transformers-compat
72− , unordered-containers >= 0.2
73− , monad-control >= 1.0
74− , mtl >= 2.0
7581 , transformers-base >= 0.3
76− , resourcet >= 1.1
77− , scientific >= 0.3.3.0
78− , microlens >= 0.2.0.0
79− , microlens-th >= 0.1.0.0
80− , semigroups
82+ , transformers-compat
8183 , unliftio-core >= 0.1
82− , stm >= 2.4
84+ , unordered-containers >= 0.2
8385
8486 hs-source-dirs: src
8587 default-language: Haskell2010
katip/src/Katip.hs+1
@@ -181,6 +181,7 @@ module Katip
181181 , mkHandleScribe
182182 , mkHandleScribeWithFormatter
183183 , mkFileScribe
184+ , mkFileScribeWithFormatter
184185 , ColorStrategy (..)
185186 , ItemFormatter
186187 , bracketFormat
katip/src/Katip/Scribes/Handle.hs+43−26
@@ -3,17 +3,20 @@ module Katip.Scribes.Handle where
33-------------------------------------------------------------------------------
44import Control.Applicative as A
55import Control.Concurrent
6−import Control.Exception (bracket_, finally)
6+import Control.Exception (finally)
77import Data.Aeson
8−import Data.Text.Lazy (toStrict)
9−import Data.Text.Lazy.Encoding (decodeUtf8)
10−import qualified Data.HashMap.Strict as HM
8+import Data.Aeson.Text
9+import Data.ByteString.Builder as B
10+import Data.ByteString.Lazy as BL (hPutStr)
11+import Data.ByteString.Lazy.Char8 as BL (singleton)
1112import Data.Monoid as M
1213import Data.Scientific as S
1314import Data.Text (Text)
14−import Data.Text.Lazy.Builder
15−import Data.Text.Lazy.IO as T
15+import Data.Text.Lazy.Builder as TL
16+import Data.Text.Lazy.Encoding (encodeUtf8)
1617import System.IO
18+import qualified Data.HashMap.Strict as HM
19+
1720-------------------------------------------------------------------------------
1821import Katip.Core
1922import Katip.Format.Time (formatAsLogTime)
@@ -21,15 +24,20 @@ import Katip.Format.Time (formatAsLogTime)
2124
2225
2326-------------------------------------------------------------------------------
24−brackets :: Builder -> Builder
27+brackets :: TL.Builder -> TL.Builder
2528brackets m = fromText "[" M.<> m <> fromText "]"
2629
30+textBuilderToBS :: TL.Builder -> B.Builder
31+textBuilderToBS = B.lazyByteString . encodeUtf8 . toLazyText
32+
33+appendNL :: B.Builder -> B.Builder
34+appendNL b = b <> B.lazyByteString (BL.singleton '\n')
2735
2836-------------------------------------------------------------------------------
29−getKeys :: LogItem s => Verbosity -> s -> [Builder]
37+getKeys :: LogItem s => Verbosity -> s -> [TL.Builder]
3038getKeys verb a = concat (renderPair A.<$> HM.toList (payloadObject verb a))
3139 where
32− renderPair :: (Text, Value) -> [Builder]
40+ renderPair :: (Text, Value) -> [TL.Builder]
3341 renderPair (k,v) =
3442 case v of
3543 Object o -> concat [renderPair (k <> "." <> k', v') | (k', v') <- HM.toList o]
@@ -65,6 +73,19 @@ data ColorStrategy
6573mkHandleScribe :: ColorStrategy -> Handle -> PermitFunc -> Verbosity -> IO Scribe
6674mkHandleScribe = mkHandleScribeWithFormatter bracketFormat
6775
76+
77+mkFileScribeWithFormatter :: (forall a . LogItem a => ItemFormatter a)
78+ -> ColorStrategy
79+ -> FilePath
80+ -> PermitFunc
81+ -> Verbosity
82+ -> IO Scribe
83+mkFileScribeWithFormatter formatter color f permitF verb = do
84+ h <- openBinaryFile f AppendMode
85+ Scribe logger finalizer permit <- mkHandleScribeWithFormatter formatter color h permitF verb
86+ return (Scribe logger (finalizer `finally` hClose h) permit)
87+
88+
6889-- | Logs to a file handle such as stdout, stderr, or a file. Takes a custom
6990-- `ItemFormatter` that can be used to format `Item` as needed.
7091--
@@ -77,14 +98,12 @@ mkHandleScribeWithFormatter :: (forall a . LogItem a => ItemFormatter a)
7798 -> Verbosity
7899 -> IO Scribe
79100mkHandleScribeWithFormatter itemFormatter cs h permitF verb = do
80− hSetBuffering h LineBuffering
81101 colorize <- case cs of
82102 ColorIfTerminal -> hIsTerminalDevice h
83103 ColorLog b -> return b
84104 lock <- newMVar ()
85− let logger i@Item{..} = do
86− bracket_ (takeMVar lock) (putMVar lock ()) $
87− T.hPutStrLn h $ toLazyText $ itemFormatter colorize verb i
105+ let logger i@Item{..} = withMVar lock $ \() -> do
106+ BL.hPutStr h $ toLazyByteString $ itemFormatter colorize verb i
88107 return $ Scribe logger (hFlush h) permitF
89108
90109
@@ -95,19 +114,17 @@ mkHandleScribeWithFormatter itemFormatter cs h permitF verb = do
95114-- 'closeScribe'/'closeScribes'. Does not do log coloring. Sets handle
96115-- to 'LineBuffering' mode.
97116mkFileScribe :: FilePath -> PermitFunc -> Verbosity -> IO Scribe
98−mkFileScribe f permitF verb = do
99− h <- openFile f AppendMode
100− Scribe logger finalizer permit <- mkHandleScribe (ColorLog False) h permitF verb
101− return (Scribe logger (finalizer `finally` hClose h) permit)
117+mkFileScribe = mkFileScribeWithFormatter bracketFormat (ColorLog False)
102118
103119
104120-------------------------------------------------------------------------------
105121-- | A custom ItemFormatter for logging `Item`s. Takes a `Bool` indicating
106122-- whether to colorize the output, `Verbosity` of output, and an `Item` to
107−-- format.
123+-- format. Note that formatter is responsible for appending the newline at the
124+-- end of the string if required
108125--
109126-- See `bracketFormat` and `jsonFormat` for examples.
110−type ItemFormatter a = Bool -> Verbosity -> Item a -> Builder
127+type ItemFormatter a = Bool -> Verbosity -> Item a -> B.Builder
111128
112129
113130formatItem :: LogItem a => ItemFormatter a
@@ -121,10 +138,10 @@ formatItem = bracketFormat
121138-- > [2016-05-11 21:01:15][MyApp.confrabulation][Debug][myhost.example.com][PID 1724][ThreadId 1154][confrab_factor:42.0][main:Helpers.Logging Helpers/Logging.hs:41:9] Confrabulating widgets, with extra namespace and context
122139-- > [2016-05-11 21:01:15][MyApp][Info][myhost.example.com][PID 1724][ThreadId 1154][main:Helpers.Logging Helpers/Logging.hs:43:7] Namespace and context are back to normal
123140bracketFormat :: LogItem a => ItemFormatter a
124−bracketFormat withColor verb Item{..} =
141+bracketFormat withColor verb Item{..} = appendNL $ textBuilderToBS $
125142 brackets nowStr <>
126143 brackets (mconcat $ map fromText $ intercalateNs _itemNamespace) <>
127− brackets (fromText (renderSeverity' _itemSeverity)) <>
144+ brackets (renderSeverity' _itemSeverity) <>
128145 brackets (fromString _itemHost) <>
129146 brackets ("PID " <> fromString (show _itemProcess)) <>
130147 brackets ("ThreadId " <> fromText (getThreadIdText _itemThread)) <>
@@ -135,7 +152,7 @@ bracketFormat withColor verb Item{..} =
135152 nowStr = fromText (formatAsLogTime _itemTime)
136153 ks = map brackets $ getKeys verb _itemPayload
137154 renderSeverity' severity =
138− colorBySeverity withColor severity (renderSeverity severity)
155+ colorBySeverity withColor severity (fromText $ renderSeverity severity)
139156
140157-- | Logs items as JSON. This can be useful in circumstances where you already
141158-- have infrastructure that is expecting JSON to be logged to a standard stream
@@ -145,15 +162,15 @@ bracketFormat withColor verb Item{..} =
145162-- > {"at":"2018-10-02T21:50:30.4523848Z","env":"production","ns":["MyApp","confrabulation"],"data":{"confrab_factor":42},"app":["MyApp"],"msg":"Confrabulating widgets, with extra namespace and context","pid":"10456","loc":{"loc_col":11,"loc_pkg":"main","loc_mod":"Helpers.Logging","loc_fn":"Helpers\\Logging.hs","loc_ln":53},"host":"myhost.example.com","sev":"Debug","thread":"ThreadId 139"}
146163-- > {"at":"2018-10-02T21:50:30.4523848Z","env":"production","ns":["MyApp"],"data":{},"app":["MyApp"],"msg":"Namespace and context are back to normal","pid":"10456","loc":{"loc_col":9,"loc_pkg":"main","loc_mod":"Helpers.Logging","loc_fn":"Helpers\\Logging.hs","loc_ln":55},"host":"myhost.example.com","sev":"Info","thread":"ThreadId 139"}
147164jsonFormat :: LogItem a => ItemFormatter a
148−jsonFormat withColor verb i =
149− fromText $
165+jsonFormat withColor verb i = appendNL $
166+ textBuilderToBS $
150167 colorBySeverity withColor (_itemSeverity i) $
151− toStrict $ decodeUtf8 $ encode $ itemJson verb i
168+ encodeToTextBuilder $ itemJson verb i
152169
153170-- | Color a text message based on `Severity`. `ErrorS` and more severe errors
154171-- are colored red, `WarningS` is colored yellow, and all other messages are
155172-- rendered in the default color.
156−colorBySeverity :: Bool -> Severity -> Text -> Text
173+colorBySeverity :: Bool -> Severity -> TL.Builder -> TL.Builder
157174colorBySeverity withColor severity msg = case severity of
158175 EmergencyS -> red msg
159176 AlertS -> red msg
katip/src/Katip/Scribes/RotatingHandler.hsnew file+126
@@ -0,0 +1,126 @@
1+module Katip.Scribes.RotatingHandler where
2+
3+import Control.Applicative
4+import Control.Concurrent
5+import Control.Concurrent.Async
6+import Control.Concurrent.STM
7+import Control.Debounce
8+import Control.DeepSeq
9+import Control.Exception
10+import Control.Monad
11+import Data.ByteString.Lazy as BL
12+import Data.IORef
13+import GHC.IO.Handle
14+import GHC.IO.Handle.FD
15+import GHC.IO.IOMode
16+
17+-- | File owner struct writing data to the file
18+-- sequentially. Automatic buffer flushing and log rotating.
19+data FileOwner = FileOwner
20+ { foDataQueune :: TBQueue BL.ByteString
21+ , foControlQueue :: TBQueue ControlMsg
22+ , foAsync :: Async ()
23+ -- ^ Wait for to be sure that worker thread is closed.
24+ }
25+
26+data FileOwnerSettings = FileOwnerSettings
27+ { fosDebounceFreq :: Maybe Int
28+ , fosDataQueueLen :: Int
29+ , fosControlQueueLen :: Int
30+ }
31+
32+defaultFileOwnerSettings :: FileOwnerSettings
33+defaultFileOwnerSettings = FileOwnerSettings
34+ { fosDebounceFreq = Just 200000 -- every 200ms
35+ , fosDataQueueLen = 1000
36+ , fosControlQueueLen = 100
37+ }
38+
39+data ControlMsg
40+ = CloseMsg
41+ | FlushMsg
42+ | ReopenMsg
43+ deriving (Eq, Ord, Show)
44+
45+newFileOwner :: FilePath -> FileOwnerSettings -> IO FileOwner
46+newFileOwner fp s = do
47+ dqueue <- newTBQueueIO $ fosDataQueueLen s
48+ cqueue <- newTBQueueIO $ fosControlQueueLen s
49+ let
50+ newResource = do
51+ openBinaryFile fp AppendMode
52+ ack = newResource >>= newIORef
53+ release ref = do
54+ h <- readIORef ref
55+ hFlush h
56+ hClose h
57+ go ref = do
58+ let
59+ readAllData = do
60+ a <- readTBQueue dqueue
61+ -- flushTBQueue never retries
62+ as <- flushTBQueue dqueue
63+ return $ a:as
64+ readMsg
65+ = (Left <$> readTBQueue cqueue)
66+ <|> (Right <$> readAllData)
67+ debounce action = case fosDebounceFreq s of
68+ Nothing -> do
69+ return $ return ()
70+ Just freq -> mkDebounce $ defaultDebounceSettings
71+ { debounceAction = action
72+ , debounceFreq = freq }
73+ flush <- debounce $ readIORef ref >>= hFlush
74+ let
75+ recur = atomically readMsg >>= \case
76+ Right bs -> do
77+ h <- readIORef ref
78+ BL.hPutStr h $ mconcat bs
79+ flush -- auto debounced flush
80+ recur
81+ Left c -> case c of
82+ CloseMsg -> return () -- release will close the handler
83+ FlushMsg -> do
84+ readIORef ref >>= hFlush
85+ recur
86+ ReopenMsg -> do
87+ newH <- newResource
88+ oldH <- atomicModifyIORef' ref (\oldH -> (newH, oldH))
89+ hFlush oldH
90+ hClose oldH
91+ recur
92+ recur
93+ worker :: IO ()
94+ worker = try (bracket ack release go) >>= \case
95+ Left (_ :: SomeException) -> do
96+ threadDelay 100000
97+ -- to not restart the worker function too often
98+ worker
99+ -- We dont want the worker thread become unavailable because
100+ -- of exceptions. E.g. could not create or write the file.
101+ Right () -> return ()
102+ asyncRet <- async worker
103+ return $ FileOwner
104+ { foDataQueune = dqueue
105+ , foControlQueue = cqueue
106+ , foAsync = asyncRet
107+ }
108+
109+
110+
111+fileOwnerControl :: FileOwner -> ControlMsg -> IO ()
112+fileOwnerControl fo msg = do
113+ atomically $ writeTBQueue (foControlQueue fo) msg
114+ when (msg == CloseMsg) $ do
115+ -- Wait for worker thread to finish
116+ void $ waitCatch $ foAsync fo
117+
118+writeFileOwner :: FileOwner -> BL.ByteString -> IO ()
119+writeFileOwner fo bs = do
120+ a <- return $!! bs
121+ -- The deepseq here is to be sure that writing thread will not
122+ -- calculate thunks and will not get into the blackhole or
123+ -- something. Precalculating is the responsibility of the sending
124+ -- thread, because there may be several sending threads and only one
125+ -- writing.
126+ atomically $ writeTBQueue (foDataQueune fo) $!! a
katip/test/Katip/Tests/Scribes/Handle.hs+6−8
@@ -1,5 +1,5 @@
11{-# LANGUAGE OverloadedStrings #-}
2−{-# LANGUAGE RankNTypes #-}
2+{-# LANGUAGE RankNTypes #-}
33module Katip.Tests.Scribes.Handle
44 ( tests
55 ) where
@@ -7,12 +7,11 @@ module Katip.Tests.Scribes.Handle
77-------------------------------------------------------------------------------
88import Control.Monad
99import Data.Aeson
10−import qualified Data.ByteString.Char8 as B
10+import Data.ByteString.Builder as B
1111import qualified Data.ByteString.Lazy as BL
1212import Data.Monoid as M
1313import Data.Text (Text)
1414import qualified Data.Text as T
15−import qualified Data.Text.Encoding as T
1615import qualified Data.Text.Lazy as LT
1716import qualified Data.Text.Lazy.Builder as LT
1817import Data.Time
@@ -245,14 +244,13 @@ writeJsonLog = writeFormattedLog jsonFormat
245244
246245writeFormattedLog :: (forall a . LogItem a => ItemFormatter a) -> (FilePath, Handle) -> IO (BL.ByteString)
247246writeFormattedLog format (path, h) = do
248− mapM_ (put . formatOne) genItems
249− mapM_ (put . formatOne) genTypedItems
247+ mapM_ put genItems
248+ mapM_ put genTypedItems
250249 hClose h
251250 BL.readFile path
252251 where
253− formatOne :: LogItem a => Item a -> Text
254− formatOne = LT.toStrict . LT.toLazyText . format False V3
255− put = B.hPutStrLn h . T.encodeUtf8
252+ put :: (LogItem a) => Item a -> IO ()
253+ put = BL.hPutStr h . toLazyByteString . format False V3
256254
257255setupTempFile :: IO (FilePath, Handle)
258256setupTempFile = do
stack.yaml+3
@@ -1,3 +1,6 @@
1+nix:
2+ packages:
3+ - zlib
14resolver: lts-12.26
25packages:
36 - katip