{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
module Arbor.Network.StatsD.Datadog
(
Z.DogStatsSettings(..)
, defaultSettings
, createStatsClient
, closeStatsClient
, send
, sendSampled
, sendEvt
, metric
, Z.Metric
, Z.MetricName
, Z.MetricType
, event
, Z.Event
, serviceCheck
, Z.ServiceCheck
, Z.ServiceCheckStatus
, ToStatsD
, Z.Tag
, envTag
, tag
, tagged
, sampled
, sampled'
, incCounter
, addCounter
, gauge
, timer
, histogram
, ToMetricValue(..)
, value
, Z.SampleRate
, Z.Priority(..)
, Z.AlertType(..)
, Z.StatsClient(Dummy)
) where
import Control.Applicative ((<$>))
import Control.Lens
import Control.Monad (when)
import Control.Monad.IO.Class
import Control.Reaper
import Data.BufferBuilder.Utf8
import Data.Generics.Product.Any
import Data.Generics.Product.Typed
import Data.List (intersperse)
import Data.Maybe (isNothing)
import Data.Semigroup ((<>))
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)
import Data.Time.Clock
import Data.Time.Clock.POSIX
import Network.Socket hiding (recv, recvFrom, send, sendTo)
import System.Environment
import System.IO (BufferMode (LineBuffering), IOMode (WriteMode), hClose, hSetBuffering)
import System.Random (randomIO)
import qualified Arbor.Network.StatsD.Type as Z
import qualified Data.ByteString as B
import qualified Data.Foldable as F
import qualified Data.Text as T
epochTime :: UTCTime -> Int
epochTime = round . utcTimeToPOSIXSeconds
sampleAlways :: Z.SampleRate
sampleAlways = Z.SampleRate 1.0
cleanMetricText :: Text -> Text
cleanMetricText = T.map $ \c -> case c of
':' -> '_'
'|' -> '_'
'@' -> '_'
_ -> c
{-# INLINE cleanMetricText #-}
escapeEventContents :: T.Text -> T.Text
escapeEventContents = T.replace "\n" "\\n"
{-# INLINE escapeEventContents #-}
tag :: Text -> Text -> Z.Tag
tag k v = Z.Tag (build k >> appendChar7 ':' >> build v)
where
build = appendText . cleanMetricText
class ToMetricValue a where
encodeValue :: a -> Utf8Builder ()
instance ToMetricValue Int where
encodeValue = appendDecimalSignedInt
instance ToMetricValue Double where
encodeValue = appendDecimalDouble
metric :: (ToMetricValue a) => Z.MetricName -> Z.MetricType -> a -> Z.Metric
metric n t v = Z.Metric n sampleAlways t (encodeValue v) []
value :: ToMetricValue a => Setter Z.Metric Z.Metric (Utf8Builder ()) a
value = sets $ \f m -> m { Z.value = encodeValue $ f $ Z.value m }
{-# INLINE value #-}
renderMetric :: Z.Metric -> Utf8Builder ()
renderMetric (Z.Metric n (Z.SampleRate sr) t v ts) = do
appendText $ cleanMetricText $ n ^. the @"text"
appendChar7 ':'
v
appendChar7 '|'
unit
formatRate
formatTags
where
unit = case t of
Z.Gauge -> appendChar7 'g'
Z.Counter -> appendChar7 'c'
Z.Timer -> appendBS7 "ms"
Z.Histogram -> appendChar7 'h'
Z.Set -> appendChar7 's'
formatTags = case ts of
[] -> return ()
xs -> appendBS7 "|#" >> F.sequence_ (intersperse (appendChar7 ',') $ map (^. the @"builder") xs)
formatRate = if sr == 1 then return () else appendBS7 "|@" >> appendDecimalDouble sr
event :: Text -> Text -> Z.Event
event t d = Z.Event t d Nothing Nothing Nothing Nothing Nothing Nothing []
renderEvent :: Z.Event -> Utf8Builder ()
renderEvent e = do
appendBS7 "_e{"
encodeValue $ B.length escapedTitle
appendChar7 ','
encodeValue $ B.length escapedText
appendBS7 "}:"
unsafeAppendBS escapedTitle
appendChar7 '|'
unsafeAppendBS escapedText
happened
formatHostname
aggregation
formatPriority
sourceType
alert
formatTags
where
escapedTitle :: B.ByteString
escapedTitle = encodeUtf8 $ escapeEventContents $ e ^. the @"title"
escapedText :: B.ByteString
escapedText = encodeUtf8 $ escapeEventContents $ e ^. the @"text"
makeField :: Foldable t => Char -> t (Utf8Builder b) -> Utf8Builder ()
makeField c v = F.forM_ v $ \jv ->
appendChar7 '|' >> appendChar7 c >> appendChar7 ':' >> jv
cleanTextValue :: Functor f => (Z.Event -> f Text) -> f (Utf8Builder ())
cleanTextValue f = (appendText . cleanMetricText) <$> f e
happened :: Utf8Builder ()
happened = F.forM_ (e ^. the @"dateHappened") $ \h -> do
appendBS7 "|d:"
appendDecimalSignedInt $ epochTime h
formatHostname :: Utf8Builder ()
formatHostname = makeField 'h' $ cleanTextValue (^. the @"hostname")
aggregation :: Utf8Builder ()
aggregation = makeField 'k' $ cleanTextValue (^. the @"aggregationKey")
formatPriority :: Utf8Builder ()
formatPriority = F.forM_ (e ^. the @"priority") $ \p -> do
appendBS7 "|p:"
appendBS7 $ case p of
Z.Low -> "low"
Z.Normal -> "normal"
sourceType :: Utf8Builder ()
sourceType = makeField 's' $ cleanTextValue (^. the @"sourceTypeName")
alert :: Utf8Builder ()
alert = F.forM_ (e ^. the @"alertType") $ \a -> do
appendBS7 "|t:"
appendBS7 $ case a of
Z.Error -> "error"
Z.Warning -> "warning"
Z.Info -> "info"
Z.Success -> "success"
formatTags :: Utf8Builder ()
formatTags = case e ^. the @"tags" of
[] -> return ()
ts -> do
appendBS7 "|#"
sequence_ $ intersperse (appendChar7 ',') $ map (^. the @"builder") ts
serviceCheck :: Text
-> Z.ServiceCheckStatus
-> Z.ServiceCheck
serviceCheck n s = Z.ServiceCheck n s Nothing Nothing Nothing []
class ToStatsD a where
toStatsD :: a -> Utf8Builder ()
instance ToStatsD Z.Metric where
toStatsD = renderMetric
instance ToStatsD Z.Event where
toStatsD = renderEvent
instance ToStatsD Z.ServiceCheck where
toStatsD check = do
appendBS7 "_sc|"
appendText $ cleanMetricText $ check ^. the @"name"
appendChar7 '|'
appendDecimalSignedInt $ fromEnum $ check ^. the @"status"
F.forM_ (check ^. the @"message") $ \msg ->
appendBS7 "|m:" >> appendText (cleanMetricText msg)
F.forM_ (check ^. the @"dateHappened") $ \ts -> do
appendBS7 "|d:"
appendDecimalSignedInt $ epochTime ts
F.forM_ (check ^. the @"hostname") $ \hn ->
appendBS7 "|h:" >> appendText (cleanMetricText hn)
case check ^. the @"tags" of
[] -> return ()
ts -> do
appendBS7 "|#"
sequence_ $ intersperse (appendChar7 ',') $ map (^. the @"builder") ts
defaultSettings :: Z.DogStatsSettings
defaultSettings = Z.DogStatsSettings "127.0.0.1" 8125
createStatsClient :: MonadIO m
=> Z.DogStatsSettings
-> Z.MetricName
-> [Z.Tag]
-> m Z.StatsClient
createStatsClient s n ts = liftIO $ do
addrInfos <- getAddrInfo (Just $ defaultHints { addrFlags = [AI_PASSIVE] })
(Just $ s ^. the @"host")
(Just $ show $ s ^. the @"port")
case addrInfos of
[] -> error "No address for hostname"
(serverAddr:_) -> do
sock <- socket (addrFamily serverAddr) Datagram defaultProtocol
connect sock (addrAddress serverAddr)
h <- socketToHandle sock WriteMode
hSetBuffering h LineBuffering
let builderAction work = do
F.mapM_ (B.hPut h . runUtf8Builder) work
return $ const Nothing
reaperSettings = defaultReaperSettings { reaperAction = builderAction
, reaperDelay = 1000000
, reaperCons = \item work -> Just $ maybe item (>> item) work
, reaperNull = isNothing
, reaperEmpty = Nothing
}
r <- mkReaper reaperSettings
return $ Z.StatsClient h r n ts
closeStatsClient :: MonadIO m => Z.StatsClient -> m ()
closeStatsClient c = liftIO $ finalizeStatsClient c >> hClose (Z.handle c)
tagged :: (HasType [Z.Tag] v) => (a -> v) -> (a -> [Z.Tag]) -> a -> v
tagged getVal getTag a = getVal a & typed @[Z.Tag] %~ (getTag a ++)
{-# INLINE tagged #-}
sampled' :: (HasType Z.SampleRate v) => (a -> v) -> (a -> Z.SampleRate) -> a -> v
sampled' getVal getRate a = getVal a & typed @Z.SampleRate .~ getRate a
{-# INLINE sampled' #-}
sampled :: (HasType Z.SampleRate v) => (a -> v) -> Z.SampleRate -> a -> v
sampled f r a = f a & typed @Z.SampleRate .~ r
{-# INLINE sampled #-}
incCounter :: Z.MetricName -> Z.Metric
incCounter n = metric n Z.Counter (1 :: Int)
{-# INLINE incCounter #-}
addCounter :: Z.MetricName -> (a -> Int) -> a -> Z.Metric
addCounter n f a = metric n Z.Counter (f a)
{-# INLINE addCounter #-}
gauge :: ToMetricValue v => Z.MetricName -> (a -> v) -> a -> Z.Metric
gauge n f a = metric n Z.Gauge (f a)
{-# INLINE gauge #-}
timer :: ToMetricValue v => Z.MetricName -> (a -> v) -> a -> Z.Metric
timer n f a = metric n Z.Timer (f a)
{-# INLINE timer #-}
histogram :: ToMetricValue v => Z.MetricName -> (a -> v) -> a -> Z.Metric
histogram n f a = metric n Z.Histogram (f a)
{-# INLINE histogram #-}
send ::
( MonadIO m
, ToStatsD v
, HasType Z.MetricName v
, HasType [Z.Tag] v)
=> Z.StatsClient
-> v
-> m ()
send Z.Dummy _ = return ()
send (Z.StatsClient _ r n ts) v = liftIO $
reaperAdd r ((toStatsD . addAspect n . addTags ts) v >> appendChar7 '\n')
{-# INLINEABLE send #-}
sendEvt :: (MonadIO m) => Z.StatsClient -> Z.Event -> m ()
sendEvt Z.Dummy _ = return ()
sendEvt (Z.StatsClient _ r (Z.MetricName n) ts) e = liftIO $
reaperAdd r ((toStatsD . addTags (tag "aspect" n : ts)) e >> appendChar7 '\n')
sendSampled ::
( MonadIO m
, ToStatsD v
, HasType Z.SampleRate v
, HasType Z.MetricName v
, HasType [Z.Tag] v)
=> Z.StatsClient
-> v
-> m ()
sendSampled Z.Dummy _ = return ()
sendSampled c v = liftIO $ do
z <- Z.SampleRate <$> randomIO
when (z <= v ^. typed @Z.SampleRate) $ send c v
{-# INLINEABLE sendSampled #-}
envTag :: Z.EnvVarName -> Z.TagKey -> IO (Maybe Z.Tag)
envTag var key = do
mbVal <- lookupEnv var
return $ (tag key . T.pack) <$> mbVal
finalizeStatsClient :: Z.StatsClient -> IO ()
finalizeStatsClient (Z.StatsClient h r _ _) = reaperStop r >>= F.mapM_ (B.hPut h . runUtf8Builder)
finalizeStatsClient Z.Dummy = return ()
addAspect :: (HasType Z.MetricName v) => Z.MetricName -> v -> v
addAspect (Z.MetricName a) v =
if T.null a
then v
else v & typed @Z.MetricName %~ (\(Z.MetricName n) -> Z.MetricName (a <> "." <> n))
{-# INLINE addAspect #-}
addTags :: (HasType [Z.Tag] v) => [Z.Tag] -> v -> v
addTags [] v = v
addTags ts v = v & typed @[Z.Tag] %~ (ts ++)
{-# INLINE addTags #-}