module Control.Distributed.Process.Extras.SystemLog
(
LogLevel(..)
, LogFormat
, LogClient
, LogChan
, LogText
, ToLog(..)
, Logger(..)
, mxLogId
, systemLog
, client
, logChannel
, addFormatter
, systemLogFile
, report
, debug
, info
, notice
, warning
, error
, critical
, alert
, emergency
, sendLog
) where
import Control.DeepSeq (NFData(..))
import Control.Distributed.Process hiding (catch)
import Control.Distributed.Process.Management
( MxEvent(MxConnected, MxDisconnected, MxLog, MxUser)
, MxAgentId(..)
, mxAgentWithFinalize
, mxSink
, mxReady
, mxReceive
, liftMX
, mxGetLocal
, mxSetLocal
, mxUpdateLocal
, mxNotify
)
import Control.Distributed.Process.Extras
( Resolvable(..)
, Routable(..)
, Addressable
)
import Control.Distributed.Process.Serializable
import Control.Exception (SomeException)
import Control.Monad.Catch (catch)
import Data.Binary
import Data.Typeable (Typeable)
import GHC.Generics
#if ! MIN_VERSION_base(4,6,0)
import Prelude hiding (catch, error, Read)
#else
import Prelude hiding (error, Read)
#endif
import System.IO
( IOMode(AppendMode)
, BufferMode(..)
, openFile
, hClose
, hPutStrLn
, hSetBuffering
)
import Text.Read (Read)
data LogLevel =
Debug
| Info
| Notice
| Warning
| Error
| Critical
| Alert
| Emergency
deriving (Typeable, Generic, Eq,
Read, Show, Ord, Enum)
instance Binary LogLevel where
instance NFData LogLevel where rnf x = x `seq` ()
data SetLevel = SetLevel !LogLevel
deriving (Typeable, Generic)
instance Binary SetLevel where
instance NFData SetLevel where rnf x = x `seq` ()
newtype AddFormatter = AddFormatter (Closure (Message -> Process (Maybe String)))
deriving (Typeable, Generic, NFData)
instance Binary AddFormatter
data LogState =
LogState { output :: !(String -> Process ())
, cleanup :: !(Process ())
, level :: !LogLevel
, format :: !(String -> Process String)
, formatters :: ![Message -> Process (Maybe String)]
}
data LogMessage =
LogMessage !String !LogLevel
| LogData !Message !LogLevel
deriving (Typeable, Generic, Show)
instance Binary LogMessage where
instance NFData LogMessage where rnf x = x `seq` ()
type LogFormat = String -> Process String
type LogChanT = ()
newtype LogChan = LogChan LogChanT
instance Routable LogChan where
sendTo _ = mxNotify
unsafeSendTo _ = mxNotify
type LogText = String
newtype LogClient = LogClient { agent :: ProcessId }
instance Resolvable LogClient where
resolve = return . Just . agent
instance Routable LogClient
class ToLog m where
toLog :: (Serializable m) => m -> Process (LogLevel -> LogMessage)
toLog = return . LogData . unsafeWrapMessage
instance ToLog LogText where
toLog = return . LogMessage
instance ToLog Message where
toLog = return . LogData
class Logger a where
logMessage :: a -> LogMessage -> Process ()
instance Logger LogClient where
logMessage = sendTo
instance Logger LogChan where
logMessage _ = mxNotify
logProcessName :: String
logProcessName = "service.systemlog"
mxLogId :: MxAgentId
mxLogId = MxAgentId logProcessName
logChannel :: LogChan
logChannel = LogChan ()
report :: (Logger l)
=> (l -> LogText -> Process ())
-> l
-> String
-> Process ()
report f l = f l
client :: Process (Maybe LogClient)
client = resolve logProcessName >>= return . maybe Nothing (Just . LogClient)
debug :: (Logger l, Serializable m, ToLog m) => l -> m -> Process ()
debug l m = sendLog l m Debug
info :: (Logger l, Serializable m, ToLog m) => l -> m -> Process ()
info l m = sendLog l m Info
notice :: (Logger l, Serializable m, ToLog m) => l -> m -> Process ()
notice l m = sendLog l m Notice
warning :: (Logger l, Serializable m, ToLog m) => l -> m -> Process ()
warning l m = sendLog l m Warning
error :: (Logger l, Serializable m, ToLog m) => l -> m -> Process ()
error l m = sendLog l m Error
critical :: (Logger l, Serializable m, ToLog m) => l -> m -> Process ()
critical l m = sendLog l m Critical
alert :: (Logger l, Serializable m, ToLog m) => l -> m -> Process ()
alert l m = sendLog l m Alert
emergency :: (Logger l, Serializable m, ToLog m) => l -> m -> Process ()
emergency l m = sendLog l m Emergency
sendLog :: (Logger l, Serializable m, ToLog m) => l -> m -> LogLevel -> Process ()
sendLog a m lv = toLog m >>= \m' -> logMessage a $ m' lv
addFormatter :: (Addressable r)
=> r
-> Closure (Message -> Process (Maybe String))
-> Process ()
addFormatter r clj = sendTo r $ AddFormatter clj
systemLogFile :: FilePath -> LogLevel -> LogFormat -> Process ProcessId
systemLogFile path lvl fmt = do
h <- liftIO $ openFile path AppendMode
liftIO $ hSetBuffering h LineBuffering
systemLog (liftIO . hPutStrLn h) (liftIO (hClose h)) lvl fmt
systemLog :: (String -> Process ())
-> (Process ())
-> LogLevel
-> LogFormat
-> Process ProcessId
systemLog o c l f = go $ LogState o c l f defaultFormatters
where
go :: LogState -> Process ProcessId
go st =
mxAgentWithFinalize mxLogId st [
(mxSink $ \(m :: LogMessage) ->
case m of
(LogMessage msg lvl) ->
mxGetLocal >>= outputMin lvl msg >> mxReceive
(LogData dat lvl) -> handleRawMsg dat lvl)
, (mxSink $ \(ev :: MxEvent) ->
case ev of
(MxUser msg) -> handleRawMsg msg Debug
(MxLog str) -> mxGetLocal >>= outputMin Debug str >> mxReceive
_ -> handleEvent ev >> mxReceive)
, (mxSink $ \(SetLevel lvl) ->
mxGetLocal >>= \st' -> mxSetLocal st' { level = lvl } >> mxReceive)
, (mxSink $ \(AddFormatter f') -> do
fmt <- liftMX $ catch (unClosure f' >>= return . Just)
(\(_ :: SomeException) -> return Nothing)
case fmt of
Nothing -> mxReady
Just mf -> do
mxUpdateLocal (\s -> s { formatters = mf:formatters s })
mxReceive)
] runCleanup
runCleanup = liftMX . cleanup =<< mxGetLocal
handleRawMsg dat' lvl' = do
st <- mxGetLocal
msg <- formatMsg dat' st
case msg of
Just str -> outputMin lvl' str st >> mxReceive
Nothing -> mxReceive
handleEvent (MxConnected _ ep) =
mxGetLocal >>= outputMin Notice
("Endpoint: " ++ show ep ++ " Disconnected")
handleEvent (MxDisconnected _ ep) =
mxGetLocal >>= outputMin Notice
("Endpoint " ++ show ep ++ " Connected")
handleEvent _ = return ()
formatMsg m LogState{..} = let fms = formatters in formatMsg' m fms
formatMsg' _ [] = return Nothing
formatMsg' m (f':fs) = do
res <- liftMX $ f' m
case res of
ok@(Just _) -> return ok
Nothing -> formatMsg' m fs
outputMin minLvl msgData LogState{..} =
case minLvl >= level of
True -> liftMX (format msgData >>= output)
False -> return ()
defaultFormatters = [basicDataFormat]
basicDataFormat :: Message -> Process (Maybe String)
basicDataFormat = unwrapMessage