packages feed

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