{-# LANGUAGE OverloadedStrings #-} ----------------------------------------------------------------------------- -- -- Module : Network.Gelf -- Copyright : Andy Georges -- License : AllRightsReserved -- -- Maintainer : itkovian@gmail.com -- Stability : experimental -- Portability : -- -- | -- ----------------------------------------------------------------------------- module Network.Gelf ( Network.Gelf.send , encode ) where import Codec.Compression.Zlib (compress) import Control.Arrow (second) import qualified Data.Aeson as A import Data.Bits(shiftL, (.|.)) import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BSL import qualified Data.ByteString.Lazy.Char8 as BSLC import Data.Digest.Pure.MD5 (md5) import Data.Maybe (isJust) import qualified Data.Text as T import qualified Data.Serialize as DS (encode) import Data.Word import Network.BSD (getHostName) import Network.Socket import qualified Network.Socket.ByteString.Lazy as NSBL import System.Time (getClockTime, ClockTime(TOD)) import Network.Gelf.Chunk (split) {- TODO: - * timestamp: specs state microsecond timestamp, so correct this - * wrap this in a monad stack with IO at the bottom, and some static - configuration info on top of that, e.g., for setting the hostname, - the destination port, chunk size, etc. -} catSecondMaybes :: [(a, Maybe b)] -> [(a, b)] catSecondMaybes [] = [] catSecondMaybes ((k, v):vs) = case v of Just v' -> (k,v') : cvs Nothing -> cvs where cvs = catSecondMaybes vs gelfMessage :: T.Text -- ^Short message -> Maybe T.Text -- ^Long message (optional) -> String -- ^Hostname -> Integer -- ^Timestamp -> Maybe T.Text -- ^Filename (optional) -> Maybe Integer -- ^Line number (optional) -> [(T.Text, Maybe T.Text)] -- ^Additional fields -> A.Value gelfMessage shortMessage longMessage hostname timestamp filename lineNumber fields = let loglevel = 1 :: Int allFields = [ ("version", A.toJSON `fmap` Just ("1.0" :: T.Text)) , ("host" , A.toJSON `fmap` Just hostname) , ("short_message", A.toJSON `fmap` Just shortMessage) , ("full_message", A.toJSON `fmap` longMessage) , ("timestamp", A.toJSON `fmap` Just timestamp) , ("level", A.toJSON `fmap` Just loglevel) , ("facility", A.toJSON `fmap` Just ("GELF" :: T.Text)) , ("line", A.toJSON `fmap` Just lineNumber) , ("file", A.toJSON `fmap` Just filename) ] ++ map (second (fmap A.toJSON)) fields in A.object $ catSecondMaybes allFields -- | Encode a log message as a GELF message. -- -- This function wraps a given log message in a GELF structure. It creates the -- JSON object, converts it to a ByteString and GZips the result. -- If the resulting ByteString is longer than the maximal chunk size, -- the GELF message is split up into chunks, each at most chunk size in length. encode :: Int -- ^Maximal chunk size -> T.Text -- ^Short message -> Maybe T.Text -- ^Long message (optional) -> String -- ^Hostname -> Integer -- ^Timestamp -> Maybe T.Text -- ^Filename of the file causing the message, e.g., for debugging purposes -> Maybe Integer -- ^Line number in the file causing the message, e.g., for debugging purposes -> [(T.Text, Maybe T.Text)] -- ^Additional fields -> [BSL.ByteString] -- ^One or more chunks encode chunkSize shortMessage longMessage hostname timestamp filename lineNumber fields = let j = gelfMessage shortMessage longMessage hostname timestamp filename lineNumber fields bs = compress $ A.encode j in if BSL.length bs + 2 < fromIntegral chunkSize then [bs] else split chunkSize id bs where id = foldl1 (\w b -> shiftL w 8 .|. b) . map fromIntegral . BS.unpack . BS.take 8 . DS.encode . md5 . BSLC.pack $ hostname ++ show timestamp :: Word64 -- | Send a log message to a server accepting Graylog2 messages. send :: HostName -- ^Remote hostname of the graylog server -> String -- ^Port number -> Int -- ^Chunk size -> T.Text -- ^Short message -> Maybe T.Text -- ^Long message (optional) -> Maybe T.Text -- ^Filename of the message cause -> Maybe Integer -- ^Line in the file where the message was sent for -> [(T.Text, Maybe T.Text)] -- ^Additional fields (name, information), should not contain 'id' as name -> IO () -- ^Does I/O send serverName serverPort chunkSize shortMessage longMessage filename lineNumber fields = do addressInfos <- getAddrInfo Nothing (Just serverName) (Just serverPort) let serverAddress = head addressInfos -- FIXME: this should handle errors too sock <- socket (addrFamily serverAddress) Datagram defaultProtocol connect sock (addrAddress serverAddress) hostname <- getHostName timestamp <- getClockTime >>= (\(TOD seconds _) -> return seconds) let ms = encode chunkSize shortMessage longMessage hostname timestamp filename lineNumber fields mapM_ (NSBL.send sock) ms