packages feed

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 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`++[![Build Status](https://travis-ci.org/sellerlabs/monad-metrics.svg?branch=master)](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"