packages feed

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

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

module Haskoin.Store.Stats (
    StatDist,
    withStats,
    createStatDist,
    addStatTime,
    addClientError,
    addServerError,
    addStatQuery,
    addStatItems,
) 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]
    , statQueries :: !Int64
    , statItems :: !Int64
    , statClientErrors :: !Int64
    , statServerErrors :: !Int64
    }

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

createStatDist :: MonadIO m => Text -> Store -> m StatDist
createStatDist t store = liftIO $ do
    distQueue <- newTQueueIO
    distQueries <- newTVarIO 0
    distItems <- newTVarIO 0
    distClientErrors <- newTVarIO 0
    distServerErrors <- newTVarIO 0
    let metrics =
            HashMap.fromList
                [
                    ( t <> ".request_count"
                    , Counter . statQueries
                    )
                ,
                    ( t <> ".item_count"
                    , Counter . statItems
                    )
                ,
                    ( t <> ".client_errors"
                    , Counter . statClientErrors
                    )
                ,
                    ( t <> ".server_errors"
                    , 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 <> ".p90avg_ms"
                    , Gauge . p90avg . statTimes
                    )
                ,
                    ( t <> ".var_ms"
                    , Gauge . var . statTimes
                    )
                ]
    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 (distQueue q)

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

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

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{..} = atomically $ do
    statTimes <- flushTQueue distQueue
    statQueries <- readTVar distQueries
    statItems <- readTVar distItems
    statClientErrors <- readTVar distClientErrors
    statServerErrors <- readTVar distServerErrors
    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 = sortBy (comparing Down) 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