/
Logging.hs
155 lines (138 loc) · 4.63 KB
/
Logging.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Adapter module to the actual logging framework.
-- All Hydra node components implements /Structured logging/ via [contra-tracer](https://hackage.haskell.org/package/contra-tracer)
-- generic logging framework. All logs are output in [JSON](https://www.json.org/json-en.html) in a format which is
-- documented in a [JSON-Schema](https://github.com/input-output-hk/hydra/blob/master/hydra-node/json-schemas/logs.yaml).
module Hydra.Logging (
-- * Tracer
Tracer (..),
natTracer,
nullTracer,
traceWith,
ToObject (..),
TracingVerbosity (..),
-- * Using it
Verbosity (..),
Envelope (..),
withTracer,
withTracerOutputTo,
showLogsOnFailure,
traceInTVar,
contramap,
) where
import Hydra.Prelude
import Cardano.BM.Tracing (ToObject (..), TracingVerbosity (..))
import Control.Monad.Class.MonadFork (myThreadId)
import Control.Monad.Class.MonadSTM (
flushTBQueue,
modifyTVar,
newTBQueueIO,
newTVarIO,
readTBQueue,
readTVarIO,
writeTBQueue,
)
import Control.Monad.Class.MonadSay (MonadSay, say)
import Control.Tracer (
Tracer (..),
natTracer,
nullTracer,
traceWith,
)
import Data.Aeson (pairs, (.=))
import qualified Data.Aeson as Aeson
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text as Text
import Test.QuickCheck.Instances.Text ()
import Test.QuickCheck.Instances.Time ()
data Verbosity = Quiet | Verbose Text
deriving (Eq, Show, Generic, ToJSON, FromJSON)
-- | Provides logging metadata for entries.
data Envelope a = Envelope
{ timestamp :: UTCTime
, threadId :: Int
, namespace :: Text
, message :: a
}
deriving (Eq, Show, Generic, FromJSON)
instance ToJSON a => ToJSON (Envelope a) where
toEncoding Envelope{timestamp, threadId, namespace, message} =
pairs $
mconcat
[ "timestamp" .= timestamp
, "threadId" .= threadId
, "namespace" .= namespace
, "message" .= message
]
instance Arbitrary a => Arbitrary (Envelope a) where
arbitrary = genericArbitrary
defaultQueueSize :: Natural
defaultQueueSize = 500
-- | Start logging thread and acquire a 'Tracer'. This tracer will dump all
-- messsages on @stdout@, one message per line, formatted as JSON. This tracer
-- is wrapping 'msg' into an 'Envelope' with metadata.
withTracer ::
forall m msg a.
(MonadIO m, MonadFork m, MonadTime m, ToJSON msg) =>
Verbosity ->
(Tracer m msg -> IO a) ->
IO a
withTracer Quiet = ($ nullTracer)
withTracer (Verbose namespace) = withTracerOutputTo stdout namespace
-- | Start logging thread acquiring a 'Tracer', outputting JSON formatted
-- messages to some 'Handle'. This tracer is wrapping 'msg' into an 'Envelope'
-- with metadata.
withTracerOutputTo ::
forall m msg a.
(MonadIO m, MonadFork m, MonadTime m, ToJSON msg) =>
Handle ->
Text ->
(Tracer m msg -> IO a) ->
IO a
withTracerOutputTo hdl namespace action = do
msgQueue <- newTBQueueIO @_ @(Envelope msg) defaultQueueSize
withAsync (writeLogs msgQueue `finally` flushLogs msgQueue) $ \_ ->
action (tracer msgQueue) `finally` flushLogs msgQueue
where
tracer queue =
Tracer $
mkEnvelope namespace >=> liftIO . atomically . writeTBQueue queue
writeLogs queue =
forever $ do
atomically (readTBQueue queue) >>= write . Aeson.encode
hFlush hdl
flushLogs queue = liftIO $ do
entries <- atomically $ flushTBQueue queue
forM_ entries (write . Aeson.encode)
hFlush hdl
write bs = LBS.hPut hdl (bs <> "\n")
-- | Capture logs and output them to stdout when an exception was raised by the
-- given 'action'. This tracer is wrapping 'msg' into an 'Envelope' with
-- metadata.
showLogsOnFailure ::
(MonadSTM m, MonadCatch m, MonadFork m, MonadTime m, MonadSay m, ToJSON msg) =>
(Tracer m msg -> m a) ->
m a
showLogsOnFailure action = do
tvar <- newTVarIO []
action (traceInTVar tvar)
`onException` (readTVarIO tvar >>= mapM_ (say . decodeUtf8 . Aeson.encode) . reverse)
traceInTVar ::
(MonadFork m, MonadTime m, MonadSTM m) =>
TVar m [Envelope msg] ->
Tracer m msg
traceInTVar tvar = Tracer $ \msg -> do
envelope <- mkEnvelope "" msg
atomically $ modifyTVar tvar (envelope :)
-- * Internal functions
mkEnvelope :: (MonadFork m, MonadTime m) => Text -> msg -> m (Envelope msg)
mkEnvelope namespace message = do
timestamp <- getCurrentTime
threadId <- mkThreadId <$> myThreadId
pure $ Envelope{namespace, timestamp, threadId, message}
where
-- NOTE(AB): This is a bit contrived but we want a numeric threadId and we
-- get some text which we know the structure of
mkThreadId = fromMaybe 0 . readMaybe . Text.unpack . Text.drop 9 . show