-
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 all 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 |
---|---|---|
@@ -0,0 +1,23 @@ | ||
{-# LANGUAGE CPP #-} | ||
|
||
module System.Compat | ||
#if !MIN_VERSION_base(4,8,0) | ||
((<$!>)) | ||
#else | ||
#endif | ||
where | ||
|
||
-- | Import <$!> from base-4.8 for ghc 7 compat | ||
#if !MIN_VERSION_base(4,8,0) | ||
infixl 4 <$!> | ||
|
||
-- | Strict version of 'Data.Functor.<$>'. | ||
-- | ||
-- @since 4.8.0.0 | ||
(<$!>) :: Monad m => (a -> b) -> m a -> m b | ||
{-# INLINE (<$!>) #-} | ||
f <$!> m = do | ||
x <- m | ||
let z = f x | ||
z `seq` return z | ||
#endif |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
{-# LANGUAGE LambdaCase #-} | ||
{-# LANGUAGE OverloadedStrings #-} | ||
{-# LANGUAGE DeriveDataTypeable #-} | ||
{-# LANGUAGE CPP #-} | ||
|
||
-- | 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) | ||
#if MIN_VERSION_base(4,8,0) | ||
import Control.Monad ((<$!>)) | ||
#else | ||
import System.Compat ((<$!>)) | ||
#endif | ||
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 | ||
|
||
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) | ||
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.
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: