git.iko.soy/typeable/control-logger
on master help sign in
11 files changed+348−217
since splitsnapshots
unifiedsplit
.gitignore+3 LICENSE+2 README.md+3 control-logger.cabal+51−41 default.nix+9 src/Control/Logger.hs+75−19 src/Control/Logger/Internal.hs+29−8 src/Control/Logger/Katip.hs+5−130 src/Control/Logger/Katip/Scribes/Reopenable.hs−19 src/Control/Logger/Katip/Utils.hs+124 src/Control/Logger/Orphans.hs+47
.gitignorenew file+3
@@ -0,0 +1,3 @@
1+.stack-work
2+*~
3+*.swp
LICENSEnew file+2
@@ -0,0 +1,2 @@
1+Copyright © 2020, Antorica LLC
2+All rights reserved
README.mdnew file+3
@@ -0,0 +1,3 @@
1+# control-logger
2+
3+Utilities for logging
control-logger.cabal+51−41
@@ -1,45 +1,55 @@
1−name: control-logger
2−version: 0.1.0.0
3−synopsis: Logger used by all projects
4−license: AllRightsReserved
5−build-type: Simple
6−cabal-version: >=1.10
1+cabal-version: 2.4
2+name: control-logger
3+version: 0.1.0.0
4+synopsis: Logger used by all projects
5+license-file: LICENSE
6+build-type: Simple
77
88library
9− hs-source-dirs: src
10− ghc-options: -Wall
9+ hs-source-dirs: src
10+ ghc-options: -Wall
11+
1112 if !impl(ghcjs)
12− exposed-modules: Control.Logger
13− Control.Logger.Katip
14− Control.Logger.Katip.Scribes.Reopenable
15− exposed-modules: Control.Logger.Internal
16− build-depends: base
17− , aeson
18− , bloodhound
19− , bytestring
20− , control-has
21− , deepseq
22− , exceptions
23− , fast-logger
24− , http-client
25− , lens
26− , mtl
27− , shakespeare
28− , text
29− , time
30− , unordered-containers
13+ exposed-modules:
14+ Control.Logger
15+ Control.Logger.Katip
16+ Control.Logger.Katip.Utils
17+ Control.Logger.Orphans
18+
19+ exposed-modules: Control.Logger.Internal
20+ build-depends:
21+ , aeson
22+ , base >=4.11
23+ , bytestring
24+ , control-has
25+ , control-has-katip
26+ , deepseq
27+ , exceptions
28+ , fast-logger >=2.4.11
29+ , http-client
30+ , lens
31+ , monad-logger
32+ , mtl
33+ , shakespeare
34+ , text
35+ , time
36+ , unordered-containers
37+
3138 if !impl(ghcjs)
32− build-depends: katip
33− , katip-elasticsearch
34− default-language: Haskell2010
35− default-extensions: RankNTypes
36− DataKinds
37− FlexibleContexts
38− FlexibleInstances
39− LambdaCase
40− OverloadedStrings
41− ScopedTypeVariables
42− DeriveGeneric
43− ConstraintKinds
44− RecordWildCards
45− TypeSynonymInstances
39+ build-depends:
40+ , katip >=0.8.8.0
41+ , optparse-applicative
42+
43+ default-language: Haskell2010
44+ default-extensions:
45+ ConstraintKinds
46+ DataKinds
47+ DeriveGeneric
48+ FlexibleContexts
49+ FlexibleInstances
50+ LambdaCase
51+ OverloadedStrings
52+ RankNTypes
53+ RecordWildCards
54+ ScopedTypeVariables
55+ TypeSynonymInstances
default.nixnew file+9
@@ -0,0 +1,9 @@
1+{ pkgs ? import <nixpkgs> {} }:
2+
3+pkgs.mkShell {
4+ buildInputs = [
5+ pkgs.ghc
6+ pkgs.haskellPackages.cabal-install
7+ pkgs.zlib
8+ ];
9+}
src/Control/Logger.hs+75−19
@@ -1,7 +1,10 @@
1−
1+{-# LANGUAGE CPP #-}
22module Control.Logger
33 ( Logger
44 , loggerContext
5+ , scrubber
6+ , censor
7+ , censorWith
58 , logMsg
69 , logMsgWith
710 , logInfo
@@ -32,71 +35,123 @@ module Control.Logger
3235 , compactCallStack
3336 , st
3437 , ToObject(..)
38+ , withScrubber
39+ , censoringWith
40+ , censoring
3541 ) where
3642
3743import Control.Has
44+import Control.Lens hiding (censoring)
3845import Control.Logger.Internal
46+import Control.Logger.Orphans ()
47+import Control.Monad
3948import Control.Monad.Catch
40−import qualified Data.List as List
41−import Data.Text (Text)
42−import qualified Data.Text as Text
49+import Control.Monad.IO.Class
50+import qualified Data.List as List
51+import Data.Monoid
52+import Data.Text (Text)
53+import qualified Data.Text as Text
4354import Data.Time
4455import GHC.Stack
45−import Katip (ToObject(..))
56+import Katip (ToObject (..))
4657import System.Log.FastLogger
47−import Text.Shakespeare.Text (st)
58+import Text.Shakespeare.Text (st)
4859
4960logError
5061 :: (LoggingMonad r m)
5162 => Text
5263 -> m ()
53−logError = withFrozenCallStack . logMsg Error
64+logError msg = withFrozenCallStack (logMsg Error msg)
5465
5566logDebug
5667 :: (LoggingMonad r m)
5768 => Text
5869 -> m ()
59−logDebug = withFrozenCallStack . logMsg Debug
70+logDebug msg = withFrozenCallStack (logMsg Debug msg)
6071
6172logInfo
6273 :: (LoggingMonad r m)
6374 => Text
6475 -> m ()
65−logInfo = withFrozenCallStack . logMsg Info
76+logInfo msg = withFrozenCallStack (logMsg Info msg)
6677
6778logWarn
6879 :: (LoggingMonad r m)
6980 => Text
7081 -> m ()
71−logWarn = withFrozenCallStack . logMsg Warn
82+logWarn msg = withFrozenCallStack (logMsg Warn msg)
7283
7384logInfoWith
7485 :: (MonadIO m, HasCallStack)
7586 => Logger
7687 -> Text
7788 -> m ()
78−logInfoWith logger = withFrozenCallStack . logMsgWith logger Info
89+logInfoWith logger msg = withFrozenCallStack (logMsgWith logger Info msg)
7990
8091logDebugWith
8192 :: (MonadIO m, HasCallStack)
8293 => Logger
8394 -> Text
8495 -> m ()
85−logDebugWith logger = withFrozenCallStack . logMsgWith logger Debug
96+logDebugWith logger msg = withFrozenCallStack (logMsgWith logger Debug msg)
8697
8798logWarnWith
8899 :: (MonadIO m, HasCallStack)
89100 => Logger
90101 -> Text
91102 -> m ()
92−logWarnWith logger = withFrozenCallStack . logMsgWith logger Warn
103+logWarnWith logger msg = withFrozenCallStack (logMsgWith logger Warn msg)
93104
94105logErrorWith
95106 :: (MonadIO m, HasCallStack)
96107 => Logger
97108 -> Text
98109 -> m ()
99−logErrorWith logger = withFrozenCallStack . logMsgWith logger Error
110+logErrorWith logger msg = withFrozenCallStack (logMsgWith logger Error msg)
111+
112+safeReplace :: Text -> Text -> Text -> Text
113+safeReplace what with where_
114+ | Text.null what = where_
115+ | otherwise = Text.replace what with where_
116+
117+-- | Add a some text to scrubber
118+censorWith :: Text -> Text -> Logger -> Logger
119+censorWith replacement needle l =
120+ l & scrubber <>~ Endo (safeReplace needle replacement)
121+
122+-- | Censor with "***"
123+censor :: Text -> Logger -> Logger
124+censor = censorWith "***"
125+
126+-- | Run an action with adjusted scrubber
127+withScrubber
128+ :: forall r m a. (Has Logger r, MonadReader r m)
129+ => (Endo Text -> Endo Text)
130+ -> m a
131+ -> m a
132+withScrubber scrubF = local expandScrubber
133+ where
134+ expandScrubber :: r -> r
135+ expandScrubber = over part (over scrubber scrubF)
136+
137+-- | Run an action while censoring additional text from logs
138+censoringWith
139+ :: (Has Logger r, MonadReader r m)
140+ => Text
141+ -> Text
142+ -> m a
143+ -> m a
144+censoringWith replacement txt = withScrubber (<> censored)
145+ where
146+ censored = Endo (safeReplace txt replacement)
147+
148+-- | Run an action while censoring additional text from logs
149+censoring
150+ :: (Has Logger r, MonadReader r m)
151+ => Text
152+ -> m a
153+ -> m a
154+censoring = censoringWith "***"
100155
101156-- | Log exceptions occured in computation and just ignore them.
102157tryLogError
@@ -141,20 +196,21 @@ logExceptions ma = catch ma $ \e -> do
141196 throwM e
142197
143198oneoffLog :: MonadIO m => Logger -> LogSeverity -> Text -> m ()
144−oneoffLog logger s t = flip runReaderT logger $ logMsg s t
199+oneoffLog logger s t =
200+ withFrozenCallStack (flip runReaderT logger $ logMsg s t)
145201
146202fileLogger :: MonadIO m => FilePath -> m Logger
147203fileLogger fp = fastFunc <$> liftIO (newFileLoggerSet defaultBufSize fp)
148204
149205silentLogger :: Logger
150−silentLogger = Logger mempty (\_ _ _ _ -> return ())
206+silentLogger = Logger mempty mempty (\_ _ _ _ -> return ())
151207
152208-- | Logger which flushes buffer after each message. This is useful with
153209-- stderrLogger.
154210flushingFastFunc
155211 :: LogSeverity -> LoggerSet -> Logger
156212flushingFastFunc minLogLevel loggerSet =
157− Logger mempty $ \_ stack logLevel msg ->
213+ Logger mempty mempty $ \_ stack logLevel msg ->
158214 when (logLevel >= minLogLevel) $ do
159215 ts <- getCurrentTime
160216 pushLogStr loggerSet
@@ -178,14 +234,14 @@ flushingStderrLogger =
178234-- | Make our outdated 'Logger' of fast-logger's 'LoggerSet'
179235fastFunc :: LoggerSet -> Logger
180236fastFunc loggerSet =
181− Logger mempty $ \_ _ _ msg -> pushLogStr loggerSet $ toLogStr msg
237+ Logger mempty mempty $ \_ _ _ msg -> pushLogStr loggerSet $ toLogStr msg
182238
183239type LogSource = Text
184240
185241-- | Default logger tests and same things.
186242defaultFastFunc :: LogSource -> LogSeverity -> LoggerSet -> Logger
187243defaultFastFunc loc minLogLevel loggerSet =
188− Logger mempty $ \_ stack s msg -> when (s >= minLogLevel) $ do
244+ Logger mempty mempty $ \_ stack s msg -> when (s >= minLogLevel) $ do
189245 ts <- getCurrentTime
190246 pushLogStr loggerSet $ formatLogStr ts stack loc s (toLogStr msg)
191247
src/Control/Logger/Internal.hs+29−8
@@ -1,9 +1,12 @@
11{-# LANGUAGE TemplateHaskell #-}
2+{-# LANGUAGE NamedFieldPuns #-}
3+{-# LANGUAGE CPP #-}
24
35module Control.Logger.Internal
46 ( Logger(..)
57 , loggerContext
68 , loggerAction
9+ , scrubber
710 , runLogger
811 , LogSeverity(..)
912 , LoggingMonad
@@ -13,10 +16,11 @@ module Control.Logger.Internal
1316
1417import Control.Has
1518import Control.Lens
19+import Control.Monad.IO.Class
1620import Control.DeepSeq
17−import Data.Aeson (ToJSON, FromJSON, Object)
21+import Data.Aeson (ToJSON, FromJSON, Object, Value(..))
1822import Data.Text (Text)
19−import GHC.Generics (Generic)
23+import Data.Monoid
2024import GHC.Stack
2125
2226
@@ -30,23 +34,26 @@ data LogSeverity
3034instance ToJSON LogSeverity
3135instance FromJSON LogSeverity
3236
33−data Logger
34− = Logger
37+data Logger = Logger
3538 { _loggerContext :: Object
39+ , _scrubber :: Endo Text
3640 , _loggerAction :: Object -> CallStack -> LogSeverity -> Text -> IO ()
3741 } deriving Generic
3842
39−instance NFData Logger
43+instance NFData Logger where
44+ rnf Logger{_loggerContext, _scrubber, _loggerAction} =
45+ rnf _loggerContext `deepseq` rwhnf (appEndo _scrubber) `deepseq` rwhnf _loggerAction
4046
4147makeLenses ''Logger
4248
4349instance Semigroup Logger where
44− (Logger ctx1 l1) <> (Logger ctx2 l2) = Logger ctx'
50+ (Logger ctx1 s1 l1) <> (Logger ctx2 s2 l2)
51+ = Logger ctx' (s1 <> s2)
4552 $ \ctx stack s t -> l1 ctx stack s t >> l2 ctx stack s t
4653 where ctx' = ctx1 <> ctx2
4754
4855instance Monoid Logger where
49− mempty = Logger mempty $ \_ _ _ _ -> pure ()
56+ mempty = Logger mempty mempty $ \_ _ _ _ -> pure ()
5057
5158type LoggingMonad r m =
5259 (MonadReader r m, Has Logger r, MonadIO m, HasCallStack)
@@ -70,4 +77,18 @@ logMsgWith logger s t =
7077 liftIO $ runLogger logger callStack s t
7178
7279runLogger :: Logger -> CallStack -> LogSeverity -> Text -> IO ()
73−runLogger (Logger ctx logger) = logger ctx
80+runLogger (Logger ctx theScrubber logger) cs ls txt
81+ = logger (scrubContext theScrubber ctx) cs ls (appEndo theScrubber txt)
82+
83+
84+scrubContext :: Endo Text -> Object -> Object
85+scrubContext theScrubber = fmap doScrub
86+ where
87+ doScrub :: Value -> Value
88+ doScrub = \case
89+ Object object -> Object (fmap doScrub object)
90+ Array array -> Array (fmap doScrub array)
91+ String string -> String (appEndo theScrubber string)
92+ n@Number{} -> n
93+ b@Bool{} -> b
94+ n@Null -> n
src/Control/Logger/Katip.hs+5−130
@@ -2,60 +2,26 @@
22{-# OPTIONS_GHC -fno-warn-orphans #-}
33
44module Control.Logger.Katip
5− ( ElasticServer(..)
6− , logSeverityToKSeverity
7− , registerElastic
8− , maybeElastic
5+ ( logSeverityToKSeverity
96 , KatipContextTState(..)
107 , getKatipLogger
11− , ourFormatter
12− , ourMapping
8+ , ourItemJson
139 , katipIndexNameString
14− , reopenableFileLog
1510 ) where
1611
1712import Control.EnvT
13+import Control.Has.Katip ()
1814import Control.Lens hiding ((.=))
1915import Control.Logger.Internal
20−import Control.Logger.Katip.Scribes.Reopenable
2116import Data.Aeson hiding (Error)
22−import Data.ByteString (ByteString)
23−import qualified Data.HashMap.Strict as HM
24−import Data.Proxy as P
2517import Data.Text (Text)
2618import qualified Data.Text as T
27−import qualified Data.Text.Encoding as T
2819import qualified Data.Text.Lazy.Builder as T.Builder
29−import Database.V5.Bloodhound
3020import Katip as K hiding (logMsg)
31−import Katip.Core (ItemFunc(..), LocJs(..), ProcessIDJs(..))
21+import Katip.Core (LocJs(..), ProcessIDJs(..))
3222import Katip.Monadic (KatipContextTState(..))
33−import Katip.Scribes.ElasticSearch
34−import Katip.Scribes.ElasticSearch.Internal as K
35−import Network.HTTP.Client
3623
3724
38−data ElasticServer = ElasticServer
39− { _esServer :: !(Maybe Server)
40− , _esUsername :: !(Maybe ByteString)
41− , _esPassword :: !(Maybe ByteString)
42− } deriving (Eq, Show)
43−
44−instance FromJSON ElasticServer where
45− parseJSON = withObject "ElasticServer" $ \obj -> ElasticServer
46− <$> (obj .: "server")
47− <*> (fmap T.encodeUtf8 <$> (obj .: "username"))
48− <*> (fmap T.encodeUtf8 <$> (obj .: "password"))
49−
50−instance ToJSON ElasticServer where
51− toJSON es = object
52− [ "server" .= fmap fromServer (_esServer es)
53− , "username" .= fmap T.decodeUtf8 (_esUsername es)
54− , "password" .= fmap T.decodeUtf8 (_esPassword es)
55− ]
56− where
57− fromServer (Server s) = s
58−
5925logSeverityToKSeverity :: LogSeverity -> Severity
6026logSeverityToKSeverity = \case
6127 Debug -> DebugS
@@ -63,91 +29,6 @@ logSeverityToKSeverity = \case
6329 Warn -> WarningS
6430 Error -> ErrorS
6531
66−registerElastic
67− :: Text
68− -- ^ git revision
69− -> Verbosity
70− -> ElasticServer
71− -> PermitFunc
72− -> LogEnv
73− -> IO LogEnv
74−registerElastic gitRev verbosity ElasticServer{..} permitF env =
75− case _esServer of
76− Nothing -> return env
77− Just server -> do
78− elastic <- do
79− mgr <- newManager defaultManagerSettings
80− let
81− bloodhoundEnv = (mkBHEnv server mgr)
82− { bhRequestHook = requestHook }
83− mkEsScribe cfg bloodhoundEnv permitF verbosity
84− registerScribe "elastic" elastic defaultScribeSettings env
85− where
86− cfg = (defaultEsScribeCfgV5 ixName mappingName)
87− { essItemFormatter = ourFormatter gitRev
88− , essIndexMappingValue = ourMapping mappingName }
89− where
90− ixName = IndexName katipIndexNameString
91− mappingName = MappingName "logs"
92− requestHook = return . case (_esUsername, _esPassword) of
93− (Just u, Just p) -> applyBasicAuth u p
94− _ -> id
95−
96−-- | Doubles the value of @msg@ key into the @msg-keyword@ and adds
97−-- the @rev-hash@ value to each record. The @msg-keyword@ is needed to
98−-- group errors by message. The @rev-hash@ is needed for many purposes
99−-- and for grouping as well.
100−ourFormatter
101− :: Text
102− -- ^ git revision string
103− -> Verbosity
104− -> ItemFunc Value
105−ourFormatter gitRev verb = ItemFunc $ \item -> go $ ourItemJson verb item
106− where
107− go :: Value -> Value
108− go = \case
109− Object h -> Object $ setRev $ dupMsg h
110− x -> x -- drop exception here?
111− where
112− setRev :: HM.HashMap Text Value -> HM.HashMap Text Value
113− setRev = HM.insert "rev-hash" (toJSON gitRev)
114− dupMsg :: HM.HashMap Text Value -> HM.HashMap Text Value
115− dupMsg m = HM.alter (const $ HM.lookup "msg" m) "msg-keyword" m
116−
117−ourMapping :: K.MappingName ESV5 -> Value
118−ourMapping mn = object
119− [fromMappingName prx mn .= object ["properties" .= object prs]]
120− where
121− prs =
122− [ unanalyzedString "thread"
123− , unanalyzedString "sev"
124− , unanalyzedString "pid"
125− -- ns is frequently fulltext searched
126− , analyzedString "ns"
127− -- we want message to be fulltext searchable
128− , analyzedString "msg"
129− -- we want to group by the message as well
130− , unanalyzedString "msg-keyword"
131− -- we want to group by revision hash
132− , unanalyzedString "rev-hash"
133− , "loc" .= locType
134− , unanalyzedString "host"
135− , unanalyzedString "env"
136− , "at" .= dateType
137− , unanalyzedString "app"
138− ]
139− prx = P.Proxy :: P.Proxy ESV5
140− unanalyzedString k = k .= unanalyzedStringSpec prx
141− analyzedString k = k .= analyzedStringSpec prx
142− locType = object ["properties" .= object locPairs]
143− locPairs =
144− [ unanalyzedString "loc_pkg"
145− , unanalyzedString "loc_mod"
146− , unanalyzedString "loc_ln"
147− , unanalyzedString "loc_fn"
148− , unanalyzedString "loc_col"
149− ]
150− dateType = object ["format" .= esDateFormat, "type" .= String "date"]
15132
15233katipIndexNameString :: Text
15334katipIndexNameString = "antorica-b2b-logs"
@@ -170,18 +51,12 @@ ourItemJson verb item = formatItem $
17051 , "loc" .= fmap LocJs _itemLoc
17152 ]
17253
173−-- | Return empty server if Nothing
174−maybeElastic :: Maybe ElasticServer -> ElasticServer
175−maybeElastic = \case
176− Nothing -> ElasticServer Nothing Nothing Nothing
177− Just a -> a
178−
17954instance LogItem Object where
18055 payloadKeys V0 _ = SomeKeys []
18156 payloadKeys _ _ = AllKeys
18257
18358getKatipLogger :: KatipContextTState -> Logger
184−getKatipLogger katipCtx = Logger (toObject . ltsContext $ katipCtx)
59+getKatipLogger katipCtx = Logger (toObject . ltsContext $ katipCtx) mempty
18560 $ \ ctx stack s msg ->
18661 let ?callStack = stack
18762 in
src/Control/Logger/Katip/Scribes/Reopenable.hsdeleted−19
@@ -1,19 +0,0 @@
1−module Control.Logger.Katip.Scribes.Reopenable where
2−
3−import Katip
4−import Katip.Scribes.Handle.FileOwner
5−
6−-- | Utility function to create scribe using the 'FileOwner'
7−-- inside. Returns new scribe and action to reopen the log
8−-- file. Reopening is performed asyncronously.
9−reopenableFileLog
10− :: FilePath
11− -> (forall a. LogItem a => ItemFormatter a)
12− -> PermitFunc
13− -> Verbosity
14− -> IO (Scribe, IO ())
15−reopenableFileLog fPath formatter permitF verbosity = do
16− owner <- newFileOwner fPath defaultFileOwnerSettings
17− s <- mkFileOwnerScribeWithFormatter formatter (ColorLog False)
18− owner permitF verbosity
19− return (s, fileOwnerControl owner ReopenMsg)
src/Control/Logger/Katip/Utils.hsnew file+124
@@ -0,0 +1,124 @@
1+{-# LANGUAGE CPP #-}
2+
3+module Control.Logger.Katip.Utils
4+ ( katipIndexNameString
5+ , module Katip.Monadic
6+ , LogFormat(..)
7+ , registerFileScribe
8+ , setLoggingCtx
9+ , getKatipLogger
10+ , logSeverityToKSeverity
11+ ) where
12+
13+import Control.EnvT
14+import Control.Exception
15+import Control.Has
16+import Control.Lens hiding ((.=))
17+import Control.Logger as L
18+import Control.Logger.Internal as L
19+import Control.Logger.Katip as L
20+import Data.Aeson hiding (Error)
21+import Data.Aeson.Text
22+#if MIN_VERSION_aeson(2,0,0)
23+import qualified Data.Aeson.KeyMap as HM
24+import qualified Data.Aeson.Key as HM
25+#else
26+import qualified Data.HashMap.Strict as HM
27+#endif
28+import Data.Text (Text)
29+import qualified Data.Text.Lazy as T
30+import qualified Data.Text.Lazy.Builder as T.Builder
31+import Katip as K hiding (logMsg)
32+import Katip.Monadic (KatipContextTState(..))
33+import qualified Katip.Scribes.Handle as K
34+import System.IO
35+
36+
37+#if MIN_VERSION_aeson(2,0,0)
38+type KeyMap v = HM.KeyMap v
39+textToKey :: Text -> Key
40+textToKey = HM.fromText
41+keyToText :: Key -> Text
42+keyToText = HM.toText
43+#else
44+type KeyMap v = HM.HashMap Text v
45+type Key = Text
46+textToKey :: Text -> Key
47+textToKey = id
48+keyToText :: Key -> Text
49+keyToText = id
50+#endif
51+
52+-- | How to format logs
53+data LogFormat
54+ = LogBracket -- ^ use `bracketFormat`
55+ | LogJson -- ^ use `jsonFormat`
56+ deriving (Eq, Show)
57+
58+registerFileScribe
59+ :: Text -> Verbosity -> LogFormat -> FilePath -> PermitFunc -> [(Text, Text)]
60+ -> LogEnv -> IO LogEnv
61+registerFileScribe gitRev verbosity format filePath permitF additions logenv =
62+ bracketOnError (openFile filePath AppendMode) hClose $ \h -> do
63+ scribe_ <- mkHandleScribeWithFormatter formatter
64+ (ColorLog False) h permitF verbosity
65+ let
66+ fileScribe = scribe_
67+ { scribeFinalizer = scribeFinalizer scribe_ `finally` hClose h
68+ }
69+ registerScribe "file" fileScribe defaultScribeSettings logenv
70+ where
71+ formatter :: forall a. LogItem a => ItemFormatter a
72+ formatter = case format of
73+ LogBracket -> customBracketFormat additions'
74+ LogJson -> customJsonFormat gitRev additions'
75+ additions'
76+ = HM.fromList
77+ . map (\(k, v) -> (textToKey k, String v))
78+ $ additions
79+
80+-- | JSON formatter that adds the specified fields to the output
81+customJsonFormat
82+ :: forall a. LogItem a => Text -> KeyMap Value -> ItemFormatter a
83+customJsonFormat gitRev additions withColor verb i = (mappend "\n") $
84+ -- Here we reimplement the standard formater because we want the
85+ -- additional key/value pairs to be added at the top level.
86+ -- So we can't use the LogWithAdditions like we do for bracket format
87+ T.Builder.fromText $
88+ K.colorBySeverity withColor (_itemSeverity i) $
89+ (T.toStrict . encodeToLazyText) (extend $ ourItemJson verb i)
90+ where
91+ extend (Object o) = Object $ HM.union additions' o
92+ extend v = v
93+ additions' = HM.insert "rev-hash" (toJSON gitRev) additions
94+
95+-- | Bracket formatter that adds the specified fields to the output
96+customBracketFormat
97+ :: forall a. LogItem a => KeyMap Value -> ItemFormatter a
98+customBracketFormat additions withColor verb i =
99+ bracketFormat withColor verb (LogWithAdditions additions <$> i)
100+
101+-- | Log item payload that adds the specified additional key/value pairs
102+-- to the output on at verbosity levels
103+data LogWithAdditions a = LogWithAdditions (KeyMap Value) a
104+ deriving (Show)
105+
106+instance LogItem a => LogItem (LogWithAdditions a) where
107+ payloadKeys v (LogWithAdditions additions a) =
108+ case payloadKeys v a of
109+ AllKeys -> AllKeys
110+ SomeKeys keys -> SomeKeys (map keyToText (HM.keys additions) ++ keys)
111+
112+instance ToObject a => ToObject (LogWithAdditions a) where
113+ toObject (LogWithAdditions additions a) =
114+ HM.union additions (toObject a)
115+
116+setLoggingCtx
117+ :: (Has Logger r, Monad m)
118+ => Object
119+ -> ReaderT r m a
120+ -> ReaderT r m a
121+setLoggingCtx ctx' = local (part %~ go)
122+ where
123+ go (Logger ctx scrbber f) =
124+ Logger (ctx <> ctx') scrbber f
src/Control/Logger/Orphans.hsnew file+47
@@ -0,0 +1,47 @@
1+{-# LANGUAGE ImplicitParams #-}
2+{-# LANGUAGE UndecidableInstances #-}
3+{-# OPTIONS_GHC -Wno-orphans #-}
4+
5+module Control.Logger.Orphans
6+ (
7+ ) where
8+
9+import Control.EnvT
10+import Control.Has
11+import Control.Logger.Internal
12+import Control.Monad.Logger
13+import qualified Data.Text.Encoding as T
14+import GHC.Stack
15+
16+
17+instance (Has Logger r, MonadIO m)
18+ => MonadLogger (EnvT r m) where
19+ monadLoggerLog loc _ level msg =
20+ let ?callStack = freezeCallStack (locToCallStack loc)
21+ in withFrozenCallStack
22+ $ logMsg
23+ (mlLevelToLogSeverity level)
24+ (T.decodeUtf8Lenient . fromLogStr . toLogStr $ msg)
25+
26+locToCallStack :: Loc -> CallStack
27+locToCallStack Loc {..} = fromCallSiteList
28+ [ ( ""
29+ , SrcLoc
30+ { srcLocFile = loc_filename
31+ , srcLocPackage = loc_package
32+ , srcLocModule = loc_module
33+ , srcLocStartLine = fst loc_start
34+ , srcLocStartCol = snd loc_start
35+ , srcLocEndLine = fst loc_end
36+ , srcLocEndCol = snd loc_end
37+ }
38+ )
39+ ]
40+
41+mlLevelToLogSeverity :: LogLevel -> LogSeverity
42+mlLevelToLogSeverity = \case
43+ LevelDebug -> Debug
44+ LevelInfo -> Info
45+ LevelWarn -> Warn
46+ LevelError -> Error
47+ LevelOther _ -> Error