monad-metrics (empty) → 0.1.0.0
raw patch · 7 files changed
+457/−0 lines, 7 filesdep +basedep +clockdep +containerssetup-changed
Dependencies added: base, clock, containers, ekg-core, monad-metrics, mtl, text, transformers
Files
- LICENSE +21/−0
- README.md +138/−0
- Setup.hs +2/−0
- monad-metrics.cabal +40/−0
- src/Control/Monad/Metrics.hs +210/−0
- src/Control/Monad/Metrics/Internal.hs +44/−0
- test/Spec.hs +2/−0
+ LICENSE view
@@ -0,0 +1,21 @@+The MIT License++Copyright (c) 2017 Taylor Fausak, Seller Labs++Permission is hereby granted, free of charge, to any person obtaining a copy of+this software and associated documentation files (the "Software"), to deal in+the Software without restriction, including without limitation the rights to+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies+of the Software, and to permit persons to whom the Software is furnished to do+so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,138 @@+#!/usr/bin/env stack+> -- stack --install-ghc runghc --package monad-metrics --package turtle --package markdown-unlit -- "-pgmL markdown-unlit"++# `monad-metrics`++[](https://travis-ci.org/sellerlabs/monad-metrics)++This library defines a convenient wrapper and API for using [EKG][] metrics in+your application. It's heavily inspired by the metrics code that Taylor Fausak+used in his Haskell application [blunt](https://github.com/tfausak/blunt).++# Usage++This [README is an executable literate Haskell+file](https://github.com/silky/literate-readme). If you have [stack][] installed, then you can run the file with:++```+./README.lhs+```++We'll need to start with the import/pragma boilerplate:++```haskell+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE NoMonomorphismRestriction #-}++import qualified Control.Monad.Metrics as Metrics+import Control.Monad.Metrics (Metrics, Resolution(..), MonadMetrics)+import Control.Monad.Reader+import qualified System.Metrics as EKG+```++The `Control.Monad.Metrics` module is designed to be imported qualified.++### Initialize!++First, you need to initialize the `Metrics` data type. You can do so using+`initialize` (to create a new EKG store) or `initializeWith` if you want to+pass a preexisting store.++```haskell+initializing :: Bool -> EKG.Store -> IO Metrics+initializing True store = Metrics.initializeWith store+initializing False _ = Metrics.initialize+```++### Embed!++The next step is to implement an instance of the class `MonadMetrics` for your+monad transformer stack. This library has explicitly decided not to provide a+concrete monad transformer to reduce the dependency footprint. Fortunately,+it's pretty easy!++Suppose you've got the following stack:++```haskell+type App = ReaderT Config IO++data Config = Config { configMetrics :: Metrics }+```++then you can easily get the required instance with:++```haskell+instance MonadMetrics (ReaderT Config IO) where+ getMetrics = asks configMetrics+```++Now, you're off to the races! Let's record some metrics.++### Measure!++Once your application has the required instance, you can use [EKG][]'s metrics+(counters, gauges, labels, distributions). ++For detailed descriptions of the various metric types, see the corresponding [EKG][] documentation:++- [Counter][]+- [Distribution][]+- [Gauge][]+- [Label][]++Generally, the library provides "sane default" functions which accept the name+of the metric to work with and the value to contribute to that metric.++```haskell+w = Metrics.label "Foo" "Bar"+x = Metrics.counter "MetricName" 6+y = Metrics.distribution "Distribute" 3.4+z = Metrics.gauge "Gauge" 7+```++Generalized versions of these functions are available with an apostrophe. Labels accept any `Show`able value, while gauges and counters accept any `Integral` value.++```haskell+a = Metrics.label' "List" [1,2,3]+b = Metrics.counter' "Count" (3 :: Integer)+```++#### Timers++You can time actions with `timed`, which has a resolution of seconds. You can+use `timed'` which accepts a `Resolution` argument to provide a different+scale.++```haskell+timedProcess :: App Int+timedProcess = + Metrics.timed "summing1" $ do+ pure $! sum [1 .. 100000]++timedInMilliseconds :: App Int+timedInMilliseconds = + Metrics.timed' Microseconds "summing2" $ do+ pure $! sum [1..100]+```++# A demonstration++```haskell+main :: IO ()+main = do+ metrics <- Metrics.initialize+ flip runReaderT (Config metrics) $ do+ Metrics.label "ProgramName" "README"+ forM_ [1..10] $ \_ -> do+ Metrics.increment "up-to-ten"+ Metrics.timed' Nanoseconds "Whatever" $ do+ liftIO $ putStrLn "Hello World!" +```++[EKG]: http://hackage.haskell.org/package/ekg-core+[stack]: https://www.haskellstack.org/+[Counter]: http://hackage.haskell.org/package/ekg-core-0.1.1.1/docs/System-Metrics-Counter.html+[Gauge]: http://hackage.haskell.org/package/ekg-core-0.1.1.1/docs/System-Metrics-Gauge.html+[Distribution]: http://hackage.haskell.org/package/ekg-core-0.1.1.1/docs/System-Metrics-Distribution.html+[Label]: http://hackage.haskell.org/package/ekg-core-0.1.1.1/docs/System-Metrics-Label.html
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ monad-metrics.cabal view
@@ -0,0 +1,40 @@+name: monad-metrics+version: 0.1.0.0+synopsis: A convenient wrapper around EKG metrics+description: Please see README.md+homepage: https://github.com/parsonsmatt/monad-metrics#readme+license: MIT+license-file: LICENSE+author: Matthew Parsons+maintainer: matt@sellerlabs.com+copyright: 2017 Seller Labs, 2016 Taylor Fausak+category: Web+build-type: Simple+extra-source-files: README.md+cabal-version: >=1.10++library+ hs-source-dirs: src+ exposed-modules: Control.Monad.Metrics+ , Control.Monad.Metrics.Internal+ build-depends: base >= 4.7 && < 5+ , clock >= 0.3 && < 0.8+ , containers >= 0.2 && < 0.6+ , ekg-core >= 0.1.0.1 && < 0.2+ , text < 1.3+ , mtl >= 2 && < 2.3+ , transformers >= 0.3 && < 0.6+ default-language: Haskell2010++test-suite monad-metrics-test+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Spec.hs+ build-depends: base >= 4.6 && <= 5.0+ , monad-metrics+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ default-language: Haskell2010++source-repository head+ type: git+ location: https://github.com/parsonsmatt/monad-metrics
+ src/Control/Monad/Metrics.hs view
@@ -0,0 +1,210 @@+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE RecordWildCards #-}++{-|+Module : Control.Monad.Metrics+Description : An easy interface to recording metrics.+Copyright : (c) Matt Parsons, 2017+ Taylor Fausak, 2016+License : MIT+Maintainer : parsonsmatt@gmail.com+Stability : experimental+Portability : POSIX++This module presents an easy interface that you can use to collect metrics about your application.+It uses EKG under the hood and is inspired by Taylor Fausak's blunt application.++This module is designed to be imported qualified.+-}+module Control.Monad.Metrics+ ( -- * The Type Class+ MonadMetrics(..)+ -- * Initializing+ , Metrics+ , initialize+ , initializeWith+ -- * Collecting Metrics+ , increment+ , counter+ , counter'+ , gauge+ , gauge'+ , distribution+ , timed+ , timed'+ , label+ , label'+ , Resolution(..)+ ) where++import Control.Monad (liftM)+import Data.Monoid (mempty)+import Control.Monad.IO.Class+import Data.IORef+import Data.Map (Map)+import qualified Data.Map as Map+import Data.Text (Text)+import qualified Data.Text as Text+import System.Clock (Clock (..), TimeSpec (..),+ getTime)+import qualified System.Metrics as EKG+import System.Metrics.Counter as Counter+import System.Metrics.Distribution as Distribution+import System.Metrics.Gauge as Gauge+import System.Metrics.Label as Label++import Prelude++import Control.Monad.Metrics.Internal++-- | A class is an instance of 'MonadMetrics' if it can provide a 'Metrics'+-- somehow. Commonly, this will be implemented as a 'ReaderT' where some+-- field in the environment is the 'Metrics' data.+--+-- Since v0.1.0.0+class MonadMetrics m where+ getMetrics :: m Metrics++-- | Initializes a 'Metrics' value with the given 'System.Metrics.Store'.+--+-- Since 0.1.0.0+initializeWith :: EKG.Store -> IO Metrics+initializeWith metricsStore = do+ metricsCounters <- newIORef mempty+ metricsDistributions <- newIORef mempty+ metricsGauges <- newIORef mempty+ metricsLabels <- newIORef mempty+ return Metrics{..}++-- | Initializes a 'Metrics' value, creating a new 'System.Metrics.Store'+-- for it.+--+-- Since v0.1.0.0+initialize :: IO Metrics+initialize = EKG.newStore >>= initializeWith++-- | Increment the named counter by 1.+--+-- Since v0.1.0.0+increment :: (MonadIO m, MonadMetrics m) => Text -> m ()+increment name = counter name 1++-- | Adds the value to the named 'System.Metrics.Counter.Counter'.+--+-- Since v0.1.0.0+counter' :: (MonadIO m, MonadMetrics m, Integral int) => Text -> int -> m ()+counter' =+ modifyMetric Counter.add fromIntegral EKG.createCounter metricsCounters++-- | A type specialized version of 'counter'' to avoid ambiguous type+-- errors.+--+-- Since v0.1.0.0+counter :: (MonadIO m, MonadMetrics m) => Text -> Int -> m ()+counter = counter'++-- | Add the value to the named 'System.Metrics.Distribution.Distribution'.+--+-- Since v0.1.0.0+distribution :: (MonadIO m, MonadMetrics m) => Text -> Double -> m ()+distribution =+ modifyMetric Distribution.add id EKG.createDistribution metricsDistributions++-- | Set the value of the named 'System.Metrics.Distribution.Gauge'.+--+-- Since v0.1.0.0+gauge' :: (MonadIO m, MonadMetrics m, Integral int) => Text -> int -> m ()+gauge' =+ modifyMetric Gauge.set fromIntegral EKG.createGauge metricsGauges++-- | A type specialized version of 'gauge'' to avoid ambiguous types.+--+-- Since v0.1.0.0+gauge :: (MonadIO m, MonadMetrics m) => Text -> Int -> m ()+gauge = gauge'++-- | Record the time taken to perform the named action. The number is+-- stored in a 'System.Metrics.Disribution.Distribution' and is converted+-- to the specified 'Resolution'.+--+-- Since v0.1.0.0+timed' :: (MonadIO m, MonadMetrics m) => Resolution -> Text -> m a -> m a+timed' resolution name action = do+ start <- liftIO $ getTime Monotonic+ result <- action+ end <- liftIO $ getTime Monotonic+ distribution name (diffTime resolution start end)+ return result++-- | Record the time of executing the given action in seconds. Defers to+-- 'timed''.+--+-- Since v0.1.0.0+timed :: (MonadIO m, MonadMetrics m) => Text -> m a -> m a+timed = timed' Seconds++label :: (MonadIO m, MonadMetrics m) => Text -> Text -> m ()+label = modifyMetric Label.set id EKG.createLabel metricsLabels++label' :: (MonadIO m, MonadMetrics m, Show a) => Text -> a -> m ()+label' l = label l . Text.pack . show++-------------------------------------------------------------------------------++diffTime :: Resolution -> TimeSpec -> TimeSpec -> Double+diffTime res (TimeSpec seca nseca) (TimeSpec secb nsecb) =+ let sec = seca - secb+ nsec = nseca - nsecb+ in convertTimeSpecTo res (TimeSpec sec nsec)++convertTimeSpecTo :: Resolution -> TimeSpec -> Double+convertTimeSpecTo res (TimeSpec secs' nsecs') =+ case res of+ Nanoseconds -> nsecs + sToNs secs+ Microseconds -> nsToUs nsecs + sToUs secs+ Milliseconds -> nsToMs nsecs + sToMs secs+ Seconds -> nsToS nsecs + secs+ Minutes -> sToMin (nsToS nsecs + secs)+ Hours -> sToHour (nsToS nsecs + secs)+ Days -> sToDay (nsToS nsecs + secs)+ where+ nsecs = fromIntegral nsecs'+ secs = fromIntegral secs'++nsToUs, nsToMs, nsToS, sToMin, sToHour, sToDay, sToNs, sToUs, sToMs :: Double -> Double+nsToUs = (/ 10^3)+nsToMs = (/ 10^6)+nsToS = (/ 10^9)+sToMin = (/ 60)+sToHour = sToMin . sToMin+sToDay = (/ 24) . sToHour+sToNs = (* 10^9)+sToUs = (* 10^6)+sToMs = (* 10^3)++modifyMetric+ :: (MonadMetrics m, MonadIO m)+ => (t -> t1 -> IO b) -- ^ The action to add a value to a metric.+ -> (t2 -> t1) -- ^ A conversion function from input to metric value.+ -> (Text -> EKG.Store -> IO t) -- ^ The function for creating a new metric.+ -> (Metrics -> IORef (Map Text t)) -- ^ A way of getting the current metrics.+ -> Text -- ^ The name of the metric to use.+ -> t2 -- ^ The value the end user can provide.+ -> m b+modifyMetric adder converter creator getter name value = do+ bar <- lookupOrCreate getter creator name+ liftIO $ adder bar (converter value)++lookupOrCreate+ :: (MonadMetrics m, MonadIO m, Ord k)+ => (Metrics -> IORef (Map k a)) -> (k -> EKG.Store -> IO a) -> k -> m a+lookupOrCreate getter creator name = do+ ref <- liftM getter getMetrics+ container <- liftIO $ readIORef ref+ case Map.lookup name container of+ Nothing -> do+ c <- liftIO . creator name =<< liftM metricsStore getMetrics+ liftIO $ modifyIORef ref (Map.insert name c)+ return c+ Just c -> return c
+ src/Control/Monad/Metrics/Internal.hs view
@@ -0,0 +1,44 @@+{-|+Module : Control.Monad.Metrics.Internal+Description : An easy interface to recording metrics.+Copyright : (c) Matt Parsons, 2017+ Taylor Fausak, 2016+License : MIT+Maintainer : parsonsmatt@gmail.com+Stability : experimental+Portability : POSIX++This is an internal module for++-}+module Control.Monad.Metrics.Internal where++import Data.IORef+import Data.Map (Map)+import Data.Text (Text)+import System.Metrics (Store)+import System.Metrics.Counter (Counter)+import System.Metrics.Distribution (Distribution)+import System.Metrics.Gauge (Gauge)+import System.Metrics.Label (Label)++-- | A container for metrics used by the 'MonadMetrics' class.+data Metrics = Metrics+ { metricsCounters :: IORef (Map Text Counter)+ , metricsGauges :: IORef (Map Text Gauge)+ , metricsDistributions :: IORef (Map Text Distribution)+ , metricsLabels :: IORef (Map Text Label)+ , metricsStore :: Store+ }++-- | A type representing the resolution of time to use for the 'timed'+-- metric.+data Resolution+ = Nanoseconds+ | Microseconds+ | Milliseconds+ | Seconds+ | Minutes+ | Hours+ | Days+ deriving (Eq, Show, Ord, Enum)
+ test/Spec.hs view
@@ -0,0 +1,2 @@+main :: IO ()+main = putStrLn "Test suite not yet implemented"