haskoin-store-1.0.0: src/Haskoin/Store/Stats.hs
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoFieldSelectors #-}
module Haskoin.Store.Stats
( StatDist,
withStats,
createStatDist,
addStatTime,
addClientError,
addServerError,
addStatQuery,
addStatItems,
)
where
import Control.Concurrent.STM.TQueue
( TQueue,
flushTQueue,
writeTQueue,
)
import Control.Foldl qualified as L
import Control.Monad (forever)
import Data.Function (on)
import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as HashMap
import Data.Int (Int64)
import Data.List (sort, sortBy, sortOn)
import Data.Maybe (fromMaybe)
import Data.Ord (Down (..), comparing)
import Data.String.Conversions (cs)
import Data.Text (Text)
import System.Metrics
( Store,
Value (..),
newStore,
registerGcMetrics,
registerGroup,
sampleAll,
)
import System.Remote.Monitoring.Statsd
( defaultStatsdOptions,
flushInterval,
forkStatsd,
host,
port,
prefix,
)
import UnliftIO
( MonadIO,
TVar,
atomically,
liftIO,
modifyTVar,
newTQueueIO,
newTVarIO,
readTVar,
withAsync,
)
import UnliftIO.Concurrent (threadDelay)
withStats :: (MonadIO m) => Text -> Int -> Text -> (Store -> m a) -> m a
withStats h p pfx go = do
store <- liftIO newStore
_statsd <-
liftIO $
forkStatsd
defaultStatsdOptions
{ prefix = pfx,
host = h,
port = p
}
store
liftIO $ registerGcMetrics store
go store
data StatData = StatData
{ times :: ![Int64],
queries :: !Int64,
items :: !Int64,
clientErrors :: !Int64,
serverErrors :: !Int64
}
data StatDist = StatDist
{ queue :: !(TQueue Int64),
queries :: !(TVar Int64),
items :: !(TVar Int64),
clientErrors :: !(TVar Int64),
serverErrors :: !(TVar Int64)
}
createStatDist :: (MonadIO m) => Text -> Store -> m StatDist
createStatDist t store = liftIO $ do
queue <- newTQueueIO
queries <- newTVarIO 0
items <- newTVarIO 0
clientErrors <- newTVarIO 0
serverErrors <- newTVarIO 0
let metrics =
HashMap.fromList
[ ( t <> ".request_count",
Counter . (.queries)
),
( t <> ".item_count",
Counter . (.items)
),
( t <> ".client_errors",
Counter . (.clientErrors)
),
( t <> ".server_errors",
Counter . (.serverErrors)
),
( t <> ".mean_ms",
Gauge . mean . (.times)
),
( t <> ".avg_ms",
Gauge . avg . (.times)
),
( t <> ".max_ms",
Gauge . maxi . (.times)
),
( t <> ".min_ms",
Gauge . mini . (.times)
),
( t <> ".p90max_ms",
Gauge . p90max . (.times)
),
( t <> ".p90avg_ms",
Gauge . p90avg . (.times)
),
( t <> ".var_ms",
Gauge . var . (.times)
)
]
let sd = StatDist {..}
registerGroup metrics (flush sd) store
return sd
toDouble :: Int64 -> Double
toDouble = fromIntegral
addStatTime :: (MonadIO m) => StatDist -> Int64 -> m ()
addStatTime q =
liftIO . atomically . writeTQueue q.queue
addStatQuery :: (MonadIO m) => StatDist -> m ()
addStatQuery q =
liftIO . atomically $ modifyTVar q.queries (+ 1)
addStatItems :: (MonadIO m) => StatDist -> Int64 -> m ()
addStatItems q =
liftIO . atomically . modifyTVar q.items . (+)
addClientError :: (MonadIO m) => StatDist -> m ()
addClientError q =
liftIO . atomically $ modifyTVar q.clientErrors (+ 1)
addServerError :: (MonadIO m) => StatDist -> m ()
addServerError q =
liftIO . atomically $ modifyTVar q.serverErrors (+ 1)
flush :: (MonadIO m) => StatDist -> m StatData
flush StatDist {..} = atomically $ do
times <- flushTQueue queue
queries <- readTVar queries
items <- readTVar items
clientErrors <- readTVar clientErrors
serverErrors <- readTVar serverErrors
return $ StatData {..}
average :: (Fractional a) => L.Fold a a
average = (/) <$> L.sum <*> L.genericLength
avg :: [Int64] -> Int64
avg = round . L.fold average . map toDouble
mean :: [Int64] -> Int64
mean = round . L.fold L.mean . map toDouble
maxi :: [Int64] -> Int64
maxi = fromMaybe 0 . L.fold L.maximum
mini :: [Int64] -> Int64
mini = fromMaybe 0 . L.fold L.minimum
var :: [Int64] -> Int64
var = round . L.fold L.variance . map toDouble
p90max :: [Int64] -> Int64
p90max ls =
case chopped of
[] -> 0
h : _ -> h
where
sorted = sortOn Down ls
len = length sorted
chopped = drop (length sorted `div` 10) sorted
p90avg :: [Int64] -> Int64
p90avg ls =
avg chopped
where
sorted = sortOn Down ls
len = length sorted
chopped = drop (length sorted `div` 10) sorted