git.iko.soy/typeable/katip-sync-handle
on master help sign in
Move this code out of our Katip fork.
commit78a6561e32c7891a5775898040c0bbd2de75218f authorNick <nick@typeable.io> dateMon 12 Apr 2021, 11:13 (6 years ago)
5 files changed+491
unifiedsplit
LICENSE+20 katip-sync-handle.cabal+36 src/Katip/Scribes/SyncHandle.hs+221 src/Katip/Scribes/SyncHandle/FileOwner.hs+147 stack.yaml+67
LICENSEnew file+20
@@ -0,0 +1,20 @@
1+Copyright (c) 2021 Alexei Uimanov
2+
3+Permission is hereby granted, free of charge, to any person obtaining
4+a copy of this software and associated documentation files (the
5+"Software"), to deal in the Software without restriction, including
6+without limitation the rights to use, copy, modify, merge, publish,
7+distribute, sublicense, and/or sell copies of the Software, and to
8+permit persons to whom the Software is furnished to do so, subject to
9+the following conditions:
10+
11+The above copyright notice and this permission notice shall be included
12+in all copies or substantial portions of the Software.
13+
14+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
katip-sync-handle.cabalnew file+36
@@ -0,0 +1,36 @@
1+cabal-version: 2.4
2+name: katip-sync-handle
3+version: 0.1
4+synopsis:
5+
6+-- A longer description of the package.
7+-- description:
8+homepage:
9+
10+-- A URL where users can report bugs.
11+-- bug-reports:
12+license: MIT
13+license-file: LICENSE
14+author: Alexei Uimanov
15+maintainer: s9gf4ult@gmail.com
16+
17+copyright: Antorica LLC
18+category: System
19+
20+library
21+ exposed-modules: Katip.Scribes.SyncHandle
22+ Katip.Scribes.SyncHandle.FileOwner
23+ build-depends: base >=4.12 && < 5,
24+ aeson >=1.4 && < 1.6,
25+ async,
26+ auto-update >= 0.1 && < 0.2,
27+ bytestring,
28+ deepseq,
29+ katip >= 0.8 && < 0.9,
30+ scientific,
31+ stm,
32+ text,
33+ unordered-containers >= 0.2 && < 0.3
34+ hs-source-dirs: src
35+ default-language: Haskell2010
36+ ghc-options: -Wall
src/Katip/Scribes/SyncHandle.hsnew file+221
@@ -0,0 +1,221 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE RankNTypes #-}
3+{-# LANGUAGE RecordWildCards #-}
4+module Katip.Scribes.SyncHandle where
5+
6+-------------------------------------------------------------------------------
7+import Control.Applicative as A
8+import Control.Concurrent
9+import Control.Exception (finally)
10+import Data.Aeson
11+import Data.Aeson.Text
12+import Data.ByteString.Builder as B
13+import Data.ByteString.Lazy as BL (hPutStr)
14+import Data.ByteString.Lazy.Char8 as BL (singleton)
15+import Data.Monoid as M
16+import Data.Scientific as S
17+import Data.Text (Text)
18+import Data.Text.Lazy.Builder as TL
19+import Data.Text.Lazy.Encoding (encodeUtf8)
20+import System.IO
21+import qualified Data.HashMap.Strict as HM
22+
23+-------------------------------------------------------------------------------
24+import Katip.Core
25+import Katip.Format.Time (formatAsLogTime)
26+import Katip.Scribes.SyncHandle.FileOwner
27+-------------------------------------------------------------------------------
28+
29+
30+-------------------------------------------------------------------------------
31+brackets :: TL.Builder -> TL.Builder
32+brackets m = fromText "[" M.<> m <> fromText "]"
33+
34+textBuilderToBS :: TL.Builder -> B.Builder
35+textBuilderToBS = B.lazyByteString . encodeUtf8 . toLazyText
36+
37+appendNL :: B.Builder -> B.Builder
38+appendNL b = b <> B.lazyByteString (BL.singleton '\n')
39+
40+-------------------------------------------------------------------------------
41+getKeys :: LogItem s => Verbosity -> s -> [TL.Builder]
42+getKeys verb a = concat (renderPair A.<$> HM.toList (payloadObject verb a))
43+ where
44+ renderPair :: (Text, Value) -> [TL.Builder]
45+ renderPair (k,v) =
46+ case v of
47+ Object o -> concat [renderPair (k <> "." <> k', v') | (k', v') <- HM.toList o]
48+ String t -> [fromText (k <> ":" <> t)]
49+ Number n -> [fromText (k <> ":") <> fromString (formatNumber n)]
50+ Bool b -> [fromText (k <> ":") <> fromString (show b)]
51+ Null -> [fromText (k <> ":null")]
52+ _ -> mempty -- Can't think of a sensible way to handle arrays
53+ formatNumber :: Scientific -> String
54+ formatNumber n =
55+ formatScientific Generic (if isFloating n then Nothing else Just 0) n
56+
57+
58+-------------------------------------------------------------------------------
59+data ColorStrategy
60+ = ColorLog Bool
61+ -- ^ Whether to use color control chars in log output
62+ | ColorIfTerminal
63+ -- ^ Color if output is a terminal
64+ deriving (Show, Eq)
65+
66+-------------------------------------------------------------------------------
67+-- | Logs to a file handle such as stdout, stderr, or a file. Contexts
68+-- and other information will be flattened out into bracketed
69+-- fields. For example:
70+--
71+-- > [2016-05-11 21:01:15][MyApp][Info][myhost.example.com][PID 1724][ThreadId 1154][main:Helpers.Logging Helpers/Logging.hs:32:7] Started
72+-- > [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
73+-- > [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
74+--
75+-- Returns the newly-created `Scribe`. The finalizer flushes the
76+-- handle. Handle mode is set to 'LineBuffering' automatically.
77+mkHandleScribe :: ColorStrategy -> Handle -> PermitFunc -> Verbosity -> IO Scribe
78+mkHandleScribe = mkHandleScribeWithFormatter bracketFormat
79+
80+
81+mkFileScribeWithFormatter :: (forall a . LogItem a => ItemFormatter a)
82+ -> ColorStrategy
83+ -> FilePath
84+ -> PermitFunc
85+ -> Verbosity
86+ -> IO Scribe
87+mkFileScribeWithFormatter formatter color f permitF verb = do
88+ h <- openBinaryFile f AppendMode
89+ Scribe logger finalizer permit <- mkHandleScribeWithFormatter formatter color h permitF verb
90+ return (Scribe logger (finalizer `finally` hClose h) permit)
91+
92+
93+-- | Logs to a file handle such as stdout, stderr, or a file. Takes a custom
94+-- `ItemFormatter` that can be used to format `Item` as needed.
95+--
96+-- Returns the newly-created `Scribe`. The finalizer flushes the
97+-- handle. Handle mode is set to 'LineBuffering' automatically.
98+mkHandleScribeWithFormatter :: (forall a . LogItem a => ItemFormatter a)
99+ -> ColorStrategy
100+ -> Handle
101+ -> PermitFunc
102+ -> Verbosity
103+ -> IO Scribe
104+mkHandleScribeWithFormatter itemFormatter cs h permitF verb = do
105+ colorize <- case cs of
106+ ColorIfTerminal -> hIsTerminalDevice h
107+ ColorLog b -> return b
108+ lock <- newMVar ()
109+ let logger i@Item{..} = withMVar lock $ \() -> do
110+ BL.hPutStr h $ toLazyByteString $ itemFormatter colorize verb i
111+ return $ Scribe logger (hFlush h) permitF
112+
113+-- | Just like mkFileScribeWithFormatter but does it with FileOwner. You can
114+-- rotate your logs with commands to the FileOwner directly. This scribe will
115+-- send messages to the given FileOwner
116+mkFileOwnerScribeWithFormatter :: (forall a . LogItem a => ItemFormatter a)
117+ -> ColorStrategy
118+ -> FileOwner
119+ -> PermitFunc
120+ -> Verbosity
121+ -> IO Scribe
122+mkFileOwnerScribeWithFormatter itemFormatter cs owner permitF verb = do
123+ let
124+ colorize = case cs of
125+ ColorIfTerminal -> False
126+ -- File is never a terminal
127+ ColorLog b -> b
128+ logger item = do
129+ writeFileOwner owner $ toLazyByteString
130+ $ itemFormatter colorize verb item
131+ scribe = Scribe logger (fileOwnerControl owner CloseMsg) permitF
132+ return scribe
133+
134+-------------------------------------------------------------------------------
135+-- | A specialization of 'mkHandleScribe' that takes a 'FilePath'
136+-- instead of a 'Handle'. It is responsible for opening the file in
137+-- 'AppendMode' and will close the file handle on
138+-- 'closeScribe'/'closeScribes'. Does not do log coloring. Sets handle
139+-- to 'LineBuffering' mode.
140+mkFileScribe :: FilePath -> PermitFunc -> Verbosity -> IO Scribe
141+mkFileScribe = mkFileScribeWithFormatter bracketFormat (ColorLog False)
142+
143+
144+-------------------------------------------------------------------------------
145+-- | A custom ItemFormatter for logging `Item`s. Takes a `Bool` indicating
146+-- whether to colorize the output, `Verbosity` of output, and an `Item` to
147+-- format. Note that formatter is responsible for appending the newline at the
148+-- end of the string if required
149+--
150+-- See `bracketFormat` and `jsonFormat` for examples.
151+type ItemFormatter a = Bool -> Verbosity -> Item a -> B.Builder
152+
153+
154+formatItem :: LogItem a => ItemFormatter a
155+formatItem = bracketFormat
156+{-# DEPRECATED formatItem "Use bracketFormat instead" #-}
157+
158+-- | A traditional 'bracketed' log format. Contexts and other information will
159+-- be flattened out into bracketed fields. For example:
160+--
161+-- > [2016-05-11 21:01:15][MyApp][Info][myhost.example.com][PID 1724][ThreadId 1154][main:Helpers.Logging Helpers/Logging.hs:32:7] Started
162+-- > [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
163+-- > [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
164+bracketFormat :: LogItem a => ItemFormatter a
165+bracketFormat withColor verb Item{..} = appendNL $ textBuilderToBS $
166+ brackets nowStr <>
167+ brackets (mconcat $ map fromText $ intercalateNs _itemNamespace) <>
168+ brackets (renderSeverity' _itemSeverity) <>
169+ brackets (fromString _itemHost) <>
170+ brackets ("PID " <> fromString (show _itemProcess)) <>
171+ brackets ("ThreadId " <> fromText (getThreadIdText _itemThread)) <>
172+ mconcat ks <>
173+ maybe mempty (brackets . fromString . locationToString) _itemLoc <>
174+ fromText " " <> (unLogStr _itemMessage)
175+ where
176+ nowStr = fromText (formatAsLogTime _itemTime)
177+ ks = map brackets $ getKeys verb _itemPayload
178+ renderSeverity' severity =
179+ colorBySeverity withColor severity (fromText $ renderSeverity severity)
180+
181+-- | Logs items as JSON. This can be useful in circumstances where you already
182+-- have infrastructure that is expecting JSON to be logged to a standard stream
183+-- or file. For example:
184+--
185+-- > {"at":"2018-10-02T21:50:30.4523848Z","env":"production","ns":["MyApp"],"data":{},"app":["MyApp"],"msg":"Started","pid":"10456","loc":{"loc_col":9,"loc_pkg":"main","loc_mod":"Helpers.Logging","loc_fn":"Helpers\\Logging.hs","loc_ln":44},"host":"myhost.example.com","sev":"Info","thread":"ThreadId 139"}
186+-- > {"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"}
187+-- > {"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"}
188+jsonFormat :: LogItem a => ItemFormatter a
189+jsonFormat withColor verb i = appendNL $
190+ textBuilderToBS $
191+ colorBySeverity withColor (_itemSeverity i) $
192+ encodeToTextBuilder $ itemJson verb i
193+
194+-- | Color a text message based on `Severity`. `ErrorS` and more severe errors
195+-- are colored red, `WarningS` is colored yellow, and all other messages are
196+-- rendered in the default color.
197+colorBySeverity :: Bool -> Severity -> TL.Builder -> TL.Builder
198+colorBySeverity withColor severity msg = case severity of
199+ EmergencyS -> red msg
200+ AlertS -> red msg
201+ CriticalS -> red msg
202+ ErrorS -> red msg
203+ WarningS -> yellow msg
204+ _ -> msg
205+ where
206+ red = colorize "31"
207+ yellow = colorize "33"
208+ colorize c s
209+ | withColor = "\ESC["<> c <> "m" <> s <> "\ESC[0m"
210+ | otherwise = s
211+
212+
213+-- | Provides a simple log environment with 1 scribe going to
214+-- stdout. This is a decent example of how to build a LogEnv and is
215+-- best for scripts that just need a quick, reasonable set up to log
216+-- to stdout.
217+ioLogEnv :: PermitFunc -> Verbosity -> IO LogEnv
218+ioLogEnv permit verb = do
219+ le <- initLogEnv "io" "io"
220+ lh <- mkHandleScribe ColorIfTerminal stdout permit verb
221+ registerScribe "stdout" lh defaultScribeSettings le
src/Katip/Scribes/SyncHandle/FileOwner.hsnew file+147
@@ -0,0 +1,147 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE LambdaCase #-}
3+{-# LANGUAGE ScopedTypeVariables #-}
4+{-# LANGUAGE RankNTypes #-}
5+module Katip.Scribes.SyncHandle.FileOwner where
6+
7+import Control.Applicative
8+import Control.Concurrent
9+import Control.Concurrent.Async
10+import Control.Concurrent.STM
11+import Control.Debounce
12+import Control.DeepSeq
13+import Control.Exception
14+import Control.Monad
15+import Data.ByteString.Lazy as BL
16+import Data.IORef
17+import GHC.IO.Handle
18+import GHC.IO.Handle.FD
19+import GHC.IO.IOMode
20+
21+#if MIN_VERSION_base(4,8,0)
22+import Numeric.Natural
23+#endif
24+
25+
26+-- | File owner struct writing data to the file
27+-- sequentially. Automatic buffer flushing and log rotating.
28+data FileOwner = FileOwner
29+ { foDataQueune :: TBQueue BL.ByteString
30+ , foControlQueue :: TBQueue ControlMsg
31+ , foAsync :: Async ()
32+ -- ^ Wait for to be sure that worker thread is closed.
33+ }
34+
35+#if MIN_VERSION_base(4,8,0)
36+type QueueLen = Natural
37+#else
38+type QueueLen = Int
39+#endif
40+
41+data FileOwnerSettings = FileOwnerSettings
42+ { fosDebounceFreq :: Maybe Int
43+ , fosDataQueueLen :: QueueLen
44+ , fosControlQueueLen :: QueueLen
45+ }
46+
47+defaultFileOwnerSettings :: FileOwnerSettings
48+defaultFileOwnerSettings = FileOwnerSettings
49+ { fosDebounceFreq = Just 200000 -- every 200ms
50+ , fosDataQueueLen = 1000
51+ , fosControlQueueLen = 100
52+ }
53+
54+data ControlMsg
55+ = CloseMsg
56+ | FlushMsg
57+ | ReopenMsg
58+ deriving (Eq, Ord, Show)
59+
60+newFileOwner :: FilePath -> FileOwnerSettings -> IO FileOwner
61+newFileOwner fp s = do
62+ dqueue <- newTBQueueIO $ fosDataQueueLen s
63+ cqueue <- newTBQueueIO $ fosControlQueueLen s
64+ let
65+ newResource = do
66+ openBinaryFile fp AppendMode
67+ ack = newResource >>= newIORef
68+ release ref = do
69+ h <- readIORef ref
70+ hFlush h
71+ hClose h
72+ go ref = do
73+ let
74+ readAllData = do
75+ a <- readTBQueue dqueue
76+ -- flushTBQueue never retries
77+ as <- flushTBQueue dqueue
78+ return $ a:as
79+ readMsg
80+ = (Left <$> readTBQueue cqueue)
81+ <|> (Right <$> readAllData)
82+ debounce action = case fosDebounceFreq s of
83+ Nothing -> do
84+ return $ return ()
85+ Just freq -> mkDebounce $ defaultDebounceSettings
86+ { debounceAction = action
87+ , debounceFreq = freq }
88+ flush <- debounce $ readIORef ref >>= hFlush
89+ let
90+ recur = atomically readMsg >>= \case
91+ Right bs -> do
92+ h <- readIORef ref
93+ BL.hPutStr h $ mconcat bs
94+ flush -- auto debounced flush
95+ recur
96+ Left c -> case c of
97+ CloseMsg -> do
98+ lastMsgs <- atomically $ flushTBQueue dqueue
99+ -- flush never blocks
100+ case lastMsgs of
101+ [] -> return ()
102+ _ -> do
103+ h <- readIORef ref
104+ BL.hPutStr h $ mconcat lastMsgs
105+ return () -- release will flush and close the handler
106+ FlushMsg -> do
107+ readIORef ref >>= hFlush
108+ recur
109+ ReopenMsg -> do
110+ newH <- newResource
111+ oldH <- atomicModifyIORef' ref (\oldH -> (newH, oldH))
112+ hFlush oldH
113+ hClose oldH
114+ recur
115+ recur
116+ worker :: IO ()
117+ worker = try (bracket ack release go) >>= \case
118+ Left (_ :: SomeException) -> do
119+ threadDelay 100000
120+ -- to not restart the worker function too often
121+ worker
122+ -- We dont want the worker thread become unavailable because
123+ -- of exceptions. E.g. could not create or write the file.
124+ Right () -> return ()
125+ asyncRet <- async worker
126+ return $ FileOwner
127+ { foDataQueune = dqueue
128+ , foControlQueue = cqueue
129+ , foAsync = asyncRet
130+ }
131+
132+fileOwnerControl :: FileOwner -> ControlMsg -> IO ()
133+fileOwnerControl fo msg = do
134+ atomically $ writeTBQueue (foControlQueue fo) msg
135+ when (msg == CloseMsg) $ do
136+ -- Wait for worker thread to finish
137+ void $ waitCatch $ foAsync fo
138+
139+writeFileOwner :: FileOwner -> BL.ByteString -> IO ()
140+writeFileOwner fo bs = do
141+ a <- return $!! bs
142+ -- The deepseq here is to be sure that writing thread will not
143+ -- calculate thunks and will not get into the blackhole or
144+ -- something. Precalculating is the responsibility of the sending
145+ -- thread, because there may be several sending threads and only one
146+ -- writing.
147+ atomically $ writeTBQueue (foDataQueune fo) $!! a
stack.yamlnew file+67
@@ -0,0 +1,67 @@
1+# This file was automatically generated by 'stack init'
2+#
3+# Some commonly used options have been documented as comments in this file.
4+# For advanced use and comprehensive documentation of the format, please see:
5+# https://docs.haskellstack.org/en/stable/yaml_configuration/
6+
7+# Resolver to choose a 'specific' stackage snapshot or a compiler version.
8+# A snapshot resolver dictates the compiler version and the set of packages
9+# to be used for project dependencies. For example:
10+#
11+# resolver: lts-3.5
12+# resolver: nightly-2015-09-21
13+# resolver: ghc-7.10.2
14+#
15+# The location of a snapshot can be provided as a file or url. Stack assumes
16+# a snapshot provided as a file might change, whereas a url resource does not.
17+#
18+# resolver: ./custom-snapshot.yaml
19+# resolver: https://example.com/snapshots/2018-01-01.yaml
20+resolver:
21+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/17/9.yaml
22+
23+# User packages to be built.
24+# Various formats can be used as shown in the example below.
25+#
26+# packages:
27+# - some-directory
28+# - https://example.com/foo/bar/baz-0.0.2.tar.gz
29+# subdirs:
30+# - auto-update
31+# - wai
32+packages:
33+- .
34+# Dependency packages to be pulled from upstream that are not in the resolver.
35+# These entries can reference officially published versions as well as
36+# forks / in-progress versions pinned to a git hash. For example:
37+#
38+# extra-deps:
39+# - acme-missiles-0.3
40+# - git: https://github.com/commercialhaskell/stack.git
41+# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
42+#
43+# extra-deps: []
44+
45+# Override default flag values for local packages and extra-deps
46+# flags: {}
47+
48+# Extra package databases containing global packages
49+# extra-package-dbs: []
50+
51+# Control whether we use the GHC we find on the path
52+# system-ghc: true
53+#
54+# Require a specific version of stack, using version ranges
55+# require-stack-version: -any # Default
56+# require-stack-version: ">=2.5"
57+#
58+# Override the architecture used by stack, especially useful on Windows
59+# arch: i386
60+# arch: x86_64
61+#
62+# Extra directories used by stack for building
63+# extra-include-dirs: [/path/to/dir]
64+# extra-lib-dirs: [/path/to/dir]
65+#
66+# Allow a newer minor version of GHC than the snapshot specifies
67+# compiler-check: newer-minor