packages feed

haskoin-store-0.47.5: src/Haskoin/Store/Stats.hs

{-# LANGUAGE OverloadedStrings #-}
module Haskoin.Store.Stats
    ( StatDist
    , withStats
    , createStatDist
    , addStatTime
    , addStatItems
    , addClientError
    , addServerError
    , addStatQuery
    ) where

import           Control.Concurrent.STM.TQueue   (TQueue, flushTQueue,
                                                  writeTQueue)
import qualified Control.Foldl                   as L
import           Control.Monad                   (forever)
import           Data.Function                   (on)
import           Data.HashMap.Strict             (HashMap)
import qualified Data.HashMap.Strict             as HashMap
import           Data.Int                        (Int64)
import           Data.List                       (sort, sortBy)
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
    {
        statTimes        :: ![Int64],
        statItems        :: !Int64,
        statQueries      :: !Int64,
        statClientErrors :: !Int64,
        statServerErrors :: !Int64
    }

data StatDist =
    StatDist
    {
        distQueue        :: !(TQueue Int64),
        distItems        :: !(TVar Int64),
        distQueries      :: !(TVar Int64),
        distClientErrors :: !(TVar Int64),
        distServerErrors :: !(TVar Int64)
    }

createStatDist :: MonadIO m => Text -> Store -> m StatDist
createStatDist t store = liftIO $ do
    q <- newTQueueIO
    items <- newTVarIO 0
    queries <- newTVarIO 0
    client_errors <- newTVarIO 0
    server_errors <- newTVarIO 0
    let metrics = HashMap.fromList
            [ (t <> ".query_count",   Counter . statQueries)
            , (t <> ".item_count",    Counter . statItems)
            , (t <> ".errors.client", Counter . statClientErrors)
            , (t <> ".errors.server", Counter . statServerErrors)
            , (t <> ".mean_ms",       Gauge . mean . statTimes)
            , (t <> ".avg_ms",        Gauge . avg . statTimes)
            , (t <> ".max_ms",        Gauge . maxi . statTimes)
            , (t <> ".min_ms",        Gauge . mini . statTimes)
            , (t <> ".p90max_ms",     Gauge . p90max . statTimes)
            , (t <> ".p90min_ms",     Gauge . p90min . statTimes)
            , (t <> ".p90avg_ms",     Gauge . p90avg . statTimes)
            , (t <> ".var_ms",        Gauge . var . statTimes)
            ]
    let sd = StatDist q items queries client_errors server_errors
    registerGroup metrics (flush sd) store
    return sd

toDouble :: Int64 -> Double
toDouble = fromIntegral

addStatTime :: MonadIO m => StatDist -> Int64 -> m ()
addStatTime q =
    liftIO . atomically . writeTQueue (distQueue q)

addStatItems :: MonadIO m => StatDist -> Int64 -> m ()
addStatItems q =
    liftIO . atomically . modifyTVar (distItems q) . (+)

addStatQuery :: MonadIO m => StatDist -> m ()
addStatQuery q =
    liftIO . atomically $ modifyTVar (distQueries q) (+1)

addClientError :: MonadIO m => StatDist -> m ()
addClientError q =
    liftIO . atomically $ modifyTVar (distClientErrors q) (+1)

addServerError :: MonadIO m => StatDist -> m ()
addServerError q =
    liftIO . atomically $ modifyTVar (distServerErrors q) (+1)

flush :: MonadIO m => StatDist -> m StatData
flush (StatDist q i n c s) = atomically $ do
    ts <- flushTQueue q
    is <- readTVar i
    qs <- readTVar n
    ce <- readTVar c
    se <- readTVar s
    return $ StatData ts is qs ce se

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 = sortBy (comparing Down) ls
    len = length sorted
    chopped = drop (length sorted * 1 `div` 10) sorted

p90min :: [Int64] -> Int64
p90min ls =
    case chopped of
        []  -> 0
        h:_ -> h
  where
    sorted = sort ls
    len = length sorted
    chopped = drop (length sorted * 1 `div` 10) sorted

p90avg :: [Int64] -> Int64
p90avg ls =
    avg chopped
  where
    sorted = sortBy (comparing Down) ls
    len = length sorted
    chopped = drop (length sorted * 1 `div` 10) sorted