matterhorn-40901.0.0: src/State/Setup/Threads/Logging.hs
{-# LANGUAGE RecordWildCards #-}
-- | This module implements the logging thread. This thread can
-- optionally write logged messages to an output file. When the thread
-- is created, it is initially not writing to any output file and
-- must be told to do so by issuing a LogCommand using the LogManager
-- interface.
--
-- The logging thread has an internal bounded log message buffer. Logged
-- messages always get written to the buffer (potentially evicting old
-- messages to maintain the size bound). If the thread is also writing
-- to a file, such messages also get written to the file. When the
-- thread begins logging to a file, the entire buffer is written to the
-- file so that a historical snapshot of log activity can be saved in
-- cases where logging is turned on at runtime only once a problematic
-- behavior is observed.
module State.Setup.Threads.Logging
( newLogManager
, shutdownLogManager
)
where
import Prelude ()
import Prelude.MH
import Brick.BChan
import Control.Concurrent.Async ( Async, async, wait )
import qualified Control.Concurrent.STM as STM
import Control.Exception ( SomeException, try )
import Control.Monad.State.Strict
import qualified Data.Sequence as Seq
import qualified Data.Text as T
import Data.Time ( getCurrentTime )
import System.IO ( Handle, IOMode(AppendMode), hPutStr, hPutStrLn
, hFlush, openFile, hClose )
import Types
-- | The state of the logging thread.
data LogThreadState =
LogThreadState { logThreadDestination :: Maybe (FilePath, Handle)
-- ^ The logging thread's active logging destination.
-- Nothing means log messages are not being written
-- anywhere except the internal buffer.
, logThreadEventChan :: BChan MHEvent
-- ^ The application event channel that we'll use to
-- notify of logging events.
, logThreadCommandChan :: STM.TChan LogCommand
-- ^ The channel on which the logging thread will
-- wait for logging commands.
, logThreadMessageBuffer :: Seq.Seq LogMessage
-- ^ The internal bounded log message buffer.
, logThreadMaxBufferSize :: Int
-- ^ The size bound of the logThreadMessageBuffer.
}
-- | Create a new log manager and start a logging thread for it.
newLogManager :: BChan MHEvent -> Int -> IO LogManager
newLogManager eventChan maxBufferSize = do
chan <- STM.newTChanIO
self <- startLoggingThread eventChan chan maxBufferSize
let mgr = LogManager { logManagerCommandChannel = chan
, logManagerHandle = self
}
return mgr
-- | Shuts down the log manager and blocks until shutdown is complete.
shutdownLogManager :: LogManager -> IO ()
shutdownLogManager mgr = do
STM.atomically $ STM.writeTChan (logManagerCommandChannel mgr) ShutdownLogging
wait $ logManagerHandle mgr
-- | The logging thread.
startLoggingThread :: BChan MHEvent -> STM.TChan LogCommand -> Int -> IO (Async ())
startLoggingThread eventChan logChan maxBufferSize = do
let initialState = LogThreadState { logThreadDestination = Nothing
, logThreadEventChan = eventChan
, logThreadCommandChan = logChan
, logThreadMessageBuffer = mempty
, logThreadMaxBufferSize = maxBufferSize
}
async $ void $ runStateT logThreadBody initialState
logThreadBody :: StateT LogThreadState IO ()
logThreadBody = do
cmd <- nextLogCommand
continue <- handleLogCommand cmd
when continue logThreadBody
-- | Get the next pending log thread command.
nextLogCommand :: StateT LogThreadState IO LogCommand
nextLogCommand = do
chan <- gets logThreadCommandChan
liftIO $ STM.atomically $ STM.readTChan chan
putMarkerMessage :: String -> Handle -> IO ()
putMarkerMessage msg h = do
now <- getCurrentTime
hPutStrLn h $ "[" <> show now <> "] " <> msg
-- | Emit a log stop marker to the file.
putLogEndMarker :: Handle -> IO ()
putLogEndMarker = putMarkerMessage "<<< Logging end >>>"
-- | Emit a log start marker to the file.
putLogStartMarker :: Handle -> IO ()
putLogStartMarker = putMarkerMessage "<<< Logging start >>>"
-- | Emit a log stop marker to the file and close it, then notify the
-- application that we have stopped logging.
finishLog :: BChan MHEvent -> FilePath -> Handle -> StateT LogThreadState IO ()
finishLog eventChan oldPath oldHandle = do
liftIO $ do
putLogEndMarker oldHandle
hClose oldHandle
writeBChan eventChan $ IEvent $ LoggingStopped oldPath
modify $ \s -> s { logThreadDestination = Nothing }
stopLogging :: StateT LogThreadState IO ()
stopLogging = do
oldDest <- gets logThreadDestination
case oldDest of
Nothing -> return ()
Just (oldPath, oldHandle) -> do
eventChan <- gets logThreadEventChan
finishLog eventChan oldPath oldHandle
-- | Handle a single logging command.
handleLogCommand :: LogCommand -> StateT LogThreadState IO Bool
handleLogCommand (LogSnapshot path) = do
-- LogSnapshot: write the current log message buffer to the
-- specified path. Ignore the request if it is for the path that we
-- are already logging to.
eventChan <- gets logThreadEventChan
dest <- gets logThreadDestination
let shouldWrite = case dest of
Nothing -> True
Just (curPath, _) -> curPath /= path
when shouldWrite $ do
result <- liftIO $ try $ openFile path AppendMode
case result of
Left (e::SomeException) -> do
liftIO $ writeBChan eventChan $ IEvent $ LogSnapshotFailed path (show e)
Right handle -> do
flushLogMessageBuffer handle
liftIO $ hClose handle
liftIO $ writeBChan eventChan $ IEvent $ LogSnapshotSucceeded path
return True
handleLogCommand GetLogDestination = do
-- GetLogDestination: the application asked us to provide the
-- current log destination.
dest <- gets logThreadDestination
eventChan <- gets logThreadEventChan
liftIO $ writeBChan eventChan $ IEvent $ LogDestination $ fst <$> dest
return True
handleLogCommand ShutdownLogging = do
-- ShutdownLogging: if we were logging to a file, close it. Then
-- unlock the shutdown lock.
stopLogging
return False
handleLogCommand StopLogging = do
-- StopLogging: if we were logging to a file, close it and notify
-- the application. Otherwise do nothing.
stopLogging
return True
handleLogCommand (LogToFile newPath) = do
-- LogToFile: if we were logging to a file, close that file, notify
-- the application, then attempt to open the new file. If that
-- failed, notify the application of the error. If it succeeded,
-- start logging and notify the application.
eventChan <- gets logThreadEventChan
oldDest <- gets logThreadDestination
shouldChange <- case oldDest of
Nothing ->
return True
Just (oldPath, _) ->
return (oldPath /= newPath)
when shouldChange $ do
result <- liftIO $ try $ openFile newPath AppendMode
case result of
Left (e::SomeException) -> liftIO $ do
let msg = "Error in log thread: could not open " <> show newPath <>
": " <> show e
writeBChan eventChan $ IEvent $ LogStartFailed newPath msg
Right handle -> do
stopLogging
modify $ \s -> s { logThreadDestination = Just (newPath, handle) }
flushLogMessageBuffer handle
liftIO $ putLogStartMarker handle
liftIO $ writeBChan eventChan $ IEvent $ LoggingStarted newPath
return True
handleLogCommand (LogAMessage lm) = do
-- LogAMessage: log a single message. Write the message to the
-- bounded internal buffer (which may cause an older message to be
-- evicted). Then, if we are actively logging to a file, write the
-- message to that file and flush the output stream.
maxBufSize <- gets logThreadMaxBufferSize
let addMessageToBuffer s =
-- Ensure that newSeq is always at most maxBufSize elements
-- long.
let newSeq = s Seq.|> lm
toDrop = Seq.length s - maxBufSize
in Seq.drop toDrop newSeq
-- Append the message to the internal buffer, maintaining the bound
-- on the internal buffer size.
modify $ \s -> s { logThreadMessageBuffer = addMessageToBuffer (logThreadMessageBuffer s) }
-- If we have an active log destination, write the message to the
-- output file.
dest <- gets logThreadDestination
case dest of
Nothing -> return ()
Just (_, handle) -> liftIO $ do
hPutLogMessage handle lm
hFlush handle
return True
-- | Write a single log message to the output handle.
hPutLogMessage :: Handle -> LogMessage -> IO ()
hPutLogMessage handle (LogMessage {..}) = do
hPutStr handle $ "[" <> show logMessageTimestamp <> "] "
hPutStr handle $ "[" <> show logMessageCategory <> "] "
case logMessageContext of
Nothing -> hPutStr handle "[No context] "
Just c -> hPutStr handle $ "[" <> show c <> "] "
hPutStrLn handle $ T.unpack logMessageText
-- | Flush the contents of the internal log message buffer.
flushLogMessageBuffer :: Handle -> StateT LogThreadState IO ()
flushLogMessageBuffer handle = do
buf <- gets logThreadMessageBuffer
when (Seq.length buf > 0) $ do
liftIO $ do
let firstLm = Seq.index buf 0
lastLm = Seq.index buf (Seq.length buf - 1)
mkMsg t m = "[" <> show t <> "] " <> m
hPutStrLn handle $ mkMsg (logMessageTimestamp firstLm)
"<<< Log message buffer begin >>>"
forM_ buf (hPutLogMessage handle)
hPutStrLn handle $ mkMsg (logMessageTimestamp lastLm)
"<<< Log message buffer end >>>"
hFlush handle