{-# language BangPatterns #-}
{-# language OverloadedStrings #-}
module Prometheus.Metric.Histogram (
Histogram
, Bucket
, histogram
, defaultBuckets
, exponentialBuckets
, linearBuckets
, BucketCounts(..)
, insert
, emptyCounts
, getHistogram
) where
import Prometheus.Info
import Prometheus.Metric
import Prometheus.Metric.Observer
import Prometheus.MonadMonitor
import Control.Applicative ((<$>))
import qualified Control.Concurrent.STM as STM
import Control.DeepSeq
import Control.Monad.IO.Class
import qualified Data.ByteString.UTF8 as BS
import qualified Data.Map.Strict as Map
import Data.Monoid ((<>))
import Data.Text (Text)
import qualified Data.Text as T
import Numeric (showFFloat)
newtype Histogram = MkHistogram (STM.TVar BucketCounts)
instance NFData Histogram where
rnf (MkHistogram a) = seq a ()
histogram :: Info -> [Bucket] -> Metric Histogram
histogram info buckets = Metric $ do
countsTVar <- STM.newTVarIO (emptyCounts buckets)
return (MkHistogram countsTVar, collectHistogram info countsTVar)
type Bucket = Double
data BucketCounts = BucketCounts {
histTotal :: !Double
, histCount :: !Int
, histCountsPerBucket :: !(Map.Map Bucket Int)
} deriving (Show, Eq, Ord)
emptyCounts :: [Bucket] -> BucketCounts
emptyCounts buckets
| isStrictlyIncreasing buckets = BucketCounts 0 0 $ Map.fromList (zip buckets (repeat 0))
| otherwise = error ("Histogram buckets must be in increasing order, got: " ++ show buckets)
where
isStrictlyIncreasing xs = and (zipWith (<) xs (tail xs))
instance Observer Histogram where
observe h v = withHistogram h (insert v)
withHistogram :: MonadMonitor m
=> Histogram -> (BucketCounts -> BucketCounts) -> m ()
withHistogram (MkHistogram !bucketCounts) f =
doIO $ STM.atomically $ STM.modifyTVar' bucketCounts f
getHistogram :: MonadIO m => Histogram -> m (Map.Map Bucket Int)
getHistogram (MkHistogram bucketsTVar) =
liftIO $ histCountsPerBucket <$> STM.atomically (STM.readTVar bucketsTVar)
insert :: Double -> BucketCounts -> BucketCounts
insert value BucketCounts { histTotal = total, histCount = count, histCountsPerBucket = counts } =
BucketCounts (total + value) (count + 1) incCounts
where
incCounts =
case Map.lookupGE value counts of
Nothing -> counts
Just (upperBound, _) -> Map.adjust (+1) upperBound counts
collectHistogram :: Info -> STM.TVar BucketCounts -> IO [SampleGroup]
collectHistogram info bucketCounts = STM.atomically $ do
BucketCounts total count counts <- STM.readTVar bucketCounts
let sumSample = Sample (name <> "_sum") [] (bsShow total)
let countSample = Sample (name <> "_count") [] (bsShow count)
let infSample = Sample (name <> "_bucket") [(bucketLabel, "+Inf")] (bsShow count)
let samples = map toSample (cumulativeSum (Map.toAscList counts))
return [SampleGroup info HistogramType $ samples ++ [infSample, sumSample, countSample]]
where
toSample (upperBound, count') =
Sample (name <> "_bucket") [(bucketLabel, formatFloat upperBound)] $ bsShow count'
name = metricName info
formatFloat x = T.pack (showFFloat Nothing x "")
cumulativeSum xs = zip (map fst xs) (scanl1 (+) (map snd xs))
bsShow :: Show s => s -> BS.ByteString
bsShow = BS.fromString . show
bucketLabel :: Text
bucketLabel = "le"
defaultBuckets :: [Double]
defaultBuckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
linearBuckets :: Bucket -> Double -> Int -> [Bucket]
linearBuckets start width count
| count <= 0 = error ("Must provide a positive number of linear buckets, got: " ++ show count)
| otherwise = take count (iterate (width+) start)
exponentialBuckets :: Bucket -> Double -> Int -> [Bucket]
exponentialBuckets start factor count
| count <= 0 = error ("Must provide a positive number of exponential buckets, got: " ++ show count)
| factor <= 1 = error ("Exponential buckets must have factor greater than 1 to ensure upper bounds are monotonically increasing, got: " ++ show factor)
| start <= 0 = error ("Exponential buckets must have positive number for start bucket to ensure upper bounds are monotonically increasing, got: " ++ show start)
| otherwise = take count (iterate (factor*) start)