-
Notifications
You must be signed in to change notification settings - Fork 39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dimensional counters #32
base: master
Are you sure you want to change the base?
Changes from 5 commits
e86c5d9
09e8a44
1a77ab4
de569d4
1528538
b9bb4db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,13 +49,18 @@ module System.Metrics | |
, registerLabel | ||
, registerDistribution | ||
, registerGroup | ||
, registerDimensional | ||
|
||
-- ** Convenience functions | ||
-- $convenience | ||
, createCounter | ||
, createGauge | ||
, createLabel | ||
, createDistribution | ||
, createDimensionalCounter | ||
, createDimensionalGauge | ||
, createDimensionalLabel | ||
, createDimensionalDistribution | ||
|
||
-- ** Predefined metrics | ||
-- $predefined | ||
|
@@ -70,7 +75,10 @@ module System.Metrics | |
|
||
import Control.Applicative ((<$>)) | ||
import Control.Monad (forM) | ||
import Data.Either (partitionEithers) | ||
import Data.Int (Int64) | ||
import Data.Traversable (traverse) | ||
import Data.Monoid ((<>)) | ||
import qualified Data.IntMap.Strict as IM | ||
import Data.IORef (IORef, atomicModifyIORef, newIORef, readIORef) | ||
import qualified Data.HashMap.Strict as M | ||
|
@@ -86,6 +94,8 @@ import System.Metrics.Gauge (Gauge) | |
import qualified System.Metrics.Gauge as Gauge | ||
import System.Metrics.Label (Label) | ||
import qualified System.Metrics.Label as Label | ||
import System.Metrics.Dimensional (Dimensional) | ||
import qualified System.Metrics.Dimensional as Dimensional | ||
|
||
-- $naming | ||
-- Compound metric names should be separated using underscores. | ||
|
@@ -137,6 +147,7 @@ data MetricSampler = CounterS !(IO Int64) | |
| GaugeS !(IO Int64) | ||
| LabelS !(IO T.Text) | ||
| DistributionS !(IO Distribution.Stats) | ||
| DimensionalS !Dimensional.Dimensions !(IO (M.HashMap Dimensional.Point Value)) | ||
|
||
-- | Create a new, empty metric store. | ||
newStore :: IO Store | ||
|
@@ -192,6 +203,16 @@ registerDistribution | |
registerDistribution name sample store = | ||
register name (DistributionS sample) store | ||
|
||
-- | Registers a dimensional metric. | ||
registerDimensional | ||
:: Dimensional a | ||
-> (a -> IO Value) -- ^ Function to sample the dimensional metric to a Value. | ||
-> Store -- ^ Metric store | ||
-> IO () | ||
registerDimensional d f store = | ||
let x = readIORef (Dimensional.dimensionalPoints d) >>= traverse f in | ||
register (Dimensional.dimensionalName d) (DimensionalS (Dimensional.dimensionalDimensions d) x) store | ||
|
||
register :: T.Text | ||
-> MetricSampler | ||
-> Store | ||
|
@@ -323,6 +344,48 @@ createDistribution name store = do | |
registerDistribution name (Distribution.read event) store | ||
return event | ||
|
||
-- | Create and register a zero-initialized dimensional counter. | ||
createDimensionalCounter :: T.Text -- ^ Counter name | ||
-> Store -- ^ Metric store | ||
-> Dimensional.Dimensions -- ^ Dimension names. | ||
-> IO (Dimensional Counter) | ||
createDimensionalCounter name store dims = do | ||
counter <- Dimensional.newDimensional Counter.new name "" dims | ||
registerDimensional counter (fmap Counter . Counter.read) store | ||
return counter | ||
|
||
-- | Create and register a zero-initialized dimensional gauge. | ||
createDimensionalGauge :: T.Text -- ^ Gauge name | ||
-> Store -- ^ Metric store | ||
-> Dimensional.Dimensions -- ^ Dimension names. | ||
-> IO (Dimensional Gauge) | ||
createDimensionalGauge name store dims = do | ||
gauge <- Dimensional.newDimensional Gauge.new name "" dims | ||
registerDimensional gauge (fmap Gauge . Gauge.read) store | ||
return gauge | ||
|
||
-- | Create and register a zero-initialized dimensional label. | ||
createDimensionalLabel :: T.Text -- ^ Label name | ||
-> Store -- ^ Metric store | ||
-> Dimensional.Dimensions -- ^ Dimension names. | ||
-> IO (Dimensional Label) | ||
createDimensionalLabel name store dims = do | ||
label <- Dimensional.newDimensional Label.new name "" dims | ||
registerDimensional label (fmap Label . Label.read) store | ||
return label | ||
|
||
-- | Create and register a zero-initialized dimensional distribution. | ||
createDimensionalDistribution :: T.Text -- ^ Distribution name | ||
-> Store -- ^ Metric store | ||
-> Dimensional.Dimensions -- ^ Dimension names. | ||
-> IO (Dimensional Distribution) | ||
createDimensionalDistribution name store dims = do | ||
distribution <- Dimensional.newDimensional Distribution.new name "" dims | ||
registerDimensional distribution (fmap Distribution . Distribution.read) store | ||
return distribution | ||
|
||
|
||
|
||
------------------------------------------------------------------------ | ||
-- * Predefined metrics | ||
|
||
|
@@ -590,7 +653,7 @@ gcParTotBytesCopied = Stats.parAvgBytesCopied | |
-- metrics atomically. | ||
|
||
-- | A sample of some metrics. | ||
type Sample = M.HashMap T.Text Value | ||
type Sample = M.HashMap (T.Text, [(T.Text, T.Text)]) Value | ||
|
||
-- | Sample all metrics. Sampling is /not/ atomic in the sense that | ||
-- some metrics might have been mutated before they're sampled but | ||
|
@@ -601,9 +664,10 @@ sampleAll store = do | |
let metrics = stateMetrics state | ||
groups = stateGroups state | ||
cbSample <- sampleGroups $ IM.elems groups | ||
sample <- readAllRefs metrics | ||
let allSamples = sample ++ cbSample | ||
return $! M.fromList allSamples | ||
(dimSamples, sample) <- partitionEithers <$> readAllRefs metrics | ||
let noDimSamples = [((k,[]),v) | (k,v) <- sample ++ cbSample] | ||
let flatDimSamples = [ ((k,zip ds dvs),v) | (k, ds, pointVals) <- dimSamples, (dvs,v) <- pointVals] | ||
return $! M.fromList (noDimSamples ++ flatDimSamples) | ||
|
||
-- | Sample all metric groups. | ||
sampleGroups :: [GroupSampler] -> IO [(T.Text, Value)] | ||
|
@@ -621,17 +685,22 @@ data Value = Counter {-# UNPACK #-} !Int64 | |
| Distribution !Distribution.Stats | ||
deriving (Eq, Show) | ||
|
||
sampleOne :: MetricSampler -> IO Value | ||
sampleOne (CounterS m) = Counter <$> m | ||
sampleOne (GaugeS m) = Gauge <$> m | ||
sampleOne (LabelS m) = Label <$> m | ||
sampleOne (DistributionS m) = Distribution <$> m | ||
type Value2 = Either (Dimensional.Dimensions, [(Dimensional.Point, Value)]) Value | ||
|
||
sampleOne :: MetricSampler -> IO Value2 | ||
sampleOne (CounterS m) = Right . Counter <$> m | ||
sampleOne (GaugeS m) = Right . Gauge <$> m | ||
sampleOne (LabelS m) = Right . Label <$> m | ||
sampleOne (DistributionS m) = Right . Distribution <$> m | ||
sampleOne (DimensionalS dims m) = Left . (\pairs -> (dims, pairs)) . M.toList <$> m | ||
|
||
-- | Get a snapshot of all values. Note that we're not guaranteed to | ||
-- see a consistent snapshot of the whole map. | ||
readAllRefs :: M.HashMap T.Text (Either MetricSampler GroupId) | ||
-> IO [(T.Text, Value)] | ||
-> IO [Either (T.Text, Dimensional.Dimensions, [(Dimensional.Point, Value)]) (T.Text, Value)] | ||
readAllRefs m = do | ||
forM ([(name, ref) | (name, Left ref) <- M.toList m]) $ \ (name, ref) -> do | ||
val <- sampleOne ref | ||
return (name, val) | ||
return $ case val of | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably want |
||
Left (dims, pairs) -> Left (name, dims, pairs) | ||
Right v -> Right (name, v) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
{-# LANGUAGE LambdaCase #-} | ||
{-# LANGUAGE OverloadedStrings #-} | ||
{-# LANGUAGE DeriveDataTypeable #-} | ||
|
||
-- | This module defines a type for mutable dimensional string-valued label. | ||
-- Dimensional label are variable values and can be used to track e.g. the | ||
-- current number of call to an api endpoint by its http return code. | ||
-- All operations on Dimensional label are thread-safe. | ||
module System.Metrics.Dimensional where | ||
|
||
import qualified System.Metrics.Counter as Counter | ||
import qualified System.Metrics.Distribution as Distribution | ||
import qualified System.Metrics.Gauge as Gauge | ||
import qualified System.Metrics.Label as Label | ||
|
||
import Control.Applicative ((<$>), pure) | ||
import Control.Exception (throwIO) | ||
import Data.HashMap.Strict (HashMap, empty) | ||
import qualified Data.HashMap.Strict as HashMap | ||
import Data.IORef (IORef, atomicModifyIORef', newIORef, readIORef) | ||
import Data.Text (Text) | ||
import Data.Typeable | ||
import GHC.Exception (Exception) | ||
import Prelude hiding (lookup) | ||
|
||
type Name = Text | ||
|
||
type Explanation = Text | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bit more of an overall comment but I think all metrics should support an explanation (although I haven't given thought to how to retrofit this into the current API) and each dimension could also use an explanation. In general I think one might want to attach the following metadata to a metric:
|
||
|
||
type Dimensions = [Text] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be convenient to allow a dimension to be a member of some |
||
|
||
type Point = [Text] | ||
|
||
-- | Dimensional metrics storage | ||
data Dimensional a = Dimensional | ||
{ dimensionalCreate :: !(IO a) | ||
, dimensionalName :: !Name | ||
, dimensionalExplanation :: !Explanation | ||
, dimensionalDimensions :: !Dimensions | ||
, dimensionalPoints :: !(IORef (HashMap Point a)) | ||
} | ||
|
||
-- | Create a new empty dimensional metrics | ||
newDimensional :: | ||
IO a -> Name -> Explanation -> Dimensions -> IO (Dimensional a) | ||
newDimensional new name explanation dimensions = | ||
Dimensional new name explanation dimensions <$> newIORef empty | ||
|
||
data DimensionError = | ||
DimensionError !Name | ||
!Dimensions | ||
!Point | ||
deriving (Show, Ord, Eq, Typeable) | ||
|
||
instance Exception DimensionError | ||
|
||
data LookupFailure | ||
= UnmatchedDimensions DimensionError | ||
| NotFound | ||
deriving (Show, Ord, Eq) | ||
|
||
-- | Returns dimensional metric with specified labels | ||
lookup :: Dimensional a -> Point -> IO (Either LookupFailure a) | ||
lookup d pt | ||
| not $ matchDimensions (dimensionalDimensions d) pt = | ||
pure $ Left (UnmatchedDimensions err) | ||
| otherwise = do | ||
toLookupResult . HashMap.lookup pt <$> readIORef (dimensionalPoints d) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably want to force the result. |
||
where | ||
err :: DimensionError | ||
err = DimensionError (dimensionalName d) (dimensionalDimensions d) pt | ||
toLookupResult Nothing = Left NotFound | ||
toLookupResult (Just x) = Right x | ||
|
||
matchDimensions :: Dimensions -> Point -> Bool | ||
matchDimensions ds ps = length ds == length ps | ||
|
||
-- | Initialize a new empty dimensional metric with specified labels | ||
create :: Dimensional a -> Point -> IO a | ||
create d pt | ||
| not $ matchDimensions (dimensionalDimensions d) pt = throwIO err | ||
| otherwise = do | ||
v <- dimensionalCreate d | ||
atomicModifyIORef' | ||
(dimensionalPoints d) | ||
(\store -> (HashMap.insert pt v store, ())) | ||
return v | ||
where | ||
err :: DimensionError | ||
err = DimensionError (dimensionalName d) (dimensionalDimensions d) pt | ||
|
||
-- | Returns dimensional metric with specified labels, creating it if not exists | ||
lookupOrCreate :: Dimensional a -> Point -> IO a | ||
lookupOrCreate d pt = | ||
lookup d pt >>= \case | ||
Left NotFound -> create d pt | ||
Left (UnmatchedDimensions err) -> throwIO err | ||
Right x -> return x | ||
|
||
type Counter = Dimensional Counter.Counter | ||
|
||
type Gauge = Dimensional Gauge.Gauge | ||
|
||
type Label = Dimensional Label.Label | ||
|
||
type Distribution = Dimensional Distribution.Distribution |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import qualified System.Metrics.Counter as Counter | ||
import System.Metrics.Dimensional | ||
|
||
main :: IO () | ||
main = do | ||
c <- | ||
newDimensional | ||
Counter.new | ||
"wai.response" | ||
"endpoints status response" | ||
["url", "status"] | ||
let url = "/hello" | ||
let status = "200" | ||
x <- lookupOrCreate c [url, status] | ||
Counter.inc x |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might want to use a strict
<$>
to force the result to avoid creating thunks. It's quite important that sampling is efficient otherwise we might be slowing down the application with a background task such as monitoring.