statsd-rupp 0.3.0.1 → 0.4.0.0
raw patch · 5 files changed
+551/−444 lines, 5 filesdep +QuickCheckdep +vectorPVP ok
version bump matches the API change (PVP)
Dependencies added: QuickCheck, vector
API changes (from Hackage documentation)
- System.Metrics.StatsD: [$sel:newline:StatConfig] :: StatConfig -> !Bool
- System.Metrics.StatsD: [$sel:port:StatConfig] :: StatConfig -> !Int
- System.Metrics.StatsD: [$sel:server:StatConfig] :: StatConfig -> !String
- System.Metrics.StatsD: [$sel:statsPrefix:StatConfig] :: StatConfig -> !String
- System.Metrics.StatsD: parseReport :: MonadPlus m => ByteString -> m Report
- System.Metrics.StatsD.Internal: Metric :: !Int -> Value
- System.Metrics.StatsD.Internal: Other :: !String -> !String -> Value
- System.Metrics.StatsD.Internal: [$sel:cfg:Stats] :: Stats -> !StatConfig
- System.Metrics.StatsD.Internal: [$sel:newline:StatConfig] :: StatConfig -> !Bool
- System.Metrics.StatsD.Internal: [$sel:port:StatConfig] :: StatConfig -> !Int
- System.Metrics.StatsD.Internal: [$sel:server:StatConfig] :: StatConfig -> !String
- System.Metrics.StatsD.Internal: [$sel:statsPrefix:StatConfig] :: StatConfig -> !String
- System.Metrics.StatsD.Internal: format :: StatConfig -> Report -> String
- System.Metrics.StatsD.Internal: type Counter = Int
- System.Metrics.StatsD.Internal: type Gauge = Int
- System.Metrics.StatsD.Internal: type Index = Int
- System.Metrics.StatsD.Internal: type Key = String
- System.Metrics.StatsD.Internal: type Sampling = Int
- System.Metrics.StatsD.Internal: type SetData = HashSet String
- System.Metrics.StatsD.Internal: type SetElement = String
- System.Metrics.StatsD.Internal: type Timing = Int
- System.Metrics.StatsD.Internal: type Timings = [Int]
+ System.Metrics.StatsD: [$sel:appendNewline:StatConfig] :: StatConfig -> !Bool
+ System.Metrics.StatsD: [$sel:prefixStats:StatConfig] :: StatConfig -> !String
+ System.Metrics.StatsD: [$sel:statsdPort:StatConfig] :: StatConfig -> !Int
+ System.Metrics.StatsD: [$sel:statsdServer:StatConfig] :: StatConfig -> !String
+ System.Metrics.StatsD.Internal: [$sel:appendNewline:StatConfig] :: StatConfig -> !Bool
+ System.Metrics.StatsD.Internal: [$sel:params:Stats] :: Stats -> !StatParams
+ System.Metrics.StatsD.Internal: [$sel:prefixStats:StatConfig] :: StatConfig -> !String
+ System.Metrics.StatsD.Internal: [$sel:statsdPort:StatConfig] :: StatConfig -> !Int
+ System.Metrics.StatsD.Internal: [$sel:statsdServer:StatConfig] :: StatConfig -> !String
+ System.Metrics.StatsD.Internal: data StatParams
+ System.Metrics.StatsD.Internal: formatReport :: Report -> ByteString
+ System.Metrics.StatsD.Internal: newMetrics :: MonadIO m => m (TVar Metrics)
+ System.Metrics.StatsD.Internal: newParams :: StatConfig -> StatParams
+ System.Metrics.StatsD.Internal: parseInt :: MonadPlus m => ByteString -> m Int
+ System.Metrics.StatsD.Internal: parseRead :: (MonadPlus m, Read a) => ByteString -> m a
+ System.Metrics.StatsD.Internal: parseReport :: MonadPlus m => ByteString -> m Report
- System.Metrics.StatsD: newStatCounter :: MonadIO m => Stats -> Key -> Sampling -> m StatCounter
+ System.Metrics.StatsD: newStatCounter :: MonadIO m => Stats -> Key -> Int -> m StatCounter
- System.Metrics.StatsD.Internal: Counter :: !Counter -> Value
+ System.Metrics.StatsD.Internal: Counter :: !Int -> Value
- System.Metrics.StatsD.Internal: CounterData :: !Counter -> MetricData
+ System.Metrics.StatsD.Internal: CounterData :: !Int -> MetricData
- System.Metrics.StatsD.Internal: Gauge :: !Gauge -> !Bool -> Value
+ System.Metrics.StatsD.Internal: Gauge :: !Int -> !Bool -> Value
- System.Metrics.StatsD.Internal: GaugeData :: !Gauge -> MetricData
+ System.Metrics.StatsD.Internal: GaugeData :: !Int -> MetricData
- System.Metrics.StatsD.Internal: Report :: !Key -> !Value -> !Double -> Report
+ System.Metrics.StatsD.Internal: Report :: !ByteString -> !Value -> !Double -> Report
- System.Metrics.StatsD.Internal: Sample :: !Key -> !Value -> !Sampling -> !Index -> Sample
+ System.Metrics.StatsD.Internal: Sample :: !ByteString -> !Value -> !Int -> !Int -> Sample
- System.Metrics.StatsD.Internal: Set :: !SetElement -> Value
+ System.Metrics.StatsD.Internal: Set :: !ByteString -> Value
- System.Metrics.StatsD.Internal: SetData :: !HashSet String -> MetricData
+ System.Metrics.StatsD.Internal: SetData :: !HashSet ByteString -> MetricData
- System.Metrics.StatsD.Internal: StatCounter :: !Stats -> !Key -> !Sampling -> StatCounter
+ System.Metrics.StatsD.Internal: StatCounter :: !Stats -> !ByteString -> !Int -> StatCounter
- System.Metrics.StatsD.Internal: StatGauge :: !Stats -> !Key -> StatGauge
+ System.Metrics.StatsD.Internal: StatGauge :: !Stats -> !ByteString -> StatGauge
- System.Metrics.StatsD.Internal: StatSet :: !Stats -> !Key -> StatSet
+ System.Metrics.StatsD.Internal: StatSet :: !Stats -> !ByteString -> StatSet
- System.Metrics.StatsD.Internal: StatTiming :: !Stats -> !Key -> !Sampling -> StatTiming
+ System.Metrics.StatsD.Internal: StatTiming :: !Stats -> !ByteString -> !Int -> StatTiming
- System.Metrics.StatsD.Internal: Stats :: !TVar Metrics -> !StatConfig -> !Socket -> Stats
+ System.Metrics.StatsD.Internal: Stats :: !TVar Metrics -> !Socket -> !StatParams -> Stats
- System.Metrics.StatsD.Internal: Store :: !Index -> !Maybe MetricData -> Store
+ System.Metrics.StatsD.Internal: Store :: !Int -> !Maybe MetricData -> Store
- System.Metrics.StatsD.Internal: Timing :: !Timing -> Value
+ System.Metrics.StatsD.Internal: Timing :: !Int -> Value
- System.Metrics.StatsD.Internal: TimingData :: !Timings -> MetricData
+ System.Metrics.StatsD.Internal: TimingData :: ![Int] -> MetricData
- System.Metrics.StatsD.Internal: TimingStats :: ![Int] -> ![Int] -> ![Int] -> TimingStats
+ System.Metrics.StatsD.Internal: TimingStats :: !Vector Int -> !Vector Int -> !Vector Int -> TimingStats
- System.Metrics.StatsD.Internal: [$sel:cumsquares:TimingStats] :: TimingStats -> ![Int]
+ System.Metrics.StatsD.Internal: [$sel:cumsquares:TimingStats] :: TimingStats -> !Vector Int
- System.Metrics.StatsD.Internal: [$sel:cumsums:TimingStats] :: TimingStats -> ![Int]
+ System.Metrics.StatsD.Internal: [$sel:cumsums:TimingStats] :: TimingStats -> !Vector Int
- System.Metrics.StatsD.Internal: [$sel:index:Sample] :: Sample -> !Index
+ System.Metrics.StatsD.Internal: [$sel:index:Sample] :: Sample -> !Int
- System.Metrics.StatsD.Internal: [$sel:index:Store] :: Store -> !Index
+ System.Metrics.StatsD.Internal: [$sel:index:Store] :: Store -> !Int
- System.Metrics.StatsD.Internal: [$sel:key:Report] :: Report -> !Key
+ System.Metrics.StatsD.Internal: [$sel:key:Report] :: Report -> !ByteString
- System.Metrics.StatsD.Internal: [$sel:key:Sample] :: Sample -> !Key
+ System.Metrics.StatsD.Internal: [$sel:key:Sample] :: Sample -> !ByteString
- System.Metrics.StatsD.Internal: [$sel:key:StatCounter] :: StatCounter -> !Key
+ System.Metrics.StatsD.Internal: [$sel:key:StatCounter] :: StatCounter -> !ByteString
- System.Metrics.StatsD.Internal: [$sel:key:StatGauge] :: StatGauge -> !Key
+ System.Metrics.StatsD.Internal: [$sel:key:StatGauge] :: StatGauge -> !ByteString
- System.Metrics.StatsD.Internal: [$sel:key:StatSet] :: StatSet -> !Key
+ System.Metrics.StatsD.Internal: [$sel:key:StatSet] :: StatSet -> !ByteString
- System.Metrics.StatsD.Internal: [$sel:key:StatTiming] :: StatTiming -> !Key
+ System.Metrics.StatsD.Internal: [$sel:key:StatTiming] :: StatTiming -> !ByteString
- System.Metrics.StatsD.Internal: [$sel:sampling:Sample] :: Sample -> !Sampling
+ System.Metrics.StatsD.Internal: [$sel:sampling:Sample] :: Sample -> !Int
- System.Metrics.StatsD.Internal: [$sel:sampling:StatCounter] :: StatCounter -> !Sampling
+ System.Metrics.StatsD.Internal: [$sel:sampling:StatCounter] :: StatCounter -> !Int
- System.Metrics.StatsD.Internal: [$sel:sampling:StatTiming] :: StatTiming -> !Sampling
+ System.Metrics.StatsD.Internal: [$sel:sampling:StatTiming] :: StatTiming -> !Int
- System.Metrics.StatsD.Internal: [$sel:timings:TimingStats] :: TimingStats -> ![Int]
+ System.Metrics.StatsD.Internal: [$sel:timings:TimingStats] :: TimingStats -> !Vector Int
- System.Metrics.StatsD.Internal: addMetric :: StatConfig -> Key -> MetricData -> Metrics -> Metrics
+ System.Metrics.StatsD.Internal: addMetric :: StatParams -> ByteString -> MetricData -> Metrics -> Metrics
- System.Metrics.StatsD.Internal: addReading :: Value -> Key -> Metrics -> Metrics
+ System.Metrics.StatsD.Internal: addReading :: Value -> ByteString -> Metrics -> Metrics
- System.Metrics.StatsD.Internal: catKey :: [Key] -> Key
+ System.Metrics.StatsD.Internal: catKey :: [ByteString] -> ByteString
- System.Metrics.StatsD.Internal: flushStats :: StatConfig -> Metrics -> ([Report], Metrics)
+ System.Metrics.StatsD.Internal: flushStats :: StatParams -> Metrics -> ([Report], Metrics)
- System.Metrics.StatsD.Internal: makeTimingStats :: Timings -> TimingStats
+ System.Metrics.StatsD.Internal: makeTimingStats :: [Int] -> TimingStats
- System.Metrics.StatsD.Internal: newMetric :: MonadIO m => Stats -> Key -> MetricData -> m ()
+ System.Metrics.StatsD.Internal: newMetric :: MonadIO m => Stats -> ByteString -> MetricData -> m ()
- System.Metrics.StatsD.Internal: newReading :: Stats -> Key -> Value -> STM Int
+ System.Metrics.StatsD.Internal: newReading :: Stats -> ByteString -> Value -> STM Int
- System.Metrics.StatsD.Internal: newStats :: MonadIO m => StatConfig -> m Stats
+ System.Metrics.StatsD.Internal: newStats :: StatConfig -> TVar Metrics -> Socket -> Stats
- System.Metrics.StatsD.Internal: percentileSuffix :: Int -> String
+ System.Metrics.StatsD.Internal: percentileSuffix :: Int -> ByteString
- System.Metrics.StatsD.Internal: processSample :: MonadIO m => Stats -> Sampling -> Key -> Value -> m ()
+ System.Metrics.StatsD.Internal: processSample :: MonadIO m => Stats -> Int -> ByteString -> Value -> m ()
- System.Metrics.StatsD.Internal: statReports :: StatConfig -> Key -> MetricData -> [Report]
+ System.Metrics.StatsD.Internal: statReports :: StatParams -> ByteString -> MetricData -> [Report]
- System.Metrics.StatsD.Internal: timingReports :: StatConfig -> Key -> Timings -> [Report]
+ System.Metrics.StatsD.Internal: timingReports :: StatParams -> ByteString -> [Int] -> [Report]
- System.Metrics.StatsD.Internal: timingStats :: StatConfig -> Key -> TimingStats -> Int -> [Report]
+ System.Metrics.StatsD.Internal: timingStats :: StatParams -> ByteString -> TimingStats -> Int -> [Report]
- System.Metrics.StatsD.Internal: type Metrics = HashMap Key Store
+ System.Metrics.StatsD.Internal: type Metrics = HashMap ByteString Store
- System.Metrics.StatsD.Internal: validateKey :: String -> Bool
+ System.Metrics.StatsD.Internal: validateKey :: ByteString -> Bool
Files
- CHANGELOG.md +7/−0
- src/System/Metrics/StatsD.hs +22/−66
- src/System/Metrics/StatsD/Internal.hs +223/−169
- statsd-rupp.cabal +5/−2
- test/Spec.hs +294/−207
CHANGELOG.md view
@@ -8,6 +8,13 @@ ## Unreleased +## 0.4.0.0 - 2023-09-04++- Use vectors where pertient.+- Use bytestrings internally.+- Change configuration parameters.+- Code and test refactoring.+ ## 0.3.0.1 - 2023-09-02 - Ignore IO errors when sending UDP packets to the network.
src/System/Metrics/StatsD.hs view
@@ -25,19 +25,13 @@ newSetElement, withStats, defStatConfig,- parseReport, ) where -import Control.Monad (MonadPlus (..))-import Data.ByteString (ByteString) import Data.ByteString.Char8 qualified as C import Data.HashSet qualified as HashSet import System.Metrics.StatsD.Internal- ( Key,- MetricData (..),- Report (..),- Sampling,+ ( MetricData (..), StatConfig (..), StatCounter (..), StatGauge (..),@@ -45,55 +39,57 @@ StatTiming (..), Stats, Value (..),+ connectStatsD, newMetric,+ newMetrics, newStats, processSample, statsLoop,- validateKey, )-import Text.Read (readMaybe) import UnliftIO (MonadIO, MonadUnliftIO) import UnliftIO.Async (link, withAsync) +type Key = String+ defStatConfig :: StatConfig defStatConfig = StatConfig { reportStats = True, reportSamples = True, namespace = "",- statsPrefix = "stats",+ prefixStats = "stats", prefixCounter = "counters", prefixTimer = "timers", prefixGauge = "gauges", prefixSet = "sets",- server = "127.0.0.1",- port = 8125,+ statsdServer = "127.0.0.1",+ statsdPort = 8125, flushInterval = 1000, timingPercentiles = [90, 95],- newline = False+ appendNewline = False } newStatCounter ::- (MonadIO m) => Stats -> Key -> Sampling -> m StatCounter+ (MonadIO m) => Stats -> Key -> Int -> m StatCounter newStatCounter stats key sampling = do- newMetric stats key (CounterData 0)- return $ StatCounter stats key sampling+ newMetric stats (C.pack key) (CounterData 0)+ return $ StatCounter stats (C.pack key) sampling newStatGauge :: (MonadIO m) => Stats -> Key -> Int -> m StatGauge newStatGauge stats key ini = do- newMetric stats key (GaugeData ini)- return $ StatGauge stats key+ newMetric stats (C.pack key) (GaugeData ini)+ return $ StatGauge stats (C.pack key) newStatTiming :: (MonadIO m) => Stats -> Key -> Int -> m StatTiming newStatTiming stats key sampling = do- newMetric stats key (TimingData [])- return $ StatTiming stats key sampling+ newMetric stats (C.pack key) (TimingData [])+ return $ StatTiming stats (C.pack key) sampling newStatSet :: (MonadIO m) => Stats -> Key -> m StatSet newStatSet stats key = do- newMetric stats key (SetData HashSet.empty)- return $ StatSet stats key+ newMetric stats (C.pack key) (SetData HashSet.empty)+ return $ StatSet stats (C.pack key) incrementCounter :: (MonadIO m) => StatCounter -> Int -> m () incrementCounter StatCounter {..} =@@ -116,53 +112,13 @@ newSetElement :: (MonadIO m) => StatSet -> String -> m () newSetElement StatSet {..} =- processSample stats 1 key . Set+ processSample stats 1 key . Set . C.pack withStats :: (MonadUnliftIO m) => StatConfig -> (Stats -> m a) -> m a withStats cfg go = do- stats <- newStats cfg+ metrics <- newMetrics+ socket <- connectStatsD cfg.statsdServer cfg.statsdPort+ let stats = newStats cfg metrics socket if cfg.reportStats then withAsync (statsLoop stats) (\a -> link a >> go stats) else go stats--parseReport :: (MonadPlus m) => ByteString -> m Report-parseReport bs =- case C.split '|' bs of- [kv, t] -> do- (k, v) <- parseKeyValue kv t- return $ Report k v 1- [kv, t, r] -> do- (k, v) <- parseKeyValue kv t- x <- parseRate r- return $ Report k v x- _ -> mzero- where- parseRead :: (MonadPlus m, Read a) => String -> m a- parseRead = maybe mzero return . readMaybe- parseKeyValue kv t = do- case C.split ':' kv of- [k, v] -> do- key <- parseKey k- value <- parseValue v t- return (key, value)- _ -> mzero- parseKey k =- let s = C.unpack k- in if validateKey s- then return s- else mzero- parseValue v t =- let s = C.unpack v- in case t of- "c" -> Counter <$> parseRead s- "g" ->- case s of- '+' : n -> Gauge <$> parseRead n <*> pure True- '-' : _ -> Gauge <$> parseRead s <*> pure True- _ -> Gauge <$> parseRead s <*> pure False- "s" -> return $ Set s- "ms" -> Timing <$> parseRead s- _ -> mzero- parseRate r = case C.unpack r of- '@' : s -> parseRead s- _ -> mzero
src/System/Metrics/StatsD/Internal.hs view
@@ -7,19 +7,14 @@ module System.Metrics.StatsD.Internal ( Stats (..),+ newStats,+ StatParams,+ newParams, StatConfig (..),- Key,- Index,- Sampling,- Counter,- Gauge,- Timing,- SetElement,- Timings,- SetData, MetricData (..), Store (..), Metrics,+ newMetrics, Value (..), Sample (..), Report (..),@@ -33,7 +28,6 @@ addReading, newReading, processSample,- newStats, statsLoop, statsFlush, flushStats,@@ -53,24 +47,35 @@ median, flush, toReport,- format,+ formatReport, submit, connectStatsD,+ parseReport,+ parseRead,+ parseInt, ) where -import Control.Monad (forM_, forever, void, when)+import Control.Monad (MonadPlus (..), forM_, forever, void, when)+import Data.Bool (bool)+import Data.ByteString (ByteString)+import Data.ByteString qualified as B+import Data.ByteString.Builder (byteString, char8, intDec, string8, toLazyByteString) import Data.ByteString.Char8 qualified as C+import Data.ByteString.Lazy qualified as L import Data.Char (isAlphaNum, isAscii) import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict qualified as HashMap import Data.HashSet (HashSet) import Data.HashSet qualified as HashSet-import Data.List (intercalate, sort)+import Data.List (sort)+import Data.Vector (Vector, (!))+import Data.Vector qualified as V import Network.Socket (Socket) import Network.Socket qualified as Net import Network.Socket.ByteString qualified as Net import Text.Printf (printf)+import Text.Read (readMaybe) import UnliftIO (MonadIO, handleIO, liftIO, throwIO) import UnliftIO.Concurrent (threadDelay) import UnliftIO.STM@@ -83,115 +88,106 @@ stateTVar, ) -type Key = String- data Stats = Stats { metrics :: !(TVar Metrics),- cfg :: !StatConfig,- socket :: !Socket+ socket :: !Socket,+ params :: !StatParams } +data StatParams = StatParams+ { pfx :: !ByteString,+ pfxCounter :: !ByteString,+ pfxTimer :: !ByteString,+ pfxGauge :: !ByteString,+ pfxSet :: !ByteString,+ flush :: !Int,+ stats :: !Bool,+ samples :: !Bool,+ percentiles :: ![Int],+ newline :: !Bool+ }+ data StatConfig = StatConfig { reportStats :: !Bool, reportSamples :: !Bool, namespace :: !String,- statsPrefix :: !String,+ prefixStats :: !String, prefixCounter :: !String, prefixTimer :: !String, prefixGauge :: !String, prefixSet :: !String,- server :: !String,- port :: !Int,+ statsdServer :: !String,+ statsdPort :: !Int, flushInterval :: !Int, timingPercentiles :: ![Int],- newline :: !Bool+ appendNewline :: !Bool } deriving (Show, Read, Eq, Ord) -type Index = Int--type Sampling = Int--type Counter = Int--type Gauge = Int--type Timing = Int--type SetElement = String--type Timings = [Int]--type SetData = HashSet String- data MetricData- = CounterData !Counter- | GaugeData !Gauge- | TimingData !Timings- | SetData !(HashSet String)+ = CounterData !Int+ | GaugeData !Int+ | TimingData ![Int]+ | SetData !(HashSet ByteString) data Store = Store- { index :: !Index,+ { index :: !Int, dat :: !(Maybe MetricData) } -type Metrics = HashMap Key Store+type Metrics = HashMap ByteString Store data Value- = Counter !Counter- | Gauge !Gauge !Bool- | Timing !Timing- | Set !SetElement- | Metric !Int- | Other !String !String+ = Counter !Int+ | Gauge !Int !Bool+ | Timing !Int+ | Set !ByteString deriving (Eq, Ord, Show, Read) data Report = Report- { key :: !Key,+ { key :: !ByteString, value :: !Value, rate :: !Double } deriving (Eq, Ord, Show, Read) data Sample = Sample- { key :: !Key,+ { key :: !ByteString, value :: !Value,- sampling :: !Sampling,- index :: !Index+ sampling :: !Int,+ index :: !Int } deriving (Eq, Ord, Show, Read) data StatCounter = StatCounter { stats :: !Stats,- key :: !Key,- sampling :: !Sampling+ key :: !ByteString,+ sampling :: !Int } data StatGauge = StatGauge { stats :: !Stats,- key :: !Key+ key :: !ByteString } data StatTiming = StatTiming { stats :: !Stats,- key :: !Key,- sampling :: !Sampling+ key :: !ByteString,+ sampling :: !Int } data StatSet = StatSet { stats :: !Stats,- key :: !Key+ key :: !ByteString } -addMetric :: StatConfig -> Key -> MetricData -> Metrics -> Metrics-addMetric cfg key md =+addMetric :: StatParams -> ByteString -> MetricData -> Metrics -> Metrics+addMetric params key dat = HashMap.insert key $ Store 0 $- if cfg.reportStats- then Just md- else Nothing+ if params.stats then Just dat else Nothing -newMetric :: (MonadIO m) => Stats -> Key -> MetricData -> m ()+newMetric :: (MonadIO m) => Stats -> ByteString -> MetricData -> m () newMetric stats key store | validateKey key = do e <- atomically $ do@@ -199,21 +195,23 @@ if exists then return True else do- modifyTVar stats.metrics (addMetric stats.cfg key store)+ modifyTVar+ stats.metrics+ (addMetric stats.params key store) return False when e $ throwIO $ userError $- "A metric already exists with key: " <> key+ "A metric already exists with key: " <> C.unpack key | otherwise =- throwIO $ userError $ "Metric key is invalid: " <> key+ throwIO $ userError $ "Metric key is invalid: " <> C.unpack key -validateKey :: String -> Bool-validateKey t = not (null t) && all valid t+validateKey :: ByteString -> Bool+validateKey t = not (C.null t) && C.all valid t where valid c = elem c ("._-" :: [Char]) || isAscii c && isAlphaNum c -addReading :: Value -> Key -> Metrics -> Metrics+addReading :: Value -> ByteString -> Metrics -> Metrics addReading reading = HashMap.adjust adjust where adjust m = m {index = m.index + 1, dat = change <$> m.dat}@@ -225,108 +223,137 @@ (Set e, SetData s) -> SetData (HashSet.insert e s) _ -> error "Stats reading mismatch" -newReading :: Stats -> Key -> Value -> STM Int+newReading :: Stats -> ByteString -> Value -> STM Int newReading stats key reading = do modifyTVar stats.metrics (addReading reading key)- maybe 0 (.index) . HashMap.lookup key <$> readTVar stats.metrics+ maybe 0 (.index) . HashMap.lookup key+ <$> readTVar stats.metrics processSample ::- (MonadIO m) => Stats -> Sampling -> Key -> Value -> m ()+ (MonadIO m) => Stats -> Int -> ByteString -> Value -> m () processSample stats sampling key val = do idx <- atomically $ newReading stats key val- when stats.cfg.reportSamples $+ when stats.params.samples $ submit stats $ Sample key val sampling idx -newStats :: (MonadIO m) => StatConfig -> m Stats-newStats cfg = do- m <- newTVarIO HashMap.empty- h <- connectStatsD cfg.server cfg.port- return $ Stats m cfg h+newMetrics :: (MonadIO m) => m (TVar Metrics)+newMetrics = newTVarIO HashMap.empty +newParams :: StatConfig -> StatParams+newParams cfg =+ StatParams+ { pfx = p,+ pfxCounter = s <> C.pack cfg.prefixCounter <> ".",+ pfxGauge = s <> C.pack cfg.prefixGauge <> ".",+ pfxTimer = s <> C.pack cfg.prefixTimer <> ".",+ pfxSet = s <> C.pack cfg.prefixSet <> ".",+ newline = cfg.appendNewline,+ stats = cfg.reportStats,+ samples = cfg.reportSamples,+ percentiles = extractPercentiles cfg,+ flush = abs cfg.flushInterval+ }+ where+ p =+ if null cfg.namespace+ then ""+ else C.pack cfg.namespace <> "."+ s =+ if null cfg.prefixStats+ then p+ else p <> C.pack cfg.prefixStats <> "."++newStats :: StatConfig -> TVar Metrics -> Socket -> Stats+newStats cfg metrics socket =+ Stats+ { metrics = metrics,+ socket = socket,+ params = newParams cfg+ }+ statsLoop :: (MonadIO m) => Stats -> m () statsLoop stats = forever $ do- threadDelay $ stats.cfg.flushInterval * 1000+ threadDelay (stats.params.flush * 1000) statsFlush stats statsFlush :: (MonadIO m) => Stats -> m () statsFlush stats = do- reports <-- atomically $- stateTVar stats.metrics (flushStats stats.cfg)- mapM_ (send stats) reports+ mapM_ (send stats)+ =<< atomically+ (stateTVar stats.metrics (flushStats stats.params)) -flushStats :: StatConfig -> Metrics -> ([Report], Metrics)-flushStats cfg metrics =- let f xs key m = maybe xs ((<> xs) . statReports cfg key) m.dat+flushStats :: StatParams -> Metrics -> ([Report], Metrics)+flushStats params metrics =+ let f xs key m = maybe xs ((<> xs) . statReports params key) m.dat rs = HashMap.foldlWithKey' f [] metrics g m = m {dat = flush <$> m.dat} ms = HashMap.map g metrics in (rs, ms) -catKey :: [Key] -> Key-catKey = intercalate "." . filter (not . null)+catKey :: [ByteString] -> ByteString+catKey = C.intercalate "." . filter (not . C.null) -statReports :: StatConfig -> Key -> MetricData -> [Report]-statReports cfg key dat = case dat of+statReports :: StatParams -> ByteString -> MetricData -> [Report]+statReports params key dat = case dat of CounterData c -> [ Report- { key = catKey [cfg.statsPrefix, cfg.prefixCounter, key, "count"],+ { key = params.pfxCounter <> key <> ".count", value = Counter c, rate = 1.0 }, Report- { key = catKey [cfg.statsPrefix, cfg.prefixCounter, key, "rate"],- value = Counter (computeRate cfg c),+ { key = params.pfxCounter <> key <> ".rate",+ value = Counter (computeRate params c), rate = 1.0 } ] GaugeData s -> [ Report- { key = catKey [cfg.statsPrefix, cfg.prefixGauge, key],+ { key = params.pfxGauge <> key, value = Gauge s False, rate = 1.0 } ] SetData s -> [ Report- { key = catKey [cfg.statsPrefix, cfg.prefixSet, key, "count"],+ { key = params.pfxSet <> key <> ".count", value = Counter (HashSet.size s), rate = 1.0 } ]- TimingData s -> timingReports cfg key s+ TimingData s -> timingReports params key s data TimingStats = TimingStats- { timings :: ![Int],- cumsums :: ![Int],- cumsquares :: ![Int]+ { timings :: !(Vector Int),+ cumsums :: !(Vector Int),+ cumsquares :: !(Vector Int) } deriving (Eq, Ord, Show, Read) -makeTimingStats :: Timings -> TimingStats+makeTimingStats :: [Int] -> TimingStats makeTimingStats timings = TimingStats- { timings = sorted,- cumsums = cumulativeSums sorted,- cumsquares = cumulativeSquares sorted+ { timings = V.fromList sorted,+ cumsums = V.fromList (cumulativeSums sorted),+ cumsquares = V.fromList (cumulativeSquares sorted) } where sorted = sort timings extractPercentiles :: StatConfig -> [Int] extractPercentiles =- HashSet.toList+ (100 :)+ . HashSet.toList . HashSet.fromList . filter (\x -> x > 0 && x < 100) . (.timingPercentiles) -timingReports :: StatConfig -> Key -> Timings -> [Report]-timingReports cfg key timings =- concatMap (timingStats cfg key tstats) percentiles+timingReports :: StatParams -> ByteString -> [Int] -> [Report]+timingReports params key timings =+ concatMap (timingStats params key tstats) params.percentiles where tstats = makeTimingStats timings- percentiles = 100 : extractPercentiles cfg trimPercentile :: Int -> TimingStats -> TimingStats trimPercentile pc ts =@@ -336,50 +363,39 @@ cumsquares = f ts.cumsquares } where- f ls = take (length ls * pc `div` 100) ls+ f ls = V.take (length ls * pc `div` 100) ls -percentileSuffix :: Int -> String+percentileSuffix :: Int -> ByteString percentileSuffix pc- | 100 <= pc = ""- | 0 > pc = "0"- | otherwise = "_" <> show pc+ | pc == 100 = ""+ | otherwise = C.pack $ printf "_%d" pc -computeRate :: StatConfig -> Int -> Int-computeRate cfg i =- round (fromIntegral i * 1000.0 / fromIntegral cfg.flushInterval :: Double)+computeRate :: StatParams -> Int -> Int+computeRate params i = i * 1000 `div` params.flush mean :: TimingStats -> Int-mean ts = last ts.cumsums `div` length ts.timings+mean ts = V.last ts.cumsums `div` length ts.timings -timingStats :: StatConfig -> Key -> TimingStats -> Int -> [Report]-timingStats cfg key tstats pc =- mkr "count" (Counter (length ts.timings))- : [mkr "count_ps" (Counter rate) | 100 <= pc]- <> if null ts.timings- then []- else stats+timingStats :: StatParams -> ByteString -> TimingStats -> Int -> [Report]+timingStats params key tstats pc =+ concat+ [ [mkr "count" (Counter (length ts.timings))],+ [mkr "count_ps" (Counter rate) | pc == 100],+ [mkr "std" (Timing (stdev ts)) | pc == 100, not empty],+ [mkr "mean" (Timing (mean ts)) | not empty],+ [mkr "upper" (Timing (V.last ts.timings)) | not empty],+ [mkr "lower" (Timing (V.head ts.timings)) | not empty],+ [mkr "sum" (Timing (V.last ts.cumsums)) | not empty],+ [mkr "sum_squares" (Timing (V.last ts.cumsquares)) | not empty],+ [mkr "median" (Timing (median ts)) | not empty]+ ] where- k s =- catKey- [ cfg.statsPrefix,- cfg.prefixTimer,- key,- s <> percentileSuffix pc- ] ts = trimPercentile pc tstats- rate = computeRate cfg (length ts.timings)- mkr s v = Report {key = k s, value = v, rate = 1.0}- stats =- [ mkr "mean" (Timing (mean ts)),- mkr "upper" (Timing (last ts.timings)),- mkr "lower" (Timing (head ts.timings)),- mkr "sum" (Timing (last ts.cumsums)),- mkr "sum_squares" (Timing (last ts.cumsquares)),- mkr "median" (Timing (median ts))- ]- <> [ mkr "std" (Timing (stdev ts))- | 100 <= pc- ]+ empty = null ts.timings+ rate = computeRate params (length ts.timings)+ sfx = percentileSuffix pc+ pfx = params.pfxTimer <> key <> "."+ mkr s v = Report {key = pfx <> s <> sfx, value = v, rate = 1.0} cumulativeSums :: (Num a) => [a] -> [a] cumulativeSums = scanl1 (+)@@ -392,18 +408,18 @@ round $ sqrt var where len = length ts.timings- var = fromIntegral diffsum / fromIntegral len :: Double- diffsum = sum $ map ((^ (2 :: Int)) . subtract (mean ts)) ts.timings+ var = fromIntegral ds / fromIntegral len :: Double+ ds = sum $ map ((^ (2 :: Int)) . subtract (mean ts)) (V.toList ts.timings) median :: TimingStats -> Int median ts | null ts.timings = 0 | even (length ts.timings) =- let lower = ts.timings !! middle- upper = ts.timings !! subtract 1 middle+ let lower = ts.timings ! middle+ upper = ts.timings ! subtract 1 middle in (lower + upper) `div` 2 | otherwise =- ts.timings !! middle+ ts.timings ! middle where middle = length ts.timings `div` 2 @@ -424,31 +440,27 @@ } | otherwise = Nothing -format :: StatConfig -> Report -> String-format cfg report- | cfg.newline = printf "%s:%s\n" key val- | otherwise = printf "%s:%s" key val+formatReport :: Report -> ByteString+formatReport report = L.toStrict $ toLazyByteString builder where- key = catKey [cfg.namespace, report.key]+ builder = byteString report.key <> char8 ':' <> val rate- | report.rate < 1.0 = printf "|@%f" report.rate- | otherwise = "" :: String+ | report.rate < 1.0 =+ string8 $ printf "|@%f" report.rate+ | otherwise = mempty val = case report.value of Counter i ->- printf "%d|c%s" i rate+ intDec i <> "|c" <> rate Gauge g False ->- printf "%d|g" g+ intDec g <> "|g" Gauge g True ->- printf "%+d|g" g+ let sign = if 0 <= g then char8 '+' else mempty+ in sign <> intDec g <> "|g" Timing t ->- printf "%d|ms%s" t rate+ intDec t <> "|ms" <> rate Set e ->- printf "%s|s" e- Metric m ->- printf "%s|m" m- Other d t ->- printf "%s|%s" t d :: String+ byteString e <> "|s" submit :: (MonadIO m) => Stats -> Sample -> m () submit stats sample =@@ -459,8 +471,9 @@ liftIO $ handleIO (const (return ())) $ void $- Net.send stats.socket $- C.pack (format stats.cfg report)+ Net.send+ stats.socket+ (formatReport report <> bool "" "\n" stats.params.newline) connectStatsD :: (MonadIO m) => String -> Int -> m Socket connectStatsD host port = liftIO $ do@@ -475,3 +488,44 @@ sock <- Net.socket (Net.addrFamily a) Net.Datagram Net.defaultProtocol Net.connect sock (Net.addrAddress a) return sock++parseReport :: (MonadPlus m) => ByteString -> m Report+parseReport bs =+ case C.split '|' bs of+ [kv, t] -> do+ (k, v) <- parseKeyValue kv t+ return $ Report k v 1.0+ [kv, t, r] -> do+ (k, v) <- parseKeyValue kv t+ x <- parseRate r+ return $ Report k v x+ _ -> mzero+ where+ parseKeyValue kv t = do+ case C.split ':' kv of+ [key, v] -> do+ value <- parseValue v t+ return (key, value)+ _ -> mzero+ parseValue v t =+ case C.unpack t of+ "c" -> Counter <$> parseRead v+ "g" ->+ case C.uncons v of+ Just ('+', n) -> Gauge <$> parseInt n <*> pure True+ Just ('-', _) -> Gauge <$> parseInt v <*> pure True+ _ -> Gauge <$> parseRead v <*> pure False+ "s" -> return $ Set v+ "ms" -> Timing <$> parseRead v+ _ -> mzero+ parseRate r = case C.uncons r of+ Just ('@', s) -> parseRead s+ _ -> mzero++parseRead :: (MonadPlus m, Read a) => ByteString -> m a+parseRead = maybe mzero return . readMaybe . C.unpack++parseInt :: (MonadPlus m) => ByteString -> m Int+parseInt bs = case C.readInt bs of+ Just (i, bs') | B.null bs' -> return i+ _ -> mzero
statsd-rupp.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: statsd-rupp-version: 0.3.0.1+version: 0.4.0.0 synopsis: Simple StatsD Client description: Please see the README on GitHub at <https://github.com/jprupp/statsd-rupp#readme> category: System@@ -40,6 +40,7 @@ , network >=3.1.4.0 && <4 , unliftio >=0.2.25 && <2 , unordered-containers >=0.2.19.1 && <2+ , vector >=0.13.0 && <2 default-language: Haskell2010 test-suite statsd-rupp-test@@ -51,11 +52,13 @@ test ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N build-depends:- base >=4.7 && <5+ QuickCheck+ , base >=4.7 && <5 , bytestring >=0.11.5.1 && <2 , hspec , network >=3.1.4.0 && <4 , statsd-rupp , unliftio >=0.2.25 && <2 , unordered-containers >=0.2.19.1 && <2+ , vector >=0.13.0 && <2 default-language: Haskell2010
test/Spec.hs view
@@ -3,236 +3,251 @@ {-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -Wno-orphans #-} import Control.Monad-import Data.ByteString.Char8 qualified as B+import Data.ByteString (ByteString)+import Data.ByteString.Char8 qualified as C+import Data.Char import Data.HashMap.Strict qualified as HashMap import Data.List (sort) import Data.Maybe+import Data.Vector qualified as V import Network.Socket import Network.Socket.ByteString import System.Metrics.StatsD import System.Metrics.StatsD.Internal import Test.Hspec+import Test.Hspec.QuickCheck+import Test.QuickCheck import UnliftIO -testConfig :: StatConfig-testConfig = defStatConfig {flushInterval = 50}- main :: IO ()-main = hspec $ around (withMockStats testConfig) $ do- describe "counter" $ do- it "creates two counters" $ \(stats, _) -> do- _ <- newStatCounter stats "planets" 1- _ <- newStatCounter stats "moons" 1- return () :: Expectation- it "reports on two counters" $ \(stats, report) -> do- planets <- newStatCounter stats "planets" 1- moons <- newStatCounter stats "moons" 1- incrementCounter planets 10- incrementCounter moons 20- rs <- replicateM 2 report- let rs' =- [ Report "planets" (Counter 10) 1.0,- Report "moons" (Counter 20) 1.0- ]- rs `shouldMatchList` rs'- it "filters reported samples" $ \(stats, report) -> do- c <- newStatCounter stats "moons" 4- let counts = [11 .. 30]- forM_ counts (incrementCounter c)- rs <- replicateM 5 report- let rs' =- [ Report "moons" (Counter x) 0.25- | (i, x) <- zip [(1 :: Int) ..] counts,- i `mod` 4 == 0- ]- rs `shouldMatchList` rs'- r <- report- r.key `shouldNotBe` "moons"- it "reports stats" $ \(stats, report) -> do- c <- newStatCounter stats "planets" 4- replicateM_ 20 (incrementCounter c 10)- replicateM_ 5 report- do+main = hspec $ do+ describe "parser" $ do+ prop "formats and parses a report" $ \r ->+ (parseReport . formatReport) r `shouldBe` Just r+ describe "statistics" $ do+ prop "calculates timing lengths" $ \ps -> do+ let ts = makeTimingStats ps+ length ts.timings `shouldBe` length ps+ prop "calculates p75 and p95 timing lengths" $ \ps -> do+ let ts = makeTimingStats ps+ length (trimPercentile 95 ts).timings `shouldBe` length (percent 95 ps)+ length (trimPercentile 75 ts).timings `shouldBe` length (percent 75 ps)+ prop "computes median" $ \ps ->+ not (null ps) ==> do+ let ts = makeTimingStats ps+ median ts `shouldBe` med ps+ prop "computes p75 and p95 median" $ \ps ->+ 2 <= length ps ==> do+ let ts = makeTimingStats ps+ median (trimPercentile 95 ts) `shouldBe` med (percent 95 ps)+ median (trimPercentile 75 ts) `shouldBe` med (percent 75 ps)+ prop "computes mean" $ \ps ->+ not (null ps) ==> do+ let ts = makeTimingStats ps+ mean ts `shouldBe` mea ps+ prop "computes p75 and p95 mean" $ \ps ->+ 2 <= length ps ==> do+ let ts = makeTimingStats ps+ mean (trimPercentile 95 ts) `shouldBe` mea (percent 95 ps)+ mean (trimPercentile 75 ts) `shouldBe` mea (percent 75 ps)+ prop "computes stdev" $ \ps ->+ not (null ps) ==> do+ let ts = makeTimingStats ps+ stdev ts `shouldBe` std ps+ prop "computes sums" $ \ps ->+ not (null ps) ==> do+ let ts = makeTimingStats ps+ V.last ts.cumsums `shouldBe` sum ps+ prop "computes p75 and p95 sums" $ \ps ->+ 2 <= length ps ==> do+ let ts = makeTimingStats ps+ V.last (trimPercentile 95 ts).cumsums `shouldBe` sum (percent 95 ps)+ V.last (trimPercentile 75 ts).cumsums `shouldBe` sum (percent 75 ps)+ prop "computes sum of squares" $ \ps ->+ not (null ps) ==> do+ let ts = makeTimingStats ps+ V.last ts.cumsquares `shouldBe` sumsq ps+ prop "computes p75 and p95 sum of squares" $ \ps ->+ 2 <= length ps ==> do+ let ts = makeTimingStats ps+ V.last (trimPercentile 95 ts).cumsquares `shouldBe` sumsq (percent 95 ps)+ V.last (trimPercentile 75 ts).cumsquares `shouldBe` sumsq (percent 75 ps)+ around (withMockStats testConfig) $ do+ describe "counter" $ do+ it "creates two counters" $ \(stats, _) -> do+ _ <- newStatCounter stats "planets" 1+ _ <- newStatCounter stats "moons" 1+ return () :: Expectation+ it "reports on two counters" $ \(stats, report) -> do+ planets <- newStatCounter stats "planets" 1+ moons <- newStatCounter stats "moons" 1+ incrementCounter planets 10+ incrementCounter moons 20 rs <- replicateM 2 report let rs' =- [ Report "stats.counters.planets.count" (Counter 200) 1.0,- Report "stats.counters.planets.rate" (Counter 4000) 1.0+ [ Report "planets" (Counter 10) 1.0,+ Report "moons" (Counter 20) 1.0 ] rs `shouldMatchList` rs'- do- rs <- replicateM 2 report+ it "filters reported samples" $ \(stats, report) -> do+ c <- newStatCounter stats "moons" 4+ let counts = [11 .. 30]+ forM_ counts (incrementCounter c)+ rs <- replicateM 5 report let rs' =- [ Report "stats.counters.planets.count" (Counter 0) 1.0,- Report "stats.counters.planets.rate" (Counter 0) 1.0+ [ Report "moons" (Counter x) 0.25+ | (i, x) <- zip [(1 :: Int) ..] counts,+ i `mod` 4 == 0 ] rs `shouldMatchList` rs'- describe "timing" $ do- it "reports events" $ \(stats, report) -> do- let timings = [51 .. 55]- t <- newStatTiming stats "trips" 1- forM_ timings (addTiming t)- rs <- replicateM (length timings) report- let rs' = map (\n -> Report "trips" (Timing n) 1.0) timings- rs `shouldMatchList` rs'- it "reports empty stats" $ \(stats, report) -> do- _ <- newStatTiming stats "holes" 1- rs <- replicateM 8 report- let rs' =- concatMap- (replicate 2)- [ Report "stats.timers.holes.count" (Counter 0) 1.0,- Report "stats.timers.holes.count_ps" (Counter 0) 1.0,- Report "stats.timers.holes.count_90" (Counter 0) 1.0,- Report "stats.timers.holes.count_95" (Counter 0) 1.0- ]- rs `shouldMatchList` rs'- it "filters reported samples" $ \(stats, report) -> do- let timings = [1001 .. 2000]- t <- newStatTiming stats "cats" 100- forM_ timings (addTiming t)- rs <- replicateM 10 report- let rs' =- [ Report "cats" (Timing x) 0.01- | (i, x) <- zip [(1 :: Int) ..] timings,- i `mod` 100 == 0- ]- rs `shouldMatchList` rs'- it "computes stats" $ const $ do- let ps = reverse [5 .. 800] ++ [801 .. 1500]- p90 = take (length ps * 90 `div` 100) (sort ps)- p95 = take (length ps * 95 `div` 100) (sort ps)- mean' ls = sum ls `div` length ls- sumsq = sum . map (\x -> x * x)- stdev' ls =- let l = fromIntegral (length ls)- ds = map (subtract (mean' ls)) ls- s = fromIntegral $ sumsq ds :: Double- v = s / l- in round (sqrt v)- median' ls- | even (length ls) =- let x = length ls `div` 2- y = x - 1- in (sort ls !! x + sort ls !! y) `div` 2- | otherwise = sort ls !! (length ls `div` 2)- ts = makeTimingStats ps- t90 = trimPercentile 90 ts- t95 = trimPercentile 95 ts- length ts.timings `shouldBe` length ps- mean ts `shouldBe` mean' ps- stdev ts `shouldBe` stdev' ps- last ts.cumsquares `shouldBe` sumsq ps- last ts.cumsums `shouldBe` sum ps- median ts `shouldBe` median' ps- length t90.timings `shouldBe` length p90- mean t90 `shouldBe` mean' p90- stdev t90 `shouldBe` stdev' p90- last t90.cumsquares `shouldBe` sumsq p90- last t90.cumsums `shouldBe` sum p90- median t90 `shouldBe` median' p90- length t95.timings `shouldBe` length p95- mean t95 `shouldBe` mean' p95- stdev t95 `shouldBe` stdev' p95- last t95.cumsquares `shouldBe` sumsq p95- last t95.cumsums `shouldBe` sum p95- median t95 `shouldBe` median' p95- it "reports stats" $ \(stats, report) -> do- let timings = [5 .. 1500]- t <- newStatTiming stats "kittens" 0- forM_ timings (addTiming t)- let l = length timings- r k v = Report ("stats.timers.kittens." <> k) v 1.0- do+ r <- report+ r.key `shouldNotBe` "moons"+ it "reports stats" $ \(stats, report) -> do+ c <- newStatCounter stats "planets" 4+ replicateM_ 20 (incrementCounter c 10)+ replicateM_ 5 report+ do+ rs <- replicateM 2 report+ let rs' =+ [ Report "stats.counters.planets.count" (Counter 200) 1.0,+ Report "stats.counters.planets.rate" (Counter 4000) 1.0+ ]+ rs `shouldMatchList` rs'+ do+ rs <- replicateM 2 report+ let rs' =+ [ Report "stats.counters.planets.count" (Counter 0) 1.0,+ Report "stats.counters.planets.rate" (Counter 0) 1.0+ ]+ rs `shouldMatchList` rs'+ describe "timing" $ do+ it "reports events" $ \(stats, report) -> do+ let timings = [51 .. 55]+ t <- newStatTiming stats "trips" 1+ forM_ timings (addTiming t)+ rs <- replicateM (length timings) report+ let rs' = map (\n -> Report "trips" (Timing n) 1.0) timings+ rs `shouldMatchList` rs'+ it "reports empty stats" $ \(stats, report) -> do+ _ <- newStatTiming stats "holes" 1+ replicateM_ 2 $ do+ let rs' =+ [ Report "stats.timers.holes.count" (Counter 0) 1.0,+ Report "stats.timers.holes.count_ps" (Counter 0) 1.0,+ Report "stats.timers.holes.count_90" (Counter 0) 1.0,+ Report "stats.timers.holes.count_95" (Counter 0) 1.0+ ]+ rs <- replicateM (length rs') report+ rs `shouldMatchList` rs'+ it "filters reported samples" $ \(stats, report) -> do+ let timings = [1001 .. 2000]+ t <- newStatTiming stats "cats" 100+ forM_ timings (addTiming t)+ rs <- replicateM 10 report let rs' =- [ r "count" (Counter l),- r "count_ps" (Counter (l * 20)),- r "std" (Timing 432),- r "mean" (Timing 752),- r "lower" (Timing 5),- r "upper" (Timing 1500),- r "sum" (Timing 1125740),- r "sum_squares" (Timing 1126125220),- r "median" (Timing 752),- r "count_95" (Counter 1421),- r "mean_95" (Timing 715),- r "lower_95" (Timing 5),- r "upper_95" (Timing 1425),- r "sum_95" (Timing 1016015),- r "sum_squares_95" (Timing 965562395),- r "median_95" (Timing 715),- r "count_90" (Counter 1346),- r "mean_90" (Timing 677),- r "lower_90" (Timing 5),- r "upper_90" (Timing 1350),- r "sum_90" (Timing 911915),- r "sum_squares_90" (Timing 821036445),- r "median_90" (Timing 677)+ [ Report "cats" (Timing x) 0.01+ | (i, x) <- zip [(1 :: Int) ..] timings,+ i `mod` 100 == 0 ]- rs <- replicateM (length rs') report rs `shouldMatchList` rs'- do- let rs' =- concatMap- (replicate 2)- [ r "count" (Counter 0),- r "count_ps" (Counter 0),- r "count_90" (Counter 0),- r "count_95" (Counter 0)+ it "reports stats" $ \(stats, report) -> do+ let timings = [5 .. 1500]+ t <- newStatTiming stats "kittens" 0+ forM_ timings (addTiming t)+ let l = length timings+ r k v = Report ("stats.timers.kittens." <> k) v 1.0+ do+ let rs' =+ [ r "count" (Counter l),+ r "count_ps" (Counter (l * 20)),+ r "std" (Timing 432),+ r "mean" (Timing 752),+ r "lower" (Timing 5),+ r "upper" (Timing 1500),+ r "sum" (Timing 1125740),+ r "sum_squares" (Timing 1126125220),+ r "median" (Timing 752),+ r "count_95" (Counter 1421),+ r "mean_95" (Timing 715),+ r "lower_95" (Timing 5),+ r "upper_95" (Timing 1425),+ r "sum_95" (Timing 1016015),+ r "sum_squares_95" (Timing 965562395),+ r "median_95" (Timing 715),+ r "count_90" (Counter 1346),+ r "mean_90" (Timing 677),+ r "lower_90" (Timing 5),+ r "upper_90" (Timing 1350),+ r "sum_90" (Timing 911915),+ r "sum_squares_90" (Timing 821036445),+ r "median_90" (Timing 677) ]- rs <- replicateM (length rs') report+ rs <- replicateM (length rs') report+ rs `shouldMatchList` rs'+ do+ let rs' =+ concatMap+ (replicate 2)+ [ r "count" (Counter 0),+ r "count_ps" (Counter 0),+ r "count_90" (Counter 0),+ r "count_95" (Counter 0)+ ]+ rs <- replicateM (length rs') report+ rs `shouldMatchList` rs'+ describe "gauge" $ do+ it "reports set events" $ \(stats, report) -> do+ let gauges = [51 .. 55]+ g <- newStatGauge stats "speed" 50+ forM_ gauges (setGauge g)+ rs <- replicateM (length gauges) report+ let rs' = map (\n -> Report "speed" (Gauge n False) 1.0) gauges rs `shouldMatchList` rs'- describe "gauge" $ do- it "reports set events" $ \(stats, report) -> do- let gauges = [51 .. 55]- g <- newStatGauge stats "speed" 50- forM_ gauges (setGauge g)- rs <- replicateM (length gauges) report- let rs' = map (\n -> Report "speed" (Gauge n False) 1.0) gauges- rs `shouldMatchList` rs'- it "reports stats" $ \(stats, report) -> do- let gauges = [51 .. 55]- g <- newStatGauge stats "radius" 50- r <- report- r `shouldBe` Report "stats.gauges.radius" (Gauge 50 False) 1.0- forM_ gauges (setGauge g)- replicateM_ (length gauges) report- rs <- replicateM 2 report- let rs' = replicate 2 $ Report "stats.gauges.radius" (Gauge 55 False) 1.0- rs `shouldMatchList` rs'- it "increments and decrements value" $ \(stats, report) -> do- g <- newStatGauge stats "breadth" 50- incrementGauge g 5- report `shouldReturn` Report "breadth" (Gauge 5 True) 1.0- report `shouldReturn` Report "stats.gauges.breadth" (Gauge 55 False) 1.0- incrementGauge g (-10)- report `shouldReturn` Report "breadth" (Gauge (-10) True) 1.0- report `shouldReturn` Report "stats.gauges.breadth" (Gauge 45 False) 1.0- decrementGauge g 50- report `shouldReturn` Report "breadth" (Gauge (-50) True) 1.0- report `shouldReturn` Report "stats.gauges.breadth" (Gauge 0 False) 1.0- decrementGauge g (-25)- report `shouldReturn` Report "breadth" (Gauge 25 True) 1.0- report `shouldReturn` Report "stats.gauges.breadth" (Gauge 25 False) 1.0- describe "set" $ do- it "reports events" $ \(stats, report) -> do- let set = ["one", "two", "three", "three"]- s <- newStatSet stats "potatoes"- forM_ set (newSetElement s)- rs <- replicateM (length set) report- let rs' = map (\x -> Report "potatoes" (Set x) 1.0) set- rs `shouldMatchList` rs'- it "reports stats" $ \(stats, report) -> do- let set = ["two", "three", "one", "two", "three", "three"]- s <- newStatSet stats "bananas"- forM_ set (newSetElement s)- let rs' =- [ Report "stats.sets.bananas.count" (Counter 3) 1.0,- Report "stats.sets.bananas.count" (Counter 0) 1.0- ]- rs <- replicateM (length set + length rs') report- rs `shouldEndWith` rs'+ it "reports stats" $ \(stats, report) -> do+ let gauges = [51 .. 55]+ g <- newStatGauge stats "radius" 50+ r <- report+ r `shouldBe` Report "stats.gauges.radius" (Gauge 50 False) 1.0+ forM_ gauges (setGauge g)+ replicateM_ (length gauges) report+ rs <- replicateM 2 report+ let rs' = replicate 2 $ Report "stats.gauges.radius" (Gauge 55 False) 1.0+ rs `shouldMatchList` rs'+ it "increments and decrements value" $ \(stats, report) -> do+ g <- newStatGauge stats "breadth" 50+ incrementGauge g 5+ report `shouldReturn` Report "breadth" (Gauge 5 True) 1.0+ report `shouldReturn` Report "stats.gauges.breadth" (Gauge 55 False) 1.0+ incrementGauge g (-10)+ report `shouldReturn` Report "breadth" (Gauge (-10) True) 1.0+ report `shouldReturn` Report "stats.gauges.breadth" (Gauge 45 False) 1.0+ decrementGauge g 50+ report `shouldReturn` Report "breadth" (Gauge (-50) True) 1.0+ report `shouldReturn` Report "stats.gauges.breadth" (Gauge 0 False) 1.0+ decrementGauge g (-25)+ report `shouldReturn` Report "breadth" (Gauge 25 True) 1.0+ report `shouldReturn` Report "stats.gauges.breadth" (Gauge 25 False) 1.0+ describe "set" $ do+ it "reports events" $ \(stats, report) -> do+ let set = ["one", "two", "three", "three"]+ s <- newStatSet stats "potatoes"+ forM_ set (newSetElement s)+ rs <- replicateM (length set) report+ let rs' = map (\x -> Report "potatoes" (Set (C.pack x)) 1.0) set+ rs `shouldMatchList` rs'+ it "reports stats" $ \(stats, report) -> do+ let set = ["two", "three", "one", "two", "three", "three"]+ s <- newStatSet stats "bananas"+ forM_ set (newSetElement s)+ let rs' =+ [ Report "stats.sets.bananas.count" (Counter 3) 1.0,+ Report "stats.sets.bananas.count" (Counter 0) 1.0+ ]+ rs <- replicateM (length set + length rs') report+ rs `shouldEndWith` rs' withMockStats :: (MonadUnliftIO m) =>@@ -245,13 +260,13 @@ withAsync (fwd s2 q) $ \a1 -> do link a1 m <- newTVarIO HashMap.empty- let s = Stats m cfg {newline = True} s1+ let s = newStats cfg {appendNewline = True} m s1 withAsync (statsLoop s) $ \a2 -> do link a2 go (s, atomically (readTQueue q)) where fwd s2 q = forever $ do- bs <- liftIO $ B.lines <$> recv s2 (2 ^ (20 :: Int))+ bs <- liftIO $ C.lines <$> recv s2 (2 ^ (20 :: Int)) let rs = map (fromJust . parseReport) bs mapM_ (atomically . writeTQueue q) rs @@ -260,3 +275,75 @@ bracket (liftIO (socketPair AF_UNIX Stream 0)) (\(s1, s2) -> liftIO (close s1 >> close s2))++testConfig :: StatConfig+testConfig = defStatConfig {flushInterval = 50}++med :: [Int] -> Int+med ls+ | odd (length ls) =+ sort ls !! (length ls `div` 2)+ | otherwise =+ let ix = length ls `div` 2+ x = sort ls !! ix+ y = sort ls !! (ix - 1)+ in (x + y) `div` 2++mea :: [Int] -> Int+mea ls = sum ls `div` length ls++sumsq :: [Int] -> Int+sumsq = sum . map (\x -> x * x)++percent :: Int -> [Int] -> [Int]+percent pc ls = take (length ls * pc `div` 100) (sort ls)++std :: [Int] -> Int+std ls =+ let l = fromIntegral (length ls)+ ds = map (subtract (mea ls)) ls+ s = fromIntegral $ sumsq ds :: Double+ v = s / l+ in round (sqrt v)++genValidString :: Gen ByteString+genValidString =+ C.pack <$> listOf1 (arbitraryASCIIChar `suchThat` isValidChar)++isValidChar :: Char -> Bool+isValidChar c =+ isAscii c && (isAlpha c || isNumber c || isWhitelist c)+ where+ isWhitelist = flip elem (".-_" :: [Char])++genRate :: Gen Double+genRate = arbitrary `suchThat` \n -> n > 0.0 && n <= 1.0++genNatural :: Gen Int+genNatural = arbitrary `suchThat` (0 <=)++instance Arbitrary Value where+ arbitrary =+ oneof [counter, gauge, timing, set]+ where+ counter = Counter <$> genNatural+ timing = Timing <$> arbitrary+ set = Set <$> genValidString+ gauge = do+ t <- arbitrary+ v <-+ if t+ then arbitrary+ else genNatural+ return $ Gauge v t++instance Arbitrary Report where+ arbitrary = do+ key <- genValidString+ value <- arbitrary+ rate <- case value of+ Counter {} -> genRate+ Gauge {} -> pure 1.0+ Timing {} -> genRate+ Set {} -> pure 1.0+ return $ Report key value rate