Skip to content

Dimensional counters #22

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

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 79 additions & 11 deletions System/Metrics.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -70,7 +75,9 @@ module System.Metrics

import Control.Applicative ((<$>))
import Control.Monad (forM)
import Data.Either (partitionEithers)
import Data.Int (Int64)
import Data.Monoid ((<>))
import qualified Data.IntMap.Strict as IM
import Data.IORef (IORef, atomicModifyIORef, newIORef, readIORef)
import qualified Data.HashMap.Strict as M
Expand All @@ -86,6 +93,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.
Expand Down Expand Up @@ -137,6 +146,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
Expand Down Expand Up @@ -192,6 +202,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._points d) >>= traverse f in
register (Dimensional._name d) (DimensionalS (Dimensional._dimensions d) x) store

register :: T.Text
-> MetricSampler
-> Store
Expand Down Expand Up @@ -323,6 +343,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

Expand Down Expand Up @@ -512,7 +574,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
Expand All @@ -523,9 +585,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)]
Expand All @@ -543,17 +606,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
Left (dims, pairs) -> Left (name, dims, pairs)
Right v -> Right (name, v)
88 changes: 88 additions & 0 deletions System/Metrics/Dimensional.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
{-# LANGUAGE LambdaCase #-}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file needs some top-level Haddocks explaining how to use this module and what it's for.

{-# LANGUAGE OverloadedStrings #-}
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 Prelude hiding (lookup)
import GHC.Exception (Exception)
import Control.Exception (throwIO)
import Data.Text (Text)
import Data.IORef (IORef, atomicModifyIORef', newIORef, readIORef)
import Data.HashMap.Strict (HashMap, empty)
import qualified Data.HashMap.Strict as HashMap

type Name = Text
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lots of Haddoc docs needed here.

type Explanation = Text
type Dimensions = [Text]
type Point = [Text]

data Dimensional a = Dimensional {
_create :: !(IO a)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please stick to the field naming pattern used for other data types (i.e. no leading underscore, but prefixed as needed).

, _name :: !Name
, _explanation :: !Explanation
, _dimensions :: !Dimensions
, _points :: !(IORef (HashMap Point a))
}

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)
instance Exception DimensionError

data LookupFailure
= UnmatchedDimensions DimensionError
| NotFound
deriving (Show, Ord, Eq)

lookup :: Dimensional a -> Point -> IO (Either LookupFailure a)
lookup d pt
| not $ matchDimensions (_dimensions d) pt = pure $ Left (UnmatchedDimensions err)
| otherwise = do
toLookupResult . HashMap.lookup pt <$> readIORef (_points d)
where
err :: DimensionError
err = DimensionError (_name d) (_dimensions d) pt

toLookupResult Nothing = Left NotFound
toLookupResult (Just x) = Right x

matchDimensions :: Dimensions -> Point -> Bool
matchDimensions ds ps = length ds == length ps

create :: Dimensional a -> Point -> IO a
create d pt
| not $ matchDimensions (_dimensions d) pt = throwIO err
| otherwise = do
v <- _create d
atomicModifyIORef' (_points d) (\store -> (HashMap.insert pt v store, ()))
return v
where
err :: DimensionError
err = DimensionError (_name d) (_dimensions d) pt

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

example :: IO ()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This belongs in the examples/ directory and/or in some docs.

example = do
c <- newDimensional Counter.new "foo" "a foo" ["url", "status"]
let url = "/hello"
let status = "200"
x <- lookupOrCreate c [url, status]
Counter.inc x
3 changes: 2 additions & 1 deletion ekg-core.cabal
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: ekg-core
version: 0.1.1.3
version: 0.2.0.0
synopsis: Tracking of system metrics
description:
This library lets you defined and track system metrics.
Expand All @@ -20,6 +20,7 @@ library
exposed-modules:
System.Metrics
System.Metrics.Counter
System.Metrics.Dimensional
System.Metrics.Distribution
System.Metrics.Distribution.Internal
System.Metrics.Gauge
Expand Down