freckle-app-1.6.0.0: library/Freckle/App/Stats.hs
{-# LANGUAGE TupleSections #-}
-- | An intentionally-leaky StatsD interface to Datadog
--
-- $usage
--
module Freckle.App.Stats
( StatsSettings
, defaultStatsSettings
, setStatsSettingsTags
, envParseStatsSettings
-- * Client
, StatsClient
, tagsL
, withStatsClient
, HasStatsClient(..)
-- * Reporting
, tagged
, increment
, counter
, gauge
, histogram
, histogramSince
, histogramSinceMs
) where
import Freckle.App.Prelude
import Blammo.Logging
import Control.Lens (Lens', lens, view, (&), (.~), (<>~))
import Control.Monad.Reader (local)
import Data.Aeson (Value(..))
import Data.String
import Data.Time (diffUTCTime)
import Freckle.App.Ecs
import qualified Freckle.App.Env as Env
import qualified Network.StatsD.Datadog as Datadog
import Yesod.Core.Lens
import Yesod.Core.Types (HandlerData)
data StatsSettings = StatsSettings
{ amsEnabled :: Bool
, amsSettings :: Datadog.DogStatsSettings
, amsTags :: [(Text, Text)]
}
defaultStatsSettings :: StatsSettings
defaultStatsSettings = StatsSettings
{ amsEnabled = False
, amsSettings = Datadog.defaultSettings
, amsTags = []
}
setStatsSettingsTags :: [(Text, Text)] -> StatsSettings -> StatsSettings
setStatsSettingsTags x settings = settings { amsTags = x }
envParseStatsSettings :: Env.Parser Env.Error StatsSettings
envParseStatsSettings =
StatsSettings
<$> Env.switch "DOGSTATSD_ENABLED" mempty
<*> (buildSettings
<$> optional (Env.var Env.str "DOGSTATSD_HOST" mempty)
<*> optional (Env.var Env.auto "DOGSTATSD_PORT" mempty)
)
<*> (buildTags
<$> optional (Env.var Env.nonempty "DD_ENV" mempty)
<*> optional (Env.var Env.nonempty "DD_SERVICE" mempty)
<*> optional (Env.var Env.nonempty "DD_VERSION" mempty)
<*> Env.var Env.keyValues "DOGSTATSD_TAGS" (Env.def [])
)
where
buildSettings mHost mPort =
Datadog.defaultSettings
& maybe id (Datadog.host .~) mHost
. maybe id (Datadog.port .~) mPort
buildTags mEnv mService mVersion tags =
catMaybes
[ ("env", ) <$> mEnv
, ("environment", ) <$> mEnv -- Legacy
, ("service", ) <$> mService
, ("version", ) <$> mVersion
]
<> tags
data StatsClient = StatsClient
{ scClient :: Datadog.StatsClient
, scTags :: [(Text, Text)]
}
tagsL :: Lens' StatsClient [(Text, Text)]
tagsL = lens scTags $ \x y -> x { scTags = y }
class HasStatsClient env where
statsClientL :: Lens' env StatsClient
instance HasStatsClient StatsClient where
statsClientL = id
instance HasStatsClient site => HasStatsClient (HandlerData child site) where
statsClientL = envL . siteL . statsClientL
withStatsClient
:: (MonadMask m, MonadUnliftIO m)
=> StatsSettings
-> (StatsClient -> m a)
-> m a
withStatsClient StatsSettings {..} f = do
if amsEnabled
then do
tags <- (amsTags <>) <$> getEcsMetadataTags
Datadog.withDogStatsD amsSettings $ \client ->
-- Add the tags to the thread context so they're present in all logs
withThreadContext (map toPair tags)
$ f StatsClient { scClient = client, scTags = tags }
else f $ StatsClient { scClient = Datadog.Dummy, scTags = amsTags }
where toPair = bimap (fromString . unpack) String
-- | Include the given tags on all metrics emitted from a block
tagged
:: (MonadReader env m, HasStatsClient env) => [(Text, Text)] -> m a -> m a
tagged tags = local $ statsClientL . tagsL <>~ tags
-- | Synonym for @'counter' 1@
increment
:: (MonadUnliftIO m, MonadReader env m, HasStatsClient env) => Text -> m ()
increment name = counter name 1
counter
:: (MonadUnliftIO m, MonadReader env m, HasStatsClient env)
=> Text
-> Int
-> m ()
counter = sendMetric Datadog.Counter
gauge
:: (MonadUnliftIO m, MonadReader env m, HasStatsClient env)
=> Text
-> Double
-> m ()
gauge = sendMetric Datadog.Gauge
-- | Emit an elapsed duration (which Datadog calls a /histogram/)
--
-- The 'ToMetricValue' constraint can be satisfied by most numeric types and is
-- assumed to be seconds.
--
histogram
:: ( MonadUnliftIO m
, MonadReader env m
, HasStatsClient env
, Datadog.ToMetricValue n
)
=> Text
-> n
-> m ()
histogram = sendMetric Datadog.Histogram
histogramSince
:: (MonadUnliftIO m, MonadReader env m, HasStatsClient env)
=> Text
-> UTCTime
-> m ()
histogramSince = histogramSinceBy toSeconds where toSeconds = round @_ @Int
histogramSinceMs
:: (MonadUnliftIO m, MonadReader env m, HasStatsClient env)
=> Text
-> UTCTime
-> m ()
histogramSinceMs = histogramSinceBy toMilliseconds
where toMilliseconds = (* 1000) . realToFrac @_ @Double
histogramSinceBy
:: ( MonadUnliftIO m
, MonadReader env m
, HasStatsClient env
, Datadog.ToMetricValue n
)
=> (NominalDiffTime -> n)
-> Text
-> UTCTime
-> m ()
histogramSinceBy f name time = do
now <- liftIO getCurrentTime
let delta = f $ now `diffUTCTime` time
sendMetric Datadog.Histogram name delta
sendMetric
:: ( MonadUnliftIO m
, MonadReader env m
, HasStatsClient env
, Datadog.ToMetricValue v
)
=> Datadog.MetricType
-> Text
-> v
-> m ()
sendMetric metricType name metricValue = do
StatsClient {..} <- view statsClientL
Datadog.send scClient
$ Datadog.metric (Datadog.MetricName name) metricType metricValue
& (Datadog.tags .~ map (uncurry Datadog.tag) scTags)
getEcsMetadataTags :: MonadIO m => m [(Text, Text)]
getEcsMetadataTags = maybe [] toTags <$> getEcsMetadata
where
toTags (EcsMetadata EcsContainerMetadata {..} EcsContainerTaskMetadata {..})
= [ ("container_id", ecmDockerId)
, ("container_name", ecmDockerName)
, ("docker_image", ecmImage)
, ("image_tag", ecmImageID)
, ("cluster_name", ectmCluster)
, ("task_arn", ectmTaskARN)
, ("task_family", ectmFamily)
, ("task_version", ectmRevision)
]
-- $usage
-- Usage:
--
-- - Use 'envParseStatsSettings' to configure things
--
-- @
-- data AppSettings = AppSettings
-- { -- ...
-- , appStatsSettings :: StatsSettings
-- }
--
-- loadSettings :: IO AppSettings
-- loadSettings = Env.parse id $ AppSettings
-- <$> -- ...
-- <*> 'envParseStatsSettings'
-- @
--
-- This will read,
--
-- - @DOGSTATSD_ENABLED=x@
-- - @DOGSTATSD_HOST=127.0.0.1@
-- - @DOGSTATSD_PORT=8125@
-- - @DOGSTATSD_TAGS=[<key>:<value>,...]@
-- - Optionally @DD_ENV@, @DD_SERVICE@, and @DD_VERSION@
--
-- - Give your @App@ a 'HasStatsClient' instance
--
-- @
-- data App = App
-- { -- ...
-- , appStatsClient :: 'StatsClient'
-- }
--
-- instance 'HasStatsClient' App where
-- 'statsClientL' = lens appStatsClient $ \x y -> { appStatsClient = y }
-- @
--
-- - Use 'withStatsClient' to build and store a client on your @App@ when you
-- run it
--
-- @
-- 'withStatsClient' appStatsSettings $ \client -> do
-- app <- App
-- <$> ...
-- <*> pure client
--
-- 'runApp' app $ ...
-- @
--
-- - Throughout your application code, emit metrics as desired
--
-- @
-- import qualified Freckle.App.Stats as Stats
--
-- myFunction :: (MonadIO m, MonadReader env m, 'HasStatsClient' env) => m ()
-- myFunction = do
-- start <- liftIO getCurrentTime
-- result <- myAction
--
-- Stats.'increment' \"action.attempt\"
-- Stats.'histogramSinceMs' \"action.duration\" start
--
-- case result of
-- Left err -> do
-- Stats.'increment' \"action.failure\"
-- -- ...
-- Right x -. do
-- Stats.'increment' \"action.success\"
-- -- ...
-- @
--