-
-
Notifications
You must be signed in to change notification settings - Fork 389
/
Copy pathLogger.hs
283 lines (248 loc) · 11.5 KB
/
Logger.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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
-- Copyright (c) 2019 The DAML Authors. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
-- | This is a compatibility module that abstracts over the
-- concrete choice of logging framework so users can plug in whatever
-- framework they want to.
module Ide.Logger
( Priority(..)
, Recorder(..)
, WithPriority(..)
, logWith
, cmap
, cmapIO
, cfilter
, withFileRecorder
, makeDefaultStderrRecorder
, makeDefaultHandleRecorder
, LoggingColumn(..)
, cmapWithPrio
, withBacklog
, lspClientMessageRecorder
, lspClientLogRecorder
, module PrettyPrinterModule
, renderStrict
, toCologActionWithPrio
, defaultLoggingColumns
) where
import Colog.Core (LogAction (..), Severity,
WithSeverity (..))
import qualified Colog.Core as Colog
import Control.Concurrent (myThreadId)
import Control.Concurrent.Extra (Lock, newLock, withLock)
import Control.Concurrent.STM (atomically, flushTBQueue,
isFullTBQueue, newTBQueueIO,
newTVarIO, readTVarIO,
writeTBQueue, writeTVar)
import Control.Exception (IOException)
import Control.Monad (unless, when, (>=>))
import Control.Monad.IO.Class (MonadIO (liftIO))
import Data.Foldable (for_)
import Data.Functor.Contravariant (Contravariant (contramap))
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import Data.Time (defaultTimeLocale, formatTime,
getCurrentTime)
import GHC.Stack (CallStack, HasCallStack,
SrcLoc (SrcLoc, srcLocModule, srcLocStartCol, srcLocStartLine),
callStack, getCallStack,
withFrozenCallStack)
import Language.LSP.Protocol.Message (SMethod (SMethod_WindowLogMessage, SMethod_WindowShowMessage))
import Language.LSP.Protocol.Types (LogMessageParams (..),
MessageType (..),
ShowMessageParams (..))
import Language.LSP.Server
import qualified Language.LSP.Server as LSP
import Prettyprinter as PrettyPrinterModule
import Prettyprinter.Render.Text (renderStrict)
import System.IO (Handle, IOMode (AppendMode),
hClose, hFlush, openFile,
stderr)
import UnliftIO (MonadUnliftIO, finally, try)
data Priority
-- Don't change the ordering of this type or you will mess up the Ord
-- instance
= Debug -- ^ Verbose debug logging.
| Info -- ^ Useful information in case an error has to be understood.
| Warning
-- ^ These error messages should not occur in a expected usage, and
-- should be investigated.
| Error -- ^ Such log messages must never occur in expected usage.
deriving (Eq, Show, Read, Ord, Enum, Bounded)
data WithPriority a = WithPriority { priority :: Priority, callStack_ :: CallStack, payload :: a } deriving Functor
-- | Note that this is logging actions _of the program_, not of the user.
-- You shouldn't call warning/error if the user has caused an error, only
-- if our code has gone wrong and is itself erroneous (e.g. we threw an exception).
newtype Recorder msg = Recorder
{ logger_ :: forall m. (MonadIO m) => msg -> m () }
logWith :: (HasCallStack, MonadIO m) => Recorder (WithPriority msg) -> Priority -> msg -> m ()
logWith recorder priority msg = withFrozenCallStack $ logger_ recorder (WithPriority priority callStack msg)
instance Semigroup (Recorder msg) where
(<>) Recorder{ logger_ = logger_1 } Recorder{ logger_ = logger_2 } =
Recorder
{ logger_ = \msg -> logger_1 msg >> logger_2 msg }
instance Monoid (Recorder msg) where
mempty =
Recorder
{ logger_ = \_ -> pure () }
instance Contravariant Recorder where
contramap f Recorder{ logger_ } =
Recorder
{ logger_ = logger_ . f }
cmap :: (a -> b) -> Recorder b -> Recorder a
cmap = contramap
cmapWithPrio :: (a -> b) -> Recorder (WithPriority b) -> Recorder (WithPriority a)
cmapWithPrio f = cmap (fmap f)
cmapIO :: (a -> IO b) -> Recorder b -> Recorder a
cmapIO f Recorder{ logger_ } =
Recorder
{ logger_ = (liftIO . f) >=> logger_ }
cfilter :: (a -> Bool) -> Recorder a -> Recorder a
cfilter p Recorder{ logger_ } =
Recorder
{ logger_ = \msg -> when (p msg) (logger_ msg) }
textHandleRecorder :: Handle -> Recorder Text
textHandleRecorder handle =
Recorder
{ logger_ = \text -> liftIO $ Text.hPutStrLn handle text *> hFlush handle }
makeDefaultStderrRecorder :: MonadIO m => Maybe [LoggingColumn] -> m (Recorder (WithPriority (Doc a)))
makeDefaultStderrRecorder columns = do
lock <- liftIO newLock
makeDefaultHandleRecorder columns lock stderr
withFileRecorder
:: MonadUnliftIO m
=> FilePath
-- ^ Log file path.
-> Maybe [LoggingColumn]
-- ^ logging columns to display. `Nothing` uses `defaultLoggingColumns`
-> (Either IOException (Recorder (WithPriority (Doc d))) -> m a)
-- ^ action given a recorder, or the exception if we failed to open the file
-> m a
withFileRecorder path columns action = do
lock <- liftIO newLock
let makeHandleRecorder = makeDefaultHandleRecorder columns lock
fileHandle :: Either IOException Handle <- liftIO $ try (openFile path AppendMode)
case fileHandle of
Left e -> action $ Left e
Right fileHandle -> finally (makeHandleRecorder fileHandle >>= action . Right) (liftIO $ hClose fileHandle)
makeDefaultHandleRecorder
:: MonadIO m
=> Maybe [LoggingColumn]
-- ^ built-in logging columns to display. Nothing uses the default
-> Lock
-- ^ lock to take when outputting to handle
-> Handle
-- ^ handle to output to
-> m (Recorder (WithPriority (Doc a)))
makeDefaultHandleRecorder columns lock handle = do
let Recorder{ logger_ } = textHandleRecorder handle
let threadSafeRecorder = Recorder { logger_ = \msg -> liftIO $ withLock lock (logger_ msg) }
let loggingColumns = fromMaybe defaultLoggingColumns columns
let textWithPriorityRecorder = cmapIO (textWithPriorityToText loggingColumns) threadSafeRecorder
pure (cmap docToText textWithPriorityRecorder)
where
docToText = fmap (renderStrict . layoutPretty defaultLayoutOptions)
data LoggingColumn
= TimeColumn
| ThreadIdColumn
| PriorityColumn
| DataColumn
| SourceLocColumn
defaultLoggingColumns :: [LoggingColumn]
defaultLoggingColumns = [TimeColumn, PriorityColumn, DataColumn]
textWithPriorityToText :: [LoggingColumn] -> WithPriority Text -> IO Text
textWithPriorityToText columns WithPriority{ priority, callStack_, payload } = do
textColumns <- mapM loggingColumnToText columns
pure $ Text.intercalate " | " textColumns
where
showAsText :: Show a => a -> Text
showAsText = Text.pack . show
utcTimeToText utcTime = Text.pack $ formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%S%6QZ" utcTime
priorityToText :: Priority -> Text
priorityToText = showAsText
threadIdToText = showAsText
callStackToSrcLoc :: CallStack -> Maybe SrcLoc
callStackToSrcLoc callStack =
case getCallStack callStack of
(_, srcLoc) : _ -> Just srcLoc
_ -> Nothing
srcLocToText = \case
Nothing -> "<unknown>"
Just SrcLoc{ srcLocModule, srcLocStartLine, srcLocStartCol } ->
Text.pack srcLocModule <> "#" <> showAsText srcLocStartLine <> ":" <> showAsText srcLocStartCol
loggingColumnToText :: LoggingColumn -> IO Text
loggingColumnToText = \case
TimeColumn -> do
utcTime <- getCurrentTime
pure (utcTimeToText utcTime)
SourceLocColumn -> pure $ (srcLocToText . callStackToSrcLoc) callStack_
ThreadIdColumn -> do
threadId <- myThreadId
pure (threadIdToText threadId)
PriorityColumn -> pure (priorityToText priority)
DataColumn -> pure payload
-- | Given a 'Recorder' that requires an argument, produces a 'Recorder'
-- that queues up messages until the argument is provided using the callback, at which
-- point it sends the backlog and begins functioning normally.
withBacklog :: (v -> Recorder a) -> IO (Recorder a, v -> IO ())
withBacklog recFun = do
-- Arbitrary backlog capacity
backlog <- newTBQueueIO 100
let backlogRecorder = Recorder $ \it -> liftIO $ atomically $ do
-- If the queue is full just drop the message on the floor. This is most likely
-- to happen if the callback is just never going to be called; in which case
-- we want neither to build up an unbounded backlog in memory, nor block waiting
-- for space!
full <- isFullTBQueue backlog
unless full $ writeTBQueue backlog it
-- The variable holding the recorder starts out holding the recorder that writes
-- to the backlog.
recVar <- newTVarIO backlogRecorder
-- The callback atomically swaps out the recorder for the final one, and flushes
-- the backlog to it.
let cb arg = do
let recorder = recFun arg
toRecord <- atomically $ writeTVar recVar recorder >> flushTBQueue backlog
for_ toRecord (logger_ recorder)
-- The recorder we actually return looks in the variable and uses whatever is there.
let varRecorder = Recorder $ \it -> do
r <- liftIO $ readTVarIO recVar
logger_ r it
pure (varRecorder, cb)
-- | Creates a recorder that sends logs to the LSP client via @window/showMessage@ notifications.
lspClientMessageRecorder :: LanguageContextEnv config -> Recorder (WithPriority Text)
lspClientMessageRecorder env = Recorder $ \WithPriority {..} ->
liftIO $ LSP.runLspT env $ LSP.sendNotification SMethod_WindowShowMessage
ShowMessageParams
{ _type_ = priorityToLsp priority,
_message = payload
}
-- | Creates a recorder that sends logs to the LSP client via @window/logMessage@ notifications.
lspClientLogRecorder :: LanguageContextEnv config -> Recorder (WithPriority Text)
lspClientLogRecorder env = Recorder $ \WithPriority {..} ->
liftIO $ LSP.runLspT env $ LSP.sendNotification SMethod_WindowLogMessage
LogMessageParams
{ _type_ = priorityToLsp priority,
_message = payload
}
priorityToLsp :: Priority -> MessageType
priorityToLsp =
\case
Debug -> MessageType_Log
Info -> MessageType_Info
Warning -> MessageType_Warning
Error -> MessageType_Error
toCologActionWithPrio :: (MonadIO m, HasCallStack) => Recorder (WithPriority msg) -> LogAction m (WithSeverity msg)
toCologActionWithPrio (Recorder _logger) = LogAction $ \WithSeverity{..} -> do
let priority = severityToPriority getSeverity
_logger $ WithPriority priority callStack getMsg
where
severityToPriority :: Severity -> Priority
severityToPriority Colog.Debug = Debug
severityToPriority Colog.Info = Info
severityToPriority Colog.Warning = Warning
severityToPriority Colog.Error = Error