packages feed

instrument 0.6.0.0 → 0.6.1.0

raw patch · 18 files changed

+1215/−1047 lines, 18 filesdep +atomic-primopssetup-changedPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: atomic-primops

API changes (from Hackage documentation)

+ Instrument.Types: instance Data.SafeCopy.SafeCopy.Migrate Instrument.Types.AggPayload
+ Instrument.Types: instance Data.SafeCopy.SafeCopy.Migrate Instrument.Types.Aggregated_v2
+ Instrument.Types: instance Data.SafeCopy.SafeCopy.Migrate Instrument.Types.Payload
+ Instrument.Types: instance Data.SafeCopy.SafeCopy.SafeCopy Instrument.Types.AggPayload_v0
+ Instrument.Types: instance Data.SafeCopy.SafeCopy.SafeCopy Instrument.Types.Aggregated_v2
+ Instrument.Types: instance Data.SafeCopy.SafeCopy.SafeCopy Instrument.Types.Payload_v0
+ Instrument.Types: instance Data.Serialize.Serialize Instrument.Types.AggPayload_v0
+ Instrument.Types: instance Data.Serialize.Serialize Instrument.Types.Aggregated_v2
+ Instrument.Types: instance Data.Serialize.Serialize Instrument.Types.Payload_v0
+ Instrument.Types: instance Data.Serialize.Serialize Instrument.Types.SubmissionPacket_v1
+ Instrument.Types: instance GHC.Classes.Eq Instrument.Types.AggPayload_v0
+ Instrument.Types: instance GHC.Classes.Eq Instrument.Types.Aggregated_v2
+ Instrument.Types: instance GHC.Classes.Eq Instrument.Types.Payload_v0
+ Instrument.Types: instance GHC.Classes.Eq Instrument.Types.SubmissionPacket_v1
+ Instrument.Types: instance GHC.Generics.Generic Instrument.Types.AggPayload_v0
+ Instrument.Types: instance GHC.Generics.Generic Instrument.Types.Aggregated_v2
+ Instrument.Types: instance GHC.Generics.Generic Instrument.Types.Payload_v0
+ Instrument.Types: instance GHC.Generics.Generic Instrument.Types.SubmissionPacket_v1
+ Instrument.Types: instance GHC.Show.Show Instrument.Types.AggPayload_v0
+ Instrument.Types: instance GHC.Show.Show Instrument.Types.Aggregated_v2
+ Instrument.Types: instance GHC.Show.Show Instrument.Types.Payload_v0
+ Instrument.Types: instance GHC.Show.Show Instrument.Types.SubmissionPacket_v1
- Instrument.Counter: readCounter :: Counter -> IO Int
+ Instrument.Counter: readCounter :: Counter -> IO Integer
- Instrument.Counter: resetCounter :: Counter -> IO Int
+ Instrument.Counter: resetCounter :: Counter -> IO Integer
- Instrument.Types: AggCount :: Int -> AggPayload
+ Instrument.Types: AggCount :: Integer -> AggPayload
- Instrument.Types: Counter :: Int -> Payload
+ Instrument.Types: Counter :: Integer -> Payload
- Instrument.Types: [unCounter] :: Payload -> Int
+ Instrument.Types: [unCounter] :: Payload -> Integer

Files

Setup.hs view
@@ -1,2 +1,3 @@ import Distribution.Simple+ main = defaultMain
instrument.cabal view
@@ -1,5 +1,5 @@ name:                instrument-version:             0.6.0.0+version:             0.6.1.0 synopsis:            Easy stats/metrics instrumentation for Haskell programs license:             BSD3 license-file:        LICENSE@@ -60,6 +60,7 @@     , exceptions     , safe-exceptions     , safecopy+    , atomic-primops  test-suite test   default-language: Haskell2010@@ -76,6 +77,7 @@     Instrument.Tests.Utils     Instrument.Tests.Arbitrary     Instrument.Tests.Worker+    Instrument.Tests.Counter   build-depends:      base    , data-default
src/Instrument.hs view
@@ -1,17 +1,18 @@ module Instrument-    ( -- * Data Collection (Client) Side-      module Instrument.Client+  ( -- * Data Collection (Client) Side+    module Instrument.Client, -      -- * Data Processing (Backend) Side-    , module Instrument.Worker+    -- * Data Processing (Backend) Side+    module Instrument.Worker, -      -- *-    , module Instrument.Types-    ) where+    -- *+    module Instrument.Types,+  )+where  --------------------------------------------------------------------------------import           Instrument.Client-import           Instrument.Types-import           Instrument.Worker--------------------------------------------------------------------------------+import Instrument.Client+import Instrument.Types+import Instrument.Worker +-------------------------------------------------------------------------------
src/Instrument/Client.hs view
@@ -1,164 +1,171 @@-{-# LANGUAGE BangPatterns      #-}-{-# LANGUAGE CPP               #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-}+ module Instrument.Client-    ( Instrument-    , initInstrument-    , sampleI-    , timeI-    , timeI'-    , timeExI-    , TM.time-    , TM.timeEx-    , submitTime-    , incrementI-    , countI-    , timerMetricName-    , stripTimerPrefix-    , timerMetricNamePrefix-    , packetsKey-    ) where+  ( Instrument,+    initInstrument,+    sampleI,+    timeI,+    timeI',+    timeExI,+    TM.time,+    TM.timeEx,+    submitTime,+    incrementI,+    countI,+    timerMetricName,+    stripTimerPrefix,+    timerMetricNamePrefix,+    packetsKey,+  )+where  --------------------------------------------------------------------------------import           Control.Concurrent     (forkIO)-import           Control.Exception      (throw)-import           Control.Exception.Safe (MonadCatch, SomeException, tryAny)-import           Control.Monad-import           Control.Monad.IO.Class-import qualified Data.ByteString.Char8  as B-import           Data.IORef             (IORef, atomicModifyIORef, newIORef,-                                         readIORef)-import           Data.List              (isPrefixOf, stripPrefix)-import qualified Data.Map               as M-import           Data.Monoid            as Monoid-import qualified Data.SafeCopy          as SC-import qualified Data.Text              as T+import Control.Concurrent (forkIO)+import Control.Exception (throw)+import Control.Exception.Safe (MonadCatch, SomeException, tryAny)+import Control.Monad+import Control.Monad.IO.Class+import qualified Data.ByteString.Char8 as B+import Data.IORef+  ( IORef,+    atomicModifyIORef,+    newIORef,+    readIORef,+  )+import Data.List (isPrefixOf, stripPrefix)+import qualified Data.Map as M+import Data.Monoid as Monoid+import qualified Data.SafeCopy as SC+import qualified Data.Text as T #if MIN_VERSION_hedis(0,12,0) import           Database.Redis         as R #else import           Database.Redis         as R hiding (HostName, time) #endif-import           Network.HostName+ --------------------------------------------------------------------------------import qualified Instrument.Counter     as C+import qualified Instrument.Counter as C import qualified Instrument.Measurement as TM-import qualified Instrument.Sampler     as S-import           Instrument.Types-import           Instrument.Utils---------------------------------------------------------------------------------+import qualified Instrument.Sampler as S+import Instrument.Types+import Instrument.Utils+import Network.HostName +-------------------------------------------------------------------------------  -- | Initialize an instrument for measurement and feeding data into the system. -- -- The resulting opaque 'Instrument' is meant to be threaded around in -- your application to be later used in conjunction with 'sample' and -- 'time'.-initInstrument :: ConnectInfo-               -- ^ Redis connection info-               -> InstrumentConfig-               -- ^ Instrument configuration. Use "def" if you don't have specific needs-               -> IO Instrument+initInstrument ::+  -- | Redis connection info+  ConnectInfo ->+  -- | Instrument configuration. Use "def" if you don't have specific needs+  InstrumentConfig ->+  IO Instrument initInstrument conn cfg = do-    p <- createInstrumentPool conn-    h        <- getHostName-    smplrs <- newIORef M.empty-    ctrs <- newIORef M.empty-    void $ forkIO $ indefinitely' $ submitSamplers smplrs p cfg-    void $ forkIO $ indefinitely' $ submitCounters ctrs p cfg-    return $ I h smplrs ctrs p+  p <- createInstrumentPool conn+  h <- getHostName+  smplrs <- newIORef M.empty+  ctrs <- newIORef M.empty+  void $ forkIO $ indefinitely' $ submitSamplers smplrs p cfg+  void $ forkIO $ indefinitely' $ submitCounters ctrs p cfg+  return $ I h smplrs ctrs p   where     indefinitely' = indefinitely "Client" (seconds 1)  --------------------------------------------------------------------------------mkSampledSubmission :: MetricName-                    -> Dimensions-                    -> [Double]-                    -> IO SubmissionPacket+mkSampledSubmission ::+  MetricName ->+  Dimensions ->+  [Double] ->+  IO SubmissionPacket mkSampledSubmission nm dims vals = do   ts <- TM.getTime   return $ SP ts nm (Samples vals) dims - ------------------------------------------------------------------------------- addHostDimension :: HostName -> Dimensions -> Dimensions addHostDimension host = M.insert hostDimension (DimensionValue (T.pack host)) - --------------------------------------------------------------------------------mkCounterSubmission :: MetricName-                    -> Dimensions-                    -> Int-                    -> IO SubmissionPacket+mkCounterSubmission ::+  MetricName ->+  Dimensions ->+  Integer ->+  IO SubmissionPacket mkCounterSubmission m dims i = do-    ts <- TM.getTime-    return $ SP ts m (Counter i) dims-+  ts <- TM.getTime+  return $ SP ts m (Counter i) dims  -- | Flush all samplers in Instrument-submitSamplers-  :: IORef Samplers-  -> Connection-  -> InstrumentConfig-  -> IO ()+submitSamplers ::+  IORef Samplers ->+  Connection ->+  InstrumentConfig ->+  IO () submitSamplers smplrs rds cfg = do   ss <- getSamplers smplrs   mapM_ (flushSampler rds cfg) ss - -- | Flush all samplers in Instrument-submitCounters-  :: IORef Counters-  -> Connection-  -> InstrumentConfig-  -> IO ()+submitCounters ::+  IORef Counters ->+  Connection ->+  InstrumentConfig ->+  IO () submitCounters cs r cfg = do-    ss <- M.toList `liftM` readIORef cs-    mapM_ (flushCounter r cfg) ss-+  ss <- M.toList `liftM` readIORef cs+  mapM_ (flushCounter r cfg) ss  ------------------------------------------------------------------------------- submitPacket :: (SC.SafeCopy a) => R.Connection -> MetricName -> Maybe Integer -> a -> IO ()-submitPacket r m mbound sp = void $ R.runRedis r $ R.multiExec $ do-  -- Write key with the stat contents-  _ <- push-  -- Remember the key we wrote to so we can retrieve it later without key scanning-  rememberKey-  where rk = B.concat [B.pack "_sq_", B.pack (metricName m)]-        push = case mbound of-          Just n  -> lpushBoundedTxn rk [encodeCompress sp] n-          Nothing -> (() <$) <$> R.lpush rk [encodeCompress sp]-        rememberKey = sadd packetsKey [rk]-+submitPacket r m mbound sp = void $+  R.runRedis r $+    R.multiExec $ do+      -- Write key with the stat contents+      _ <- push+      -- Remember the key we wrote to so we can retrieve it later without key scanning+      rememberKey+  where+    rk = B.concat [B.pack "_sq_", B.pack (metricName m)]+    push = case mbound of+      Just n -> lpushBoundedTxn rk [encodeCompress sp] n+      Nothing -> (() <$) <$> R.lpush rk [encodeCompress sp]+    rememberKey = sadd packetsKey [rk]  -------------------------------------------------------------------------------+ -- | A key pointing to a SET of keys with _sq_ prefix, which contain -- data packets. These are processed by worker. packetsKey :: B.ByteString packetsKey = "_sqkeys" - -------------------------------------------------------------------------------+ -- | Flush given counter to remote service and reset in-memory counter -- back to 0.-flushCounter-  :: Connection-  -> InstrumentConfig-  -> ((MetricName, Dimensions), C.Counter)-  -> IO ()+flushCounter ::+  Connection ->+  InstrumentConfig ->+  ((MetricName, Dimensions), C.Counter) ->+  IO () flushCounter r cfg ((m, dims), c) =-    C.resetCounter c >>=-    mkCounterSubmission m dims >>=-    submitPacket r m (redisQueueBound cfg)-+  C.resetCounter c+    >>= mkCounterSubmission m dims+    >>= submitPacket r m (redisQueueBound cfg)  -------------------------------------------------------------------------------+ -- | Flush given sampler to remote service and flush in-memory queue-flushSampler-  :: Connection-  -> InstrumentConfig-  -> ((MetricName, Dimensions), S.Sampler)-  -> IO ()+flushSampler ::+  Connection ->+  InstrumentConfig ->+  ((MetricName, Dimensions), S.Sampler) ->+  IO () flushSampler r cfg ((name, dims), sampler) = do   vals <- S.get sampler   case vals of@@ -167,58 +174,57 @@       S.reset sampler       submitPacket r name (redisQueueBound cfg) =<< mkSampledSubmission name dims vals - -------------------------------------------------------------------------------+ -- | Increment a counter by one. Same as calling 'countI' with 1. -- -- >>> incrementI \"uploadedFiles\" instr-incrementI-  :: (MonadIO m)-  => MetricName-  -> HostDimensionPolicy-  -> Dimensions-  -> Instrument-  -> m ()+incrementI ::+  (MonadIO m) =>+  MetricName ->+  HostDimensionPolicy ->+  Dimensions ->+  Instrument ->+  m () incrementI m hostDimPolicy rawDims i =   liftIO $ C.increment =<< getCounter m dims i   where     dims = case hostDimPolicy of-      AddHostDimension      -> addHostDimension (hostName i) rawDims+      AddHostDimension -> addHostDimension (hostName i) rawDims       DoNotAddHostDimension -> rawDims - -------------------------------------------------------------------------------+ -- | Increment a counter by n. -- -- >>> countI \"uploadedFiles\" 1 instr-countI-  :: MonadIO m-  => MetricName-  -> HostDimensionPolicy-  -> Dimensions-  -> Int-  -> Instrument-  -> m ()+countI ::+  MonadIO m =>+  MetricName ->+  HostDimensionPolicy ->+  Dimensions ->+  Int ->+  Instrument ->+  m () countI m hostDimPolicy rawDims n i =   liftIO $ C.add n =<< getCounter m dims i   where     dims = case hostDimPolicy of-      AddHostDimension      -> addHostDimension (hostName i) rawDims+      AddHostDimension -> addHostDimension (hostName i) rawDims       DoNotAddHostDimension -> rawDims - -- | Run a monadic action while measuring its runtime. Push the -- measurement into the instrument system. -- -- >>> timeI \"fileUploadTime\" policy dims instr $ uploadFile file-timeI-  :: (MonadIO m)-  => MetricName-  -> HostDimensionPolicy-  -> Dimensions-  -> Instrument-  -> m a-  -> m a+timeI ::+  (MonadIO m) =>+  MetricName ->+  HostDimensionPolicy ->+  Dimensions ->+  Instrument ->+  m a ->+  m a timeI nm hostDimPolicy rawDims = do   timeI' (const (pure (Just (nm, hostDimPolicy, rawDims)))) @@ -228,12 +234,12 @@ -- -- * Emit different metrics or suppress metrics on error -- * Fetch some dimension info from the environment-timeI'-  :: (MonadIO m)-  => (a -> m (Maybe (MetricName, HostDimensionPolicy, Dimensions)))-  -> Instrument-  -> m a-  -> m a+timeI' ::+  (MonadIO m) =>+  (a -> m (Maybe (MetricName, HostDimensionPolicy, Dimensions))) ->+  Instrument ->+  m a ->+  m a timeI' toMetric i act = do   (!secs, !res) <- TM.time act   metricMay <- toMetric res@@ -248,12 +254,12 @@ -- failure -- -- >>> timeExI \"fileUploadTimeError\" \"fileUploadTime\" policy dims instr $ uploadFile file-timeExI-  :: (MonadIO m, MonadCatch m)-  => (Either SomeException a -> (MetricName, HostDimensionPolicy, Dimensions))-  -> Instrument-  -> m a-  -> m a+timeExI ::+  (MonadIO m, MonadCatch m) =>+  (Either SomeException a -> (MetricName, HostDimensionPolicy, Dimensions)) ->+  Instrument ->+  m a ->+  m a timeExI toMetric i act = do   resE <- timeI' (pure . Just . toMetric) i (tryAny act)   either throw pure resE@@ -262,23 +268,21 @@ timerMetricName :: MetricName -> MetricName timerMetricName name@(MetricName nameS) =   if timerMetricNamePrefix `isPrefixOf` nameS-     then name-     else MetricName (timerMetricNamePrefix Monoid.<> nameS)-+    then name+    else MetricName (timerMetricNamePrefix Monoid.<> nameS)  ------------------------------------------------------------------------------- stripTimerPrefix :: MetricName -> MetricName stripTimerPrefix (MetricName n) = case stripPrefix timerMetricNamePrefix n of   Just unprefixed -> MetricName unprefixed-  Nothing         -> MetricName n-+  Nothing -> MetricName n  ------------------------------------------------------------------------------- timerMetricNamePrefix :: String timerMetricNamePrefix = "time." - -------------------------------------------------------------------------------+ -- | Sometimes dimensions are determined within a code block that -- you're measuring. In that case, you can use 'time' to measure it -- and when you're ready to submit, use 'submitTime'.@@ -288,22 +292,22 @@ -- -- Note: for legacy purposes, metric name will have "time." prepended -- to it.-submitTime-  :: (MonadIO m)-  => MetricName-  -> HostDimensionPolicy-  -> Dimensions-  -> Double-  -- ^ Time in seconds-  -> Instrument-  -> m ()+submitTime ::+  (MonadIO m) =>+  MetricName ->+  HostDimensionPolicy ->+  Dimensions ->+  -- | Time in seconds+  Double ->+  Instrument ->+  m () submitTime nameRaw hostDimPolicy rawDims secs i =   liftIO $ sampleI nm hostDimPolicy rawDims secs i   where     nm = timerMetricName nameRaw - -------------------------------------------------------------------------------+ -- | Record given measurement under the given label. -- -- Instrument will automatically capture useful stats like min, max,@@ -313,49 +317,53 @@ -- something like: -- -- >>> sampleI \"uploadQueue\" 27 inst-sampleI-  :: MonadIO m-  => MetricName-  -> HostDimensionPolicy-  -> Dimensions-  -> Double-  -> Instrument-  -> m ()+sampleI ::+  MonadIO m =>+  MetricName ->+  HostDimensionPolicy ->+  Dimensions ->+  Double ->+  Instrument ->+  m () sampleI name hostDimPolicy rawDims v i =   liftIO $ S.sample v =<< getSampler name dims i   where     dims = case hostDimPolicy of-      AddHostDimension      -> addHostDimension (hostName i) rawDims+      AddHostDimension -> addHostDimension (hostName i) rawDims       DoNotAddHostDimension -> rawDims - ------------------------------------------------------------------------------- getCounter :: MetricName -> Dimensions -> Instrument -> IO C.Counter getCounter nm dims i = getRef C.newCounter (nm, dims) (counters i) - -- | Get or create a sampler under given name getSampler :: MetricName -> Dimensions -> Instrument -> IO S.Sampler getSampler name dims i = getRef (S.new 1000) (name, dims) (samplers i) - -- | Get a list of current samplers present getSamplers :: IORef Samplers -> IO [((MetricName, Dimensions), S.Sampler)] getSamplers ss = M.toList `fmap` readIORef ss - -- | Lookup a 'Ref' by name in the given map.  If no 'Ref' exists -- under the given name, create a new one, insert it into the map and -- return it.+-- Note mapRef is append only, so we can use double-checked locking+-- to avoid synchronization on reads. That makes hot-path lock free.+-- We'll only synchronize the first time metric is inserted. getRef :: Ord k => IO b -> k -> IORef (M.Map k b) -> IO b getRef f name mapRef = do-    empty <- f-    atomicModifyIORef mapRef $ \ m ->+  mapRef' <- readIORef mapRef+  case M.lookup name mapRef' of+    Just ref -> pure ref+    Nothing -> do+      empty <- f+      atomicModifyIORef mapRef $ \m ->         case M.lookup name m of-            Nothing  -> let m' = M.insert name empty m-                        in (m', empty)-            Just ref -> (m, ref)-{-# INLINABLE getRef #-}+          Nothing ->+            let m' = M.insert name empty m+             in (m', empty)+          Just ref -> (m, ref)+{-# INLINEABLE getRef #-}  -- | Bounded version of lpush which truncates *new* data first. This -- effectively stops accepting data until the queue shrinks below the@@ -363,4 +371,4 @@ lpushBoundedTxn :: B.ByteString -> [B.ByteString] -> Integer -> RedisTx (Queued ()) lpushBoundedTxn k vs mx = do   _ <- lpush k vs-  fmap (() <$) (ltrim k (-mx) (-1))+  fmap (() <$) (ltrim k (- mx) (-1))
src/Instrument/ClientClass.hs view
@@ -1,9 +1,12 @@-{-# LANGUAGE FlexibleContexts      #-}-{-# LANGUAGE FlexibleInstances     #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE UndecidableInstances  #-}+{-# LANGUAGE UndecidableInstances #-}  -----------------------------------------------------------------------------++----------------------------------------------------------------------------+ -- | -- Module      :  Instrument.ClientClass -- Copyright   :  Soostone Inc@@ -16,26 +19,25 @@ -- instead exposes a typeclass facilitated interface. Once you define -- the typeclass for your application's main monad, you can call all -- the mesaurement functions directly.------------------------------------------------------------------------------ module Instrument.ClientClass-    ( I.Instrument-    , I.initInstrument-    , HasInstrument (..)-    , sampleI-    , timeI-    , countI-    , incrementI-    ) where+  ( I.Instrument,+    I.initInstrument,+    HasInstrument (..),+    sampleI,+    timeI,+    countI,+    incrementI,+  )+where  --------------------------------------------------------------------------------import           Control.Monad.IO.Class-import           Control.Monad.Reader---------------------------------------------------------------------------------import qualified Instrument.Client      as I-import           Instrument.Types+import Control.Monad.IO.Class+import Control.Monad.Reader -------------------------------------------------------------------------------+import qualified Instrument.Client as I+import Instrument.Types +-------------------------------------------------------------------------------  class HasInstrument m where   getInstrument :: m I.Instrument@@ -43,52 +45,50 @@ instance (Monad m) => HasInstrument (ReaderT I.Instrument m) where   getInstrument = ask - -- | Run a monadic action while measuring its runtime-timeI :: (MonadIO m, HasInstrument m)-     => MetricName-     -> HostDimensionPolicy-     -> Dimensions-     -> m a-     -> m a+timeI ::+  (MonadIO m, HasInstrument m) =>+  MetricName ->+  HostDimensionPolicy ->+  Dimensions ->+  m a ->+  m a timeI name hostDimPolicy dims act = do   i <- getInstrument   I.timeI name hostDimPolicy dims i act - -- | Record a measurement sample-sampleI :: (MonadIO m, HasInstrument m )-       => MetricName-       -> HostDimensionPolicy-       -> Dimensions-       -> Double-       -> m ()+sampleI ::+  (MonadIO m, HasInstrument m) =>+  MetricName ->+  HostDimensionPolicy ->+  Dimensions ->+  Double ->+  m () sampleI name hostDimPolicy dims val =   I.sampleI name hostDimPolicy dims val =<< getInstrument - --------------------------------------------------------------------------------incrementI-  :: ( MonadIO m-     , HasInstrument m-     )-  => MetricName-  -> HostDimensionPolicy-  -> Dimensions-  -> m ()+incrementI ::+  ( MonadIO m,+    HasInstrument m+  ) =>+  MetricName ->+  HostDimensionPolicy ->+  Dimensions ->+  m () incrementI m hostDimPolicy dims =   I.incrementI m hostDimPolicy dims =<< getInstrument - --------------------------------------------------------------------------------countI-  :: ( MonadIO m-     , HasInstrument m-     )-  => MetricName-  -> HostDimensionPolicy-  -> Dimensions-  -> Int-  -> m ()+countI ::+  ( MonadIO m,+    HasInstrument m+  ) =>+  MetricName ->+  HostDimensionPolicy ->+  Dimensions ->+  Int ->+  m () countI m hostDimPolicy dims v =   I.countI m hostDimPolicy dims v =<< getInstrument
src/Instrument/Counter.hs view
@@ -1,44 +1,59 @@ {-# LANGUAGE BangPatterns #-}+{-# LANGUAGE TypeApplications #-}  module Instrument.Counter-    ( Counter-    , newCounter-    , readCounter-    , resetCounter-    , add-    , increment-    ) where+  ( Counter,+    newCounter,+    readCounter,+    resetCounter,+    add,+    increment,+  )+where  --------------------------------------------------------------------------------import           Control.Monad-import           Data.IORef+import Data.IORef+import qualified Data.Atomics.Counter as A ------------------------------------------------------------------------------- -newtype Counter = Counter { unCounter :: IORef Int }+data Counter = Counter {_atomicCounter :: A.AtomicCounter, _lastResetValue :: IORef Int}  ------------------------------------------------------------------------------- newCounter :: IO Counter-newCounter = Counter `liftM` newIORef 0-+newCounter = Counter <$> A.newCounter 0 <*> newIORef 0  --------------------------------------------------------------------------------readCounter :: Counter -> IO Int-readCounter (Counter i) = readIORef i +-- | Reads current counter value+readCounter :: Counter -> IO Integer+readCounter (Counter i lastReset) = calculateDelta <$> readIORef lastReset <*> A.readCounter i +-- | Our counters are represented as Int (with machine word size) and increment+-- | only, with no possibility to reset. Since we report deltas, we need facility+-- | to reset counter: we do that by storing last reported value in IORef.+-- | When application will run for long-enough counters will inevitably overflow.+-- | In such case our delta would be negative – this does not make sense for increment only.+-- | To overcome this issue we apply heuristic: when current value of counter+-- | is lower last value the counter has been reset, rollover happened and we need to+-- | take it into account. Otherwise, we can simply subtract values.+calculateDelta :: Int -> Int -> Integer+calculateDelta lastReset current | current < lastReset =+  fromIntegral (maxBound @Int - lastReset) + fromIntegral (current - minBound @Int) + 1+calculateDelta lastReset current = fromIntegral current - fromIntegral lastReset+ -------------------------------------------------------------------------------+ -- | Reset the counter while reading it-resetCounter :: Counter -> IO Int-resetCounter (Counter i) = atomicModifyIORef i f-    where f i' = (0, i')+resetCounter :: Counter -> IO Integer+resetCounter (Counter i lastReset) = do+  ctrValue <- A.readCounter i+  oldLast <- atomicModifyIORef' lastReset $ \oldLast -> (ctrValue, oldLast)+  pure $ calculateDelta oldLast ctrValue  ------------------------------------------------------------------------------- increment :: Counter -> IO () increment = add 1 - ------------------------------------------------------------------------------- add :: Int -> Counter -> IO ()-add x c = atomicModifyIORef (unCounter c) f-    where-      f !i = (i + x, ())+add x (Counter i _) = A.incrCounter_ x i
src/Instrument/Measurement.hs view
@@ -1,6 +1,6 @@-{-# LANGUAGE BangPatterns        #-}+{-# LANGUAGE BangPatterns #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeOperators       #-}+{-# LANGUAGE TypeOperators #-}  -- | -- Module      : Instrument.Measurement@@ -8,25 +8,25 @@ --               (c) 2012, Ozgun Ataman -- -- License     : BSD-style- module Instrument.Measurement-    (-      getTime-    , time-    , time_-    , timeEx-    ) where+  ( getTime,+    time,+    time_,+    timeEx,+  )+where  --------------------------------------------------------------------------------import           Control.Monad.IO.Class-import           Data.Time.Clock.POSIX  (getPOSIXTime)-import           Control.Exception (SomeException)-import           Control.Exception.Safe (MonadCatch, tryAny)-------------------------------------------------------------------------------- +import Control.Exception (SomeException)+import Control.Exception.Safe (MonadCatch, tryAny)+import Control.Monad.IO.Class+import Data.Time.Clock.POSIX (getPOSIXTime) +-------------------------------------------------------------------------------+ -- | Measure how long action took, in seconds along with its result-time :: MonadIO m =>  m a -> m (Double, a)+time :: MonadIO m => m a -> m (Double, a) time act = do   start <- liftIO getTime   !result <- act@@ -40,8 +40,7 @@  -- | Just measure how long action takes, discard its result time_ :: MonadIO m => m a -> m Double-time_  = fmap fst . time-+time_ = fmap fst . time  ------------------------------------------------------------------------------- getTime :: IO Double
src/Instrument/Sampler.hs view
@@ -1,4 +1,7 @@ -----------------------------------------------------------------------------++----------------------------------------------------------------------------+ -- | -- Module      :  Instrument.Sampler -- Copyright   :  Soostone Inc@@ -10,40 +13,39 @@ -- A container that can capture actual numeric values, efficiently and -- concurrently buffering them in memory. We can impose a cap on how -- many values are captured at a time.------------------------------------------------------------------------------ module Instrument.Sampler-    ( Sampler (..)-    , new-    , sample-    , get-    , reset-    ) where+  ( Sampler (..),+    new,+    sample,+    get,+    reset,+  )+where  --------------------------------------------------------------------------------import           Control.Concurrent.MVar-import           Control.Exception       (mask_, onException)-import           Control.Monad-import qualified Data.Vector.Mutable     as V--------------------------------------------------------------------------------+import Control.Concurrent.MVar+import Control.Exception (mask_, onException)+import Control.Monad+import qualified Data.Vector.Mutable as V +------------------------------------------------------------------------------- --- |'BoundedChan' is an abstract data type representing a bounded channel.-data Buffer a = B {-       _size     :: Int-     , _contents :: V.IOVector a-     , _writePos :: MVar Int-     }+-- | 'BoundedChan' is an abstract data type representing a bounded channel.+data Buffer a = B+  { _size :: Int,+    _contents :: V.IOVector a,+    _writePos :: MVar Int+  }  -- Versions of modifyMVar and withMVar that do not 'restore' the previous mask state when running -- 'io', with added modification strictness.  The lack of 'restore' may make these perform better -- than the normal version.  Moving strictness here makes using them more pleasant. {-# INLINE modifyMVar_mask #-}-modifyMVar_mask :: MVar a -> (a -> IO (a,b)) -> IO b+modifyMVar_mask :: MVar a -> (a -> IO (a, b)) -> IO b modifyMVar_mask m io =   mask_ $ do     a <- takeMVar m-    (a',b) <- io a `onException` putMVar m a+    (a', b) <- io a `onException` putMVar m a     putMVar m $! a'     return b @@ -55,15 +57,13 @@     a' <- io a `onException` putMVar m a     putMVar m $! a' - ------------------------------------------------------------------------------- newBuffer :: Int -> IO (Buffer a) newBuffer lim = do-  pos  <- newMVar 0+  pos <- newMVar 0   entries <- V.new lim   return (B lim entries pos) - -- | Write an element to the channel. If the channel is full, nothing -- will happen and the function will return immediately. We don't want -- to disturb production code.@@ -71,45 +71,35 @@ writeBuffer (B size contents wposMV) x = modifyMVar_mask_ wposMV $   \wpos ->     case wpos >= size of-      True -> return wpos       -- buffer full, don't do anything+      True -> return wpos -- buffer full, don't do anything       False -> do         V.write contents wpos x         return (succ wpos) - ------------------------------------------------------------------------------- getBuffer :: Buffer a -> IO [a] getBuffer (B _size contents pos) = do-  wpos <- modifyMVar_mask pos $ \ wpos -> return (wpos, wpos)-  forM [0.. (wpos - 1)] $ \ i -> (V.read contents i)--+  wpos <- modifyMVar_mask pos $ \wpos -> return (wpos, wpos)+  forM [0 .. (wpos - 1)] $ \i -> (V.read contents i)  ------------------------------------------------------------------------------- resetBuffer :: Buffer a -> IO () resetBuffer (B _size _els pos) = modifyMVar_mask_ pos (const (return 0)) --- -- | An in-memory, bounded buffer for measurement samples.-newtype Sampler = S { unS :: Buffer Double }-+newtype Sampler = S {unS :: Buffer Double}  -- | Create a new, empty sampler new :: Int -> IO Sampler new i = S `fmap` newBuffer i - ------------------------------------------------------------------------------- sample :: Double -> Sampler -> IO () sample v s = writeBuffer (unS s) v - ------------------------------------------------------------------------------- get :: Sampler -> IO [Double] get (S buffer) = getBuffer buffer-  ------------------------------------------------------------------------------- reset :: Sampler -> IO ()
src/Instrument/Types.hs view
@@ -1,152 +1,214 @@-{-# LANGUAGE CPP                        #-}-{-# LANGUAGE DeriveGeneric              #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings          #-}-{-# LANGUAGE RecordWildCards            #-}-{-# LANGUAGE TemplateHaskell            #-}-{-# LANGUAGE TypeFamilies               #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}  module Instrument.Types-  ( createInstrumentPool-  , Samplers-  , Counters-  , Instrument(..)-  , InstrumentConfig(..)-  , SubmissionPacket(..)-  , MetricName(..)-  , DimensionName(..)-  , DimensionValue(..)-  , Dimensions-  , Payload(..)-  , Aggregated(..)-  , AggPayload(..)-  , Stats(..)-  , hostDimension-  , HostDimensionPolicy(..)-  , Quantile(..)-  ) where+  ( createInstrumentPool,+    Samplers,+    Counters,+    Instrument (..),+    InstrumentConfig (..),+    SubmissionPacket (..),+    MetricName (..),+    DimensionName (..),+    DimensionValue (..),+    Dimensions,+    Payload (..),+    Aggregated (..),+    AggPayload (..),+    Stats (..),+    hostDimension,+    HostDimensionPolicy (..),+    Quantile (..),+  )+where  --------------------------------------------------------------------------------import           Control.Applicative   as A+import Control.Applicative as A import qualified Data.ByteString.Char8 as B-import           Data.Default-import           Data.IORef-import qualified Data.Map              as M-import           Data.Monoid           as Monoid-import qualified Data.SafeCopy         as SC-import           Data.Serialize        as Ser-import           Data.Serialize.Text   ()-import           Data.String-import           Data.Text             (Text)-import qualified Data.Text             as T-#if MIN_VERSION_hedis(0,12,0)+import Data.Default+import Data.IORef+import qualified Data.Map as M+import Data.Monoid as Monoid+import qualified Data.SafeCopy as SC+import Data.Serialize as Ser+import Data.Serialize.Text ()+import Data.String+import Data.Text (Text)+import qualified Data.Text as T import           Database.Redis        as R-#else-import           Database.Redis        as R hiding (HostName, time)-#endif-import           GHC.Generics-import           Network.HostName---------------------------------------------------------------------------------import qualified Instrument.Counter    as C-import qualified Instrument.Sampler    as S+import GHC.Generics -------------------------------------------------------------------------------+import qualified Instrument.Counter as C+import qualified Instrument.Sampler as S+import Network.HostName +-------------------------------------------------------------------------------  ------------------------------------------------------------------------------- createInstrumentPool :: ConnectInfo -> IO Connection-createInstrumentPool ci = do-  c <- connect ci {-         connectMaxIdleTime = 15-       , connectMaxConnections = 1 }-  return c+createInstrumentPool ci =+    connect+      ci+        { connectMaxIdleTime = 15,+          connectMaxConnections = 1+        } +------------------------------------------------------------------------------- +newtype DimensionName = DimensionName+  { dimensionName :: Text+  }+  deriving (Eq, Ord, Show, Generic, Serialize, IsString)++$(SC.deriveSafeCopy 0 'SC.base ''DimensionName)++-- | Convention for the dimension of the hostname. Used in the client+-- to inject hostname into the parameters map+hostDimension :: DimensionName+hostDimension = "host"++newtype DimensionValue = DimensionValue+  { dimensionValue :: Text+  }+  deriving (Eq, Ord, Show, Generic, Serialize, IsString)++$(SC.deriveSafeCopy 0 'SC.base ''DimensionValue)++newtype MetricName = MetricName+  { metricName :: String+  }+  deriving (Eq, Show, Generic, Ord, IsString, Serialize)++$(SC.deriveSafeCopy 0 'SC.base ''MetricName)++-------------------------------------------------------------------------------+ -- Map of user-defined samplers. type Samplers = M.Map (MetricName, Dimensions) S.Sampler  -- Map of user-defined counters. type Counters = M.Map (MetricName, Dimensions) C.Counter - type Dimensions = M.Map DimensionName DimensionValue +data Payload_v0+  = Samples_v0 {unSamples_v0 :: [Double]}+  | Counter_v0 {unCounter_v0 :: Int}+  deriving (Eq, Show, Generic) -newtype MetricName = MetricName {-      metricName :: String-    } deriving (Eq,Show,Generic,Ord,IsString,Serialize)+instance Serialize Payload_v0 +data Payload+  = Samples {unSamples :: [Double]}+  | Counter {unCounter :: Integer}+  deriving (Eq, Show, Generic) -data Instrument = I {-      hostName :: HostName-    , samplers :: !(IORef Samplers)-    , counters :: !(IORef Counters)-    , redis    :: Connection-    }+instance Serialize Payload -data InstrumentConfig = ICfg {-      redisQueueBound :: Maybe Integer-    }+$(SC.deriveSafeCopy 0 'SC.base ''Payload_v0)+$(SC.deriveSafeCopy 1 'SC.extension ''Payload) +instance SC.Migrate Payload where+  type MigrateFrom Payload = Payload_v0+  migrate (Samples_v0 n) = Samples n+  migrate (Counter_v0 n) = Counter $ fromIntegral n++data Instrument = I+  { hostName :: HostName,+    samplers :: !(IORef Samplers),+    counters :: !(IORef Counters),+    redis :: Connection+  }++data InstrumentConfig = ICfg+  { redisQueueBound :: Maybe Integer+  }+ instance Default InstrumentConfig where   def = ICfg Nothing - -- | Submitted package of collected samples-data SubmissionPacket_v0 = SP_v0 {-      spTimeStamp_v0 :: !Double-    -- ^ Timing of this submission-    , spHostName_v0  :: !HostName-    -- ^ Who sent it-    , spName_v0      :: String-    -- ^ Metric name-    , spPayload_v0   :: Payload-    -- ^ Collected values-    } deriving (Eq,Show,Generic)-+data SubmissionPacket_v0 = SP_v0+  { -- | Timing of this submission+    spTimeStamp_v0 :: !Double,+    -- | Who sent it+    spHostName_v0 :: !HostName,+    -- | Metric name+    spName_v0 :: String,+    -- | Collected values+    spPayload_v0 :: Payload+  }+  deriving (Eq, Show, Generic)  instance Serialize SubmissionPacket_v0 --data SubmissionPacket = SP {-      spTimeStamp  :: !Double-    -- ^ Timing of this submission-    , spName       :: !MetricName-    -- ^ Metric name-    , spPayload    :: !Payload-    -- ^ Collected values-    , spDimensions :: !Dimensions-    -- ^ Defines slices that this packet belongs to. This allows+data SubmissionPacket_v1 = SP_v1+  { -- | Timing of this submission+    spTimeStamp_v1 :: !Double,+    -- | Metric name+    spName_v1 :: !MetricName,+    -- | Collected values+    spPayload_v1 :: !Payload_v0,+    -- | Defines slices that this packet belongs to. This allows     -- drill-down on the backends. For instance, you could do     -- "server_name" "app1" or "queue_name" "my_queue"-    } deriving (Eq,Show,Generic)+    spDimensions_v1 :: !Dimensions+  }+  deriving (Eq, Show, Generic) +instance Serialize SubmissionPacket_v1 +data SubmissionPacket = SP+  { -- | Timing of this submission+    spTimeStamp :: !Double,+    -- | Metric name+    spName :: !MetricName,+    -- | Collected values+    spPayload :: !Payload,+    -- | Defines slices that this packet belongs to. This allows+    -- drill-down on the backends. For instance, you could do+    -- "server_name" "app1" or "queue_name" "my_queue"+    spDimensions :: !Dimensions+  }+  deriving (Eq, Show, Generic)+ instance Serialize SubmissionPacket where-  get = (to <$> gGet) <|> (upgradeSP0 <$> Ser.get)+  get = (to <$> gGet)  <|> (upgradeSP1 <$> Ser.get) <|> (upgradeSP0 <$> Ser.get) +upgradeSP0 :: SubmissionPacket_v0 -> SubmissionPacket+upgradeSP0 SP_v0 {..} =+  SP+    { spTimeStamp = spTimeStamp_v0,+      spName = MetricName spName_v0,+      spPayload = spPayload_v0,+      spDimensions = M.singleton hostDimension (DimensionValue (T.pack spHostName_v0))+    } +upgradeSP1 :: SubmissionPacket_v1 -> SubmissionPacket+upgradeSP1 SP_v1 {..} =+  SP+    { spTimeStamp = spTimeStamp_v1,+      spName = spName_v1,+      spPayload = case spPayload_v1 of+        Samples_v0 n -> Samples n+        Counter_v0 n -> Counter $ fromIntegral n,+      spDimensions = spDimensions_v1+    }++$(SC.deriveSafeCopy 0 'SC.base ''SubmissionPacket_v0)+$(SC.deriveSafeCopy 1 'SC.extension ''SubmissionPacket)+ instance SC.Migrate SubmissionPacket where   type MigrateFrom SubmissionPacket = SubmissionPacket_v0   migrate = upgradeSP0 --upgradeSP0 :: SubmissionPacket_v0 -> SubmissionPacket-upgradeSP0 SP_v0 {..} = SP-  { spTimeStamp = spTimeStamp_v0-  , spName = MetricName spName_v0-  , spPayload = spPayload_v0-  , spDimensions = M.singleton hostDimension (DimensionValue (T.pack spHostName_v0))-  }-- ---------------------------------------------------------------------------------- | Convention for the dimension of the hostname. Used in the client--- to inject hostname into the parameters map-hostDimension :: DimensionName-hostDimension = "host" --------------------------------------------------------------------------------- -- | Should we automatically pull the host and add it as a -- dimension. Used at the call site of the various metrics ('timeI', -- 'sampleI', etc). Hosts are basically grandfathered in as a@@ -154,164 +216,172 @@ -- useful, but it is not relevant to some metrics and actually makes -- some metrics difficult to use depending on the backend, so we made -- them opt-in.-data HostDimensionPolicy = AddHostDimension-                         | DoNotAddHostDimension-                         deriving (Show, Eq)-+data HostDimensionPolicy+  = AddHostDimension+  | DoNotAddHostDimension+  deriving (Show, Eq)  --------------------------------------------------------------------------------newtype DimensionName = DimensionName {-    dimensionName :: Text-  } deriving (Eq,Ord,Show,Generic,Serialize,IsString)+data Stats = Stats+  { smean :: Double,+    ssum :: Double,+    scount :: Int,+    smax :: Double,+    smin :: Double,+    srange :: Double,+    sstdev :: Double,+    sskewness :: Double,+    skurtosis :: Double,+    squantiles :: M.Map Int Double+  }+  deriving (Eq, Show, Generic) +instance Default Stats where+  def = Stats 0 0 0 0 0 0 0 0 0 mempty ---------------------------------------------------------------------------------newtype DimensionValue = DimensionValue {-    dimensionValue :: Text-  } deriving (Eq,Ord,Show,Generic,Serialize,IsString)+instance Serialize Stats +$(SC.deriveSafeCopy 0 'SC.base ''Stats)  --------------------------------------------------------------------------------data Payload-    = Samples { unSamples :: [Double] }-    | Counter { unCounter :: Int }+-- | Resulting payload for metrics aggregation+data AggPayload_v0+  = AggStats_v0 Stats+  | AggCount_v0 Int   deriving (Eq, Show, Generic) -instance Serialize Payload-+instance Serialize AggPayload_v0 ---------------------------------------------------------------------------------data Aggregated_v0 = Aggregated_v0 {-      aggTS_v0      :: Double-      -- ^ Timestamp for this aggregation-    , aggName_v0    :: String-    -- ^ Name of the metric-    , aggGroup_v0   :: B.ByteString-    -- ^ The aggregation level/group for this stat-    , aggPayload_v0 :: AggPayload-    -- ^ Calculated stats for the metric-    } deriving (Eq,Show,Generic)+data AggPayload+  = AggStats Stats+  | AggCount Integer+  deriving (Eq, Show, Generic) +instance Serialize AggPayload -instance Serialize Aggregated_v0+instance Default AggPayload where+  def = AggStats def +$(SC.deriveSafeCopy 0 'SC.base ''AggPayload_v0)+$(SC.deriveSafeCopy 1 'SC.extension ''AggPayload) -data Aggregated_v1 = Aggregated_v1 {-      aggTS_v1      :: Double-      -- ^ Timestamp for this aggregation-    , aggName_v1    :: MetricName-    -- ^ Name of the metric-    , aggGroup_v1   :: B.ByteString-    -- ^ The aggregation level/group for this stat-    , aggPayload_v1 :: AggPayload-    -- ^ Calculated stats for the metric-    } deriving (Eq,Show,Generic)+instance SC.Migrate AggPayload where+  type MigrateFrom AggPayload = AggPayload_v0+  migrate (AggStats_v0 n) = AggStats n+  migrate (AggCount_v0 n) = AggCount $ fromIntegral n +-------------------------------------------------------------------------------+data Aggregated = Aggregated+  { -- | Timestamp for this aggregation+    aggTS :: Double,+    -- | Name of the metric+    aggName :: MetricName,+    -- | Calculated stats for the metric+    aggPayload :: AggPayload,+    aggDimensions :: Dimensions+  }+  deriving (Eq, Show, Generic) -instance SC.Migrate Aggregated_v1 where-  type MigrateFrom Aggregated_v1 = Aggregated_v0-  migrate = upgradeAggregated_v0+instance Serialize Aggregated where+  get = (to <$> gGet) <|> (upgradeAggregated_v2 <$> Ser.get) <|> (upgradeAggregated_v2 . upgradeAggregated_v1 <$> Ser.get) +instance Default Aggregated where+  def = Aggregated 0 "" def mempty -upgradeAggregated_v0 :: Aggregated_v0 -> Aggregated_v1-upgradeAggregated_v0 a = Aggregated_v1-  { aggTS_v1 = aggTS_v0 a-  , aggName_v1 = MetricName (aggName_v0 a)-  , aggGroup_v1 = (aggGroup_v0 a)-  , aggPayload_v1 = (aggPayload_v0 a)+data Aggregated_v2 = Aggregated_v2+  { -- | Timestamp for this aggregation+    aggTS_v2 :: Double,+    -- | Name of the metric+    aggName_v2 :: MetricName,+    -- | Calculated stats for the metric+    aggPayload_v2 :: AggPayload_v0,+    aggDimensions_v2 :: Dimensions   }--instance Serialize Aggregated_v1 where-  get = (to <$> gGet) <|> (upgradeAggregated_v0 <$> Ser.get)-+  deriving (Eq, Show, Generic) -data Aggregated = Aggregated {-      aggTS         :: Double-      -- ^ Timestamp for this aggregation-    , aggName       :: MetricName-    -- ^ Name of the metric-    , aggPayload    :: AggPayload-    -- ^ Calculated stats for the metric-    , aggDimensions :: Dimensions-    } deriving (Eq,Show, Generic)+instance Serialize Aggregated_v2 +upgradeAggregated_v2 :: Aggregated_v2 -> Aggregated+upgradeAggregated_v2 a =+  Aggregated+    { aggTS = aggTS_v2 a,+      aggName = aggName_v2 a,+      aggPayload = SC.migrate (aggPayload_v2 a),+      aggDimensions = aggDimensions_v2 a+    } -upgradeAggregated_v1 :: Aggregated_v1 -> Aggregated-upgradeAggregated_v1 a = Aggregated-  { aggTS = aggTS_v1 a-  , aggName = aggName_v1 a-  , aggPayload = aggPayload_v1 a-  , aggDimensions = Monoid.mempty+data Aggregated_v1 = Aggregated_v1+  { -- | Timestamp for this aggregation+    aggTS_v1 :: Double,+    -- | Name of the metric+    aggName_v1 :: MetricName,+    -- | The aggregation level/group for this stat+    aggGroup_v1 :: B.ByteString,+    -- | Calculated stats for the metric+    aggPayload_v1 :: AggPayload_v0   }-+  deriving (Eq, Show, Generic) -instance SC.Migrate Aggregated where-  type MigrateFrom Aggregated = Aggregated_v1-  migrate = upgradeAggregated_v1+upgradeAggregated_v1 :: Aggregated_v1 -> Aggregated_v2+upgradeAggregated_v1 a =+  Aggregated_v2+    { aggTS_v2 = aggTS_v1 a,+      aggName_v2 = aggName_v1 a,+      aggPayload_v2 = aggPayload_v1 a,+      aggDimensions_v2 = Monoid.mempty+    } +data Aggregated_v0 = Aggregated_v0+  { -- | Timestamp for this aggregation+    aggTS_v0 :: Double,+    -- | Name of the metric+    aggName_v0 :: String,+    -- | The aggregation level/group for this stat+    aggGroup_v0 :: B.ByteString,+    -- | Calculated stats for the metric+    aggPayload_v0 :: AggPayload_v0+  }+  deriving (Eq, Show, Generic) -instance Serialize Aggregated where-  get = (to <$> gGet) <|> (upgradeAggregated_v1 <$> Ser.get)+instance Serialize Aggregated_v0 +upgradeAggregated_v0 :: Aggregated_v0 -> Aggregated_v1+upgradeAggregated_v0 a =+  Aggregated_v1+    { aggTS_v1 = aggTS_v0 a,+      aggName_v1 = MetricName (aggName_v0 a),+      aggGroup_v1 = aggGroup_v0 a,+      aggPayload_v1 = aggPayload_v0 a+    } --- | Resulting payload for metrics aggregation-data AggPayload-    = AggStats Stats-    | AggCount Int-    deriving (Eq,Show, Generic)+instance Serialize Aggregated_v1 where+  get = (to <$> gGet) <|> (upgradeAggregated_v0 <$> Ser.get) -instance Serialize AggPayload+$(SC.deriveSafeCopy 0 'SC.base ''Aggregated_v0) +instance SC.Migrate Aggregated_v1 where+  type MigrateFrom Aggregated_v1 = Aggregated_v0+  migrate = upgradeAggregated_v0 +$(SC.deriveSafeCopy 1 'SC.extension ''Aggregated_v1) +instance SC.Migrate Aggregated_v2 where+  type MigrateFrom Aggregated_v2 = Aggregated_v1+  migrate = upgradeAggregated_v1 -instance Default AggPayload where-    def = AggStats def+$(SC.deriveSafeCopy 2 'SC.extension ''Aggregated_v2) -instance Default Aggregated where-    def = Aggregated 0 "" def mempty+instance SC.Migrate Aggregated where+  type MigrateFrom Aggregated = Aggregated_v2+  migrate = upgradeAggregated_v2 +$(SC.deriveSafeCopy 3 'SC.extension ''Aggregated)  --------------------------------------------------------------------------------data Stats = Stats {-      smean      :: Double-    , ssum       :: Double-    , scount     :: Int-    , smax       :: Double-    , smin       :: Double-    , srange     :: Double-    , sstdev     :: Double-    , sskewness  :: Double-    , skurtosis  :: Double-    , squantiles :: M.Map Int Double-    } deriving (Eq, Show, Generic) ---instance Default Stats where-    def = Stats 0 0 0 0 0 0 0 0 0 mempty--instance Serialize Stats---------------------------------------------------------------------------------- -- | Integer quantile, valid values range from 1-99, inclusive.-newtype Quantile = Q { quantile :: Int } deriving (Show, Eq, Ord)+newtype Quantile = Q {quantile :: Int} deriving (Show, Eq, Ord)  instance Bounded Quantile where   minBound = Q 1   maxBound = Q 99-----------------------------------------------------------------------------------$(SC.deriveSafeCopy 0 'SC.base ''Payload)-$(SC.deriveSafeCopy 0 'SC.base ''SubmissionPacket_v0)-$(SC.deriveSafeCopy 1 'SC.extension ''SubmissionPacket)-$(SC.deriveSafeCopy 0 'SC.base ''AggPayload)-$(SC.deriveSafeCopy 0 'SC.base ''Aggregated_v0)-$(SC.deriveSafeCopy 1 'SC.extension ''Aggregated_v1)-$(SC.deriveSafeCopy 2 'SC.extension ''Aggregated)-$(SC.deriveSafeCopy 0 'SC.base ''Stats)-$(SC.deriveSafeCopy 0 'SC.base ''DimensionName)-$(SC.deriveSafeCopy 0 'SC.base ''DimensionValue)-$(SC.deriveSafeCopy 0 'SC.base ''MetricName)
src/Instrument/Utils.hs view
@@ -1,60 +1,58 @@+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE NoMonomorphismRestriction #-}-{-# LANGUAGE OverloadedStrings         #-}  module Instrument.Utils-    ( formatDecimal-    , formatInt-    , showT-    , showBS-    , collect-    , noDots-    , encodeCompress-    , decodeCompress-    , indefinitely-    , seconds-    , milliseconds-    , for-    ) where-+  ( formatDecimal,+    formatInt,+    showT,+    showBS,+    collect,+    noDots,+    encodeCompress,+    decodeCompress,+    indefinitely,+    seconds,+    milliseconds,+    for,+  )+where  --------------------------------------------------------------------------------import           Codec.Compression.GZip-import           Control.Applicative    ((<|>))-import           Control.Concurrent     (threadDelay)-import           Control.Exception      (SomeException)-import           Control.Monad-import           Control.Monad.Catch    (Handler (..))-import           Control.Retry-import qualified Data.ByteString.Char8  as B-import           Data.ByteString.Lazy   (fromStrict, toStrict)-import qualified Data.Map               as M-import qualified Data.Map.Strict        as MS-import qualified Data.SafeCopy          as SC-import           Data.Serialize-import           Data.Text              (Text)-import qualified Data.Text              as T-import           Numeric-import           System.IO---------------------------------------------------------------------------------+import Codec.Compression.GZip+import Control.Applicative ((<|>))+import Control.Concurrent (threadDelay)+import Control.Exception (SomeException)+import Control.Monad+import Control.Monad.Catch (Handler (..))+import Control.Retry+import qualified Data.ByteString.Char8 as B+import Data.ByteString.Lazy (fromStrict, toStrict)+import qualified Data.Map as M+import qualified Data.Map.Strict as MS+import qualified Data.SafeCopy as SC+import Data.Serialize+import Data.Text (Text)+import qualified Data.Text as T+import Numeric+import System.IO +-------------------------------------------------------------------------------  --------------------------------------------------------------------------------collect :: (Ord b)-        => [a]-        -> (a -> b)-        -> (a -> c)-        -> M.Map b [c]+collect ::+  (Ord b) =>+  [a] ->+  (a -> b) ->+  (a -> c) ->+  M.Map b [c] collect as mkKey mkVal = foldr step M.empty as-    where-      step x acc = MS.insertWith (++) (mkKey x) ([mkVal x]) acc-+  where+    step x acc = MS.insertWith (++) (mkKey x) ([mkVal x]) acc  ------------------------------------------------------------------------------- noDots :: Text -> Text noDots = T.intercalate "_" . T.splitOn "." - ------------------------------------------------------------------------------- showT :: Show a => a -> Text showT = T.pack . show@@ -66,38 +64,36 @@ formatInt :: RealFrac a => a -> Text formatInt i = showT ((floor i) :: Int) - --------------------------------------------------------------------------------formatDecimal-    :: RealFloat a-    => Int-    -- ^ Digits after the point-    -> Bool-    -- ^ Add thousands sep?-    -> a-    -- ^ Number-    -> Text+formatDecimal ::+  RealFloat a =>+  -- | Digits after the point+  Int ->+  -- | Add thousands sep?+  Bool ->+  -- | Number+  a ->+  Text formatDecimal n th i =-    let res = T.pack . showFFloat (Just n) i $ ""-    in if th then addThousands res else res--+  let res = T.pack . showFFloat (Just n) i $ ""+   in if th then addThousands res else res  ------------------------------------------------------------------------------- addThousands :: Text -> Text addThousands t = T.concat [n', dec]-    where-      (n,dec) = T.span (/= '.') t-      n' = T.reverse . T.intercalate "," . T.chunksOf 3 . T.reverse $ n-+  where+    (n, dec) = T.span (/= '.') t+    n' = T.reverse . T.intercalate "," . T.chunksOf 3 . T.reverse $ n  -------------------------------------------------------------------------------+ -- | Serialize and compress with GZip in that order. This is the only -- function we use for serializing to Redis. encodeCompress :: SC.SafeCopy a => a -> B.ByteString encodeCompress = toStrict . compress . runPutLazy . SC.safePut  -------------------------------------------------------------------------------+ -- | Decompress from GZip and deserialize in that order. Tries to -- decode SafeCopy first and falls back to Serialize if that fails to -- account for old data. Note that encodeCompress only serializes to@@ -107,8 +103,8 @@   where     decodeWithFallback lbs = runGetLazy SC.safeGet lbs <|> decodeLazy lbs - -------------------------------------------------------------------------------+ -- | Run an IO repeatedly with the given delay in microseconds. If -- there are exceptions in the inner loop, they are logged to stderr, -- prefixed with the given string context and retried at an exponential@@ -118,7 +114,6 @@   where     delayed = (>> threadDelay n) - ------------------------------------------------------------------------------- logAndBackoff :: String -> IO () -> IO () logAndBackoff ctx = recovering policy [h] . const@@ -130,18 +125,17 @@       where         msg = "Caught exception in " ++ ctx ++ ": " ++ show e ++ ". Retrying..." - -------------------------------------------------------------------------------+ -- | Convert seconds to microseconds seconds :: Int -> Int seconds = (* milliseconds 1000) - -------------------------------------------------------------------------------+ -- | Convert milliseconds to microseconds milliseconds :: Int -> Int milliseconds = (* 1000)-  ------------------------------------------------------------------------------- for :: (Functor f) => f a -> (a -> b) -> f b
src/Instrument/Worker.hs view
@@ -1,81 +1,88 @@-{-# LANGUAGE BangPatterns        #-}-{-# LANGUAGE FlexibleContexts    #-}-{-# LANGUAGE OverloadedStrings   #-}-{-# LANGUAGE RecordWildCards     #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-}+ module Instrument.Worker-    ( initWorkerCSV-    , initWorkerCSV'-    , initWorkerGraphite-    , initWorkerGraphite'-    , work-    , initWorker-    , AggProcess(..)+  ( initWorkerCSV,+    initWorkerCSV',+    initWorkerGraphite,+    initWorkerGraphite',+    work,+    initWorker,+    AggProcess (..),+     -- * Configuring agg processes-    , AggProcessConfig(..)-    , standardQuantiles-    , noQuantiles-    , quantileMap-    , defAggProcessConfig+    AggProcessConfig (..),+    standardQuantiles,+    noQuantiles,+    quantileMap,+    defAggProcessConfig,+     -- * Exported for testing-    , expandDims-    ) where+    expandDims,+  )+where  --------------------------------------------------------------------------------import           Control.Error-import           Control.Monad-import           Control.Monad.IO.Class-import qualified Data.ByteString.Char8  as B-import           Data.Conduit           (runConduit, (.|))-import qualified Data.Conduit.List      as CL-import           Data.CSV.Conduit-import           Data.Default-import qualified Data.Map               as M-import qualified Data.SafeCopy          as SC-import           Data.Semigroup         as Semigroup-import           Data.Serialize-import qualified Data.Set               as Set-import qualified Data.Text              as T-import qualified Data.Text.IO           as T-import qualified Data.Vector.Unboxed    as V-import           Database.Redis         as R hiding (decode)-import           Network.Socket         as N-import qualified Statistics.Quantile    as Q-import           Statistics.Sample-import           System.IO-import           System.Posix+import Control.Error+import Control.Monad+import Control.Monad.IO.Class+import qualified Data.ByteString.Char8 as B+import Data.CSV.Conduit+import Data.Conduit (runConduit, (.|))+import qualified Data.Conduit.List as CL+import Data.Default+import qualified Data.Map as M+import qualified Data.SafeCopy as SC+import Data.Semigroup as Semigroup+import Data.Serialize+import qualified Data.Set as Set+import qualified Data.Text as T+import qualified Data.Text.IO as T+import qualified Data.Vector.Unboxed as V+import Database.Redis as R hiding (decode) --------------------------------------------------------------------------------import           Instrument.Client      (packetsKey, stripTimerPrefix,-                                         timerMetricName)+import Instrument.Client+  ( packetsKey,+    stripTimerPrefix,+    timerMetricName,+  ) import qualified Instrument.Measurement as TM-import           Instrument.Types-import           Instrument.Utils---------------------------------------------------------------------------------+import Instrument.Types+import Instrument.Utils+import Network.Socket as N+import qualified Statistics.Quantile as Q+import Statistics.Sample+import System.IO+import System.Posix +-------------------------------------------------------------------------------  -------------------------------------------------------------------------------+ -- | A CSV backend to store aggregation results in a CSV-initWorkerCSV-  :: ConnectInfo-  -> FilePath-  -- ^ Target file name-  -> Int-  -- ^ Aggregation period / flush interval in seconds-  -> AggProcessConfig-  -> IO ()+initWorkerCSV ::+  ConnectInfo ->+  -- | Target file name+  FilePath ->+  -- | Aggregation period / flush interval in seconds+  Int ->+  AggProcessConfig ->+  IO () initWorkerCSV conn fp n cfg =   initWorker "CSV Worker" conn n =<< initWorkerCSV' fp cfg - -------------------------------------------------------------------------------+ -- | Create an AggProcess that dumps to CSV. Use this to compose with -- other AggProcesses-initWorkerCSV'-  :: FilePath-  -- ^ Target file name-  -> AggProcessConfig-  -> IO AggProcess+initWorkerCSV' ::+  -- | Target file name+  FilePath ->+  AggProcessConfig ->+  IO AggProcess initWorkerCSV' fp cfg = do   !res <- fileExist fp   !h <- openFile fp AppendMode@@ -84,34 +91,34 @@     T.hPutStrLn h $ rowToStr defCSVSettings . M.keys $ aggToCSV def   return $ putAggregateCSV h cfg - -------------------------------------------------------------------------------+ -- | Initialize a Graphite backend-initWorkerGraphite-    :: ConnectInfo-    -- ^ Redis connection-    -> Int-    -- ^ Aggregation period / flush interval in seconds-    -> HostName-    -- ^ Graphite host-    -> Int-    -- ^ Graphite port-    -> AggProcessConfig-    -> IO ()+initWorkerGraphite ::+  -- | Redis connection+  ConnectInfo ->+  -- | Aggregation period / flush interval in seconds+  Int ->+  -- | Graphite host+  HostName ->+  -- | Graphite port+  Int ->+  AggProcessConfig ->+  IO () initWorkerGraphite conn n server port cfg =-    initWorker "Graphite Worker" conn n =<< initWorkerGraphite' server port cfg-+  initWorker "Graphite Worker" conn n =<< initWorkerGraphite' server port cfg  -------------------------------------------------------------------------------+ -- | Crete an AggProcess that dumps to graphite. Use this to compose -- with other AggProcesses-initWorkerGraphite'-    :: HostName-    -- ^ Graphite host-    -> Int-    -- ^ Graphite port-    -> AggProcessConfig-    -> IO AggProcess+initWorkerGraphite' ::+  -- | Graphite host+  HostName ->+  -- | Graphite port+  Int ->+  AggProcessConfig ->+  IO AggProcess initWorkerGraphite' server port cfg = do   addr <- resolve server (fromIntegral port)   sock <- open addr@@ -122,79 +129,82 @@     portNumberToServiceName :: N.PortNumber -> N.ServiceName     portNumberToServiceName = show     resolve host portNumber = do-      let hints = N.defaultHints { N.addrSocketType = N.Stream }-      addr:_ <- N.getAddrInfo-        (Just hints)-        (Just host)-        (Just (portNumberToServiceName portNumber))+      let hints = N.defaultHints {N.addrSocketType = N.Stream}+      addr : _ <-+        N.getAddrInfo+          (Just hints)+          (Just host)+          (Just (portNumberToServiceName portNumber))       return addr     open addr = do-      sock <- N.socket-        (N.addrFamily addr)-        (N.addrSocketType addr)-        (N.addrProtocol addr)+      sock <-+        N.socket+          (N.addrFamily addr)+          (N.addrSocketType addr)+          (N.addrProtocol addr)       N.connect sock (N.addrAddress addr)       return sock - -------------------------------------------------------------------------------+ -- | Generic utility for making worker backends. Will retry -- indefinitely with exponential backoff. initWorker :: String -> ConnectInfo -> Int -> AggProcess -> IO () initWorker wname conn n f = do-    p <- createInstrumentPool conn-    indefinitely' $ work p n f+  p <- createInstrumentPool conn+  indefinitely' $ work p n f   where     indefinitely' = indefinitely wname (seconds n) -- -------------------------------------------------------------------------------+ -- | Extract statistics out of the given sample for this flush period mkStats :: Set.Set Quantile -> Sample -> Stats-mkStats qs s = Stats { smean = mean s-                     , ssum = V.sum s-                     , scount = V.length s-                     , smax = V.maximum s-                     , smin = V.minimum s-                     , srange = range s-                     , sstdev = stdDev s-                     , sskewness = skewness s-                     , skurtosis = kurtosis s-                     , squantiles = quantiles }+mkStats qs s =+  Stats+    { smean = mean s,+      ssum = V.sum s,+      scount = V.length s,+      smax = V.maximum s,+      smin = V.minimum s,+      srange = range s,+      sstdev = stdDev s,+      sskewness = skewness s,+      skurtosis = kurtosis s,+      squantiles = quantiles+    }   where     quantiles = M.fromList (mkQ 100 . quantile <$> Set.toList qs)     mkQ mx i = (i, Q.weightedAvg i mx s) - -------------------------------------------------------------------------------+ -- | Go over all pending stats buffers in redis. work :: R.Connection -> Int -> AggProcess -> IO () work r n f = runRedis r $ do-    dbg "entered work block"-    estimate <- either (const 0) id <$> scard packetsKey-    runConduit $-      CL.unfoldM nextKey estimate .|-        CL.mapM_ (processSampler n f)+  dbg "entered work block"+  estimate <- either (const 0) id <$> scard packetsKey+  runConduit $+    CL.unfoldM nextKey estimate+      .| CL.mapM_ (processSampler n f)   where     nextKey estRemaining       | estRemaining > 0 = do-         mk <- spop packetsKey-         return $ case mk of-           Right (Just k) -> Just (k, estRemaining - 1)-           _              -> Nothing+        mk <- spop packetsKey+        return $ case mk of+          Right (Just k) -> Just (k, estRemaining - 1)+          _ -> Nothing       | otherwise = return Nothing - --------------------------------------------------------------------------------processSampler-    :: Int-    -- ^ Flush interval - determines resolution-    -> AggProcess-    -- ^ What to do with aggregation results-    -> B.ByteString-    -- ^ Redis buffer for this metric-    -> Redis ()+processSampler ::+  -- | Flush interval - determines resolution+  Int ->+  -- | What to do with aggregation results+  AggProcess ->+  -- | Redis buffer for this metric+  B.ByteString ->+  Redis () processSampler n (AggProcess cfg f) k = do   packets <- popLAll k   case packets of@@ -206,13 +216,15 @@           byDims :: M.Map Dimensions [SubmissionPacket]           byDims = collect packets spDimensions id           mkAgg xs =-              case spPayload $ head xs of-                Samples _ -> AggStats . mkStats qs . V.fromList .-                             concatMap (unSamples . spPayload) $-                             xs-                Counter _ -> AggCount . sum .-                             map (unCounter . spPayload) $-                             xs+            case spPayload $ head xs of+              Samples _ ->+                AggStats . mkStats qs . V.fromList+                  . concatMap (unSamples . spPayload)+                  $ xs+              Counter _ ->+                AggCount . sum+                  . map (unCounter . spPayload)+                  $ xs       t <- (fromIntegral . (* n) . (`div` n) . round) `liftM` liftIO TM.getTime       let aggs = map mkDimsAgg $ M.toList $ expandDims $ byDims           mkDimsAgg (dims, ps) = Aggregated t nm (mkAgg ps) dims@@ -221,8 +233,8 @@   where     quantilesFn = metricQuantiles cfg - -------------------------------------------------------------------------------+ -- | Take a map of packets by dimensions and *add* aggregations of the -- existing dims that isolate each distinct dimension/dimensionvalue -- pair + one more entry with an empty dimension set that aggregates@@ -241,10 +253,11 @@ -- , {d2=>d2v2} => p2 -- , {} => p1 + p2 -- }-expandDims-  :: forall packets. (Monoid packets, Eq packets)-  => M.Map Dimensions packets-  -> M.Map Dimensions packets+expandDims ::+  forall packets.+  (Monoid packets, Eq packets) =>+  M.Map Dimensions packets ->+  M.Map Dimensions packets expandDims m =   -- left-biased so technically if we have anything occupying the aggregated spots, leave them be   m <> additions <> fullAggregation@@ -255,191 +268,191 @@     mkIsolatedMap :: (DimensionName, DimensionValue) -> M.Map Dimensions packets     mkIsolatedMap dPair =       let matches = snd <$> filter ((== dPair) . fst) mFlat-      in if matches == mempty+       in if matches == mempty             then mempty             else M.singleton (uncurry M.singleton dPair) (mconcat matches)     mFlat :: [((DimensionName, DimensionValue), packets)]-    mFlat = [ ((dn, dv), packets)-            | (dimensionsMap, packets) <- M.toList m-            , (dn, dv) <- M.toList dimensionsMap]+    mFlat =+      [ ((dn, dv), packets)+        | (dimensionsMap, packets) <- M.toList m,+          (dn, dv) <- M.toList dimensionsMap+      ]     -- All packets across any combination of dimensions     fullAggregation = M.singleton mempty (mconcat (M.elems m)) - -- | A function that does something with the aggregation results. Can -- implement multiple backends simply using this. Note that Semigroup and Monoid instances are provided for defaulting and combining agg processes. data AggProcess = AggProcess-  { apConfig :: AggProcessConfig-  , apProc   :: Aggregated -> Redis ()+  { apConfig :: AggProcessConfig,+    apProc :: Aggregated -> Redis ()   } - instance Semigroup.Semigroup AggProcess where   (AggProcess cfg1 prc1) <> (AggProcess cfg2 prc2) =     AggProcess (cfg1 <> cfg2) (\agg -> prc1 agg >> prc2 agg) - instance Monoid AggProcess where   mempty = AggProcess mempty (const (pure ()))   mappend = (<>) - -------------------------------------------------------------------------------+ -- | General configuration for agg processes. Defaulted with 'def', -- 'defAggProcessConfig', and 'mempty'. Configurations can be combined -- with (<>) from Monoid or Semigroup. data AggProcessConfig = AggProcessConfig-  { metricQuantiles :: MetricName -> Set.Set Quantile-  -- ^ What quantiles should we calculate for any given metric, if-  -- any? We offer some common patterns for this in 'quantileMap',-  -- 'standardQuantiles', and 'noQuantiles'.+  { -- | What quantiles should we calculate for any given metric, if+    -- any? We offer some common patterns for this in 'quantileMap',+    -- 'standardQuantiles', and 'noQuantiles'.+    metricQuantiles :: MetricName -> Set.Set Quantile   } - instance Semigroup AggProcessConfig where   AggProcessConfig f1 <> AggProcessConfig f2 =     let f3 = f1 <> f2-    in AggProcessConfig f3-+     in AggProcessConfig f3  instance Monoid AggProcessConfig where   mempty = AggProcessConfig mempty   mappend = (<>) - -- | Uses 'standardQuantiles'. defAggProcessConfig :: AggProcessConfig defAggProcessConfig = AggProcessConfig standardQuantiles - instance Default AggProcessConfig where   def = defAggProcessConfig - -- | Regardless of metric, produce no quantiles. noQuantiles :: MetricName -> Set.Set Quantile noQuantiles = const mempty - -- | This is usually a good, comprehensive default. Produces quantiles -- 10,20,30,40,50,60,70,80,90,99. *Note:* for some backends like -- cloudwatch, each quantile produces an additional metric, so you -- should probably consider using something more limited than this. standardQuantiles :: MetricName -> Set.Set Quantile standardQuantiles _ =-  Set.fromList [Q 10,Q 20,Q 30,Q 40,Q 50,Q 60,Q 70,Q 80,Q 90,Q 99]-+  Set.fromList [Q 10, Q 20, Q 30, Q 40, Q 50, Q 60, Q 70, Q 80, Q 90, Q 99]  -- | If you have a fixed set of metric names, this is often a -- convenient way to express quantiles-per-metric.-quantileMap-  :: M.Map MetricName (Set.Set Quantile)-  -> Set.Set Quantile-  -- ^ What to return on miss-  -> (MetricName -> Set.Set Quantile)+quantileMap ::+  M.Map MetricName (Set.Set Quantile) ->+  -- | What to return on miss+  Set.Set Quantile ->+  (MetricName -> Set.Set Quantile) quantileMap m qdef mn = fromMaybe qdef (M.lookup mn m) - -------------------------------------------------------------------------------+ -- | Store aggregation results in a CSV file putAggregateCSV :: Handle -> AggProcessConfig -> AggProcess putAggregateCSV h cfg = AggProcess cfg $ \agg ->   let d = rowToStr defCSVSettings $ aggToCSV agg-  in liftIO $ T.hPutStrLn h d-+   in liftIO $ T.hPutStrLn h d  typePrefix :: AggPayload -> T.Text-typePrefix AggStats{} = "samples"-typePrefix AggCount{} = "counts"-+typePrefix AggStats {} = "samples"+typePrefix AggCount {} = "counts"  -------------------------------------------------------------------------------+ -- | Push data into a Graphite database using the plaintext protocol putAggregateGraphite :: Handle -> AggProcessConfig -> AggProcess putAggregateGraphite h cfg = AggProcess cfg $ \agg ->-    let (ss, ts) = mkStatsFields agg-        -- Expand dimensions into one datum per dimension pair as the group-        mkLines (m, val) = for (M.toList (aggDimensions agg)) $ \(DimensionName dimName, DimensionValue dimVal) -> T.concat-            [ "inst."-            , typePrefix (aggPayload agg), "."-            ,  T.pack (metricName (aggName agg)), "."-            , m, "."-            , dimName, "."-            , dimVal, " "-            , val, " "-            , ts ]-    in liftIO $ mapM_ (mapM_ (T.hPutStrLn h) . mkLines) ss-+  let (ss, ts) = mkStatsFields agg+      -- Expand dimensions into one datum per dimension pair as the group+      mkLines (m, val) = for (M.toList (aggDimensions agg)) $ \(DimensionName dimName, DimensionValue dimVal) ->+        T.concat+          [ "inst.",+            typePrefix (aggPayload agg),+            ".",+            T.pack (metricName (aggName agg)),+            ".",+            m,+            ".",+            dimName,+            ".",+            dimVal,+            " ",+            val,+            " ",+            ts+          ]+   in liftIO $ mapM_ (mapM_ (T.hPutStrLn h) . mkLines) ss  -------------------------------------------------------------------------------+ -- | Pop all keys in a redis List popLAll :: (Serialize a, SC.SafeCopy a) => B.ByteString -> Redis [a] popLAll k = do   res <- popLMany k 100   case res of     [] -> return res-    _  -> (res ++ ) `liftM` popLAll k-+    _ -> (res ++) `liftM` popLAll k  -------------------------------------------------------------------------------+ -- | Pop up to N items from a queue. It will pop from left and preserve order. popLMany :: (Serialize a, SC.SafeCopy a) => B.ByteString -> Int -> Redis [a] popLMany k n = do-    res <- replicateM n pop-    case sequence res of-      Left _   -> return []-      Right xs -> return $ mapMaybe conv $ catMaybes xs-    where-      pop = R.lpop k-      conv x =  hush $ decodeCompress x-+  res <- replicateM n pop+  case sequence res of+    Left _ -> return []+    Right xs -> return $ mapMaybe conv $ catMaybes xs+  where+    pop = R.lpop k+    conv x = hush $ decodeCompress x  -------------------------------------------------------------------------------+ -- | Need to pull in a debugging library here. dbg :: (Monad m) => String -> m () dbg _ = return () - -- ------------------------------------------------------------------------------ -- dbg :: (MonadIO m) => String -> m () -- dbg s = debug $ "Instrument.Worker: " ++ s - -------------------------------------------------------------------------------+ -- | Expand count aggregation to have the full columns aggToCSV :: Aggregated -> M.Map T.Text T.Text-aggToCSV agg@Aggregated{..} = els <> defFields <> dimFields+aggToCSV agg@Aggregated {..} = els <> defFields <> dimFields   where     els :: MapRow T.Text-    els = M.fromList $-            ("metric", T.pack (metricName aggName)) :-            ("timestamp", ts) :-            fields+    els =+      M.fromList $+        ("metric", T.pack (metricName aggName)) :+        ("timestamp", ts) :+        fields     (fields, ts) = mkStatsFields agg-    defFields = M.fromList $ fst $ mkStatsFields $ agg { aggPayload =  (AggStats def) }-    dimFields = M.fromList [(k,v) | (DimensionName k, DimensionValue v) <- M.toList aggDimensions]-+    defFields = M.fromList $ fst $ mkStatsFields $ agg {aggPayload = (AggStats def)}+    dimFields = M.fromList [(k, v) | (DimensionName k, DimensionValue v) <- M.toList aggDimensions]  -------------------------------------------------------------------------------+ -- | Get agg results into a form ready to be output mkStatsFields :: Aggregated -> ([(T.Text, T.Text)], T.Text)-mkStatsFields Aggregated{..}  = (els, ts)-    where-      els =-        case aggPayload of-          AggStats Stats{..} ->-              [ ("mean", formatDecimal 6 False smean)-              , ("count", showT scount)-              , ("max", formatDecimal 6 False smax)-              , ("min", formatDecimal 6 False smin)-              , ("srange", formatDecimal 6 False srange)-              , ("stdDev", formatDecimal 6 False sstdev)-              , ("sum", formatDecimal 6 False ssum)-              , ("skewness", formatDecimal 6 False sskewness)-              , ("kurtosis", formatDecimal 6 False skurtosis)-              ] ++ (map mkQ $ M.toList squantiles)-          AggCount i ->-              [ ("count", showT i)]+mkStatsFields Aggregated {..} = (els, ts)+  where+    els =+      case aggPayload of+        AggStats Stats {..} ->+          [ ("mean", formatDecimal 6 False smean),+            ("count", showT scount),+            ("max", formatDecimal 6 False smax),+            ("min", formatDecimal 6 False smin),+            ("srange", formatDecimal 6 False srange),+            ("stdDev", formatDecimal 6 False sstdev),+            ("sum", formatDecimal 6 False ssum),+            ("skewness", formatDecimal 6 False sskewness),+            ("kurtosis", formatDecimal 6 False skurtosis)+          ]+            ++ (map mkQ $ M.toList squantiles)+        AggCount i ->+          [("count", showT i)] -      mkQ (k,v) = (T.concat ["percentile_", showT k], formatDecimal 6 False v)-      ts = formatInt aggTS+    mkQ (k, v) = (T.concat ["percentile_", showT k], formatDecimal 6 False v)+    ts = formatInt aggTS
test/src/Instrument/Tests/Arbitrary.hs view
@@ -1,24 +1,24 @@ {-# OPTIONS_GHC -fno-warn-orphans #-}-module Instrument.Tests.Arbitrary-    (-    ) where +module Instrument.Tests.Arbitrary+  (+  )+where  --------------------------------------------------------------------------------import           Test.QuickCheck-import           Test.QuickCheck.Instances ()---------------------------------------------------------------------------------import           Instrument.Types+ -------------------------------------------------------------------------------+import Instrument.Types+import Test.QuickCheck+import Test.QuickCheck.Instances () +-------------------------------------------------------------------------------  instance Arbitrary DimensionName where   arbitrary = DimensionName <$> arbitrary - instance Arbitrary DimensionValue where   arbitrary = DimensionValue <$> arbitrary-  instance Arbitrary MetricName where   arbitrary = MetricName <$> arbitrary
test/src/Instrument/Tests/Client.hs view
@@ -1,65 +1,70 @@ {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards   #-}+{-# LANGUAGE RecordWildCards #-}+ module Instrument.Tests.Client-    ( tests-    ) where+  ( tests,+  )+where  --------------------------------------------------------------------------------import           Control.Concurrent-import           Control.Concurrent.Async-import           Control.Concurrent.STM-import           Control.Exception.Safe     (Exception, throwM, tryAny)-import           Control.Monad-import           Control.Monad.IO.Class-import           Data.Default-import           Data.List                  (find)-import qualified Data.Map                   as M-import           Data.Monoid                as Monoid-import           Database.Redis-import           System.Timeout-import           Test.Tasty-import           Test.Tasty.HUnit-import           Test.Tasty.QuickCheck+import Control.Concurrent+import Control.Concurrent.Async+import Control.Concurrent.STM+import Control.Exception.Safe (Exception, throwM, tryAny)+import Control.Monad+import Control.Monad.IO.Class+import Data.Default+import Data.List (find)+import qualified Data.Map as M+import Data.Monoid as Monoid+import Database.Redis --------------------------------------------------------------------------------import           Instrument.Client-import           Instrument.Tests.Arbitrary ()-import           Instrument.Types-import           Instrument.Worker+import Instrument.Client+import Instrument.Tests.Arbitrary ()+import Instrument.Types+import Instrument.Worker+import System.Timeout+import Test.Tasty+import Test.Tasty.HUnit+import Test.Tasty.QuickCheck+ -------------------------------------------------------------------------------  tests :: TestTree-tests = testGroup "Instrument.Client"-    [ withRedisCleanup $ testCase "queue bounding works" . queue_bounding_test-    , withRedisCleanup $ testCase "multiple keys eventually get flushed" . multi_key_test-    , withRedisCleanup $ testCaseSteps "also times exceptions" . time_test-    , timerMetricNameTests+tests =+  testGroup+    "Instrument.Client"+    [ withRedisCleanup $ testCase "queue bounding works" . queue_bounding_test,+      withRedisCleanup $ testCase "multiple keys eventually get flushed" . multi_key_test,+      withRedisCleanup $ testCaseSteps "also times exceptions" . time_test,+      timerMetricNameTests     ]  ------------------------------------------------------------------------------- queue_bounding_test :: IO Connection -> IO () queue_bounding_test mkConn = do-    conn <- mkConn-    instr <- initInstrument redisCI icfg-    agg <- newEmptyMVar-    replicateM_ 2 (sampleI key DoNotAddHostDimension dims 1 instr >> sleepFlush)-    -- redis queue slots now full of the above aggregate-    -- bounds will drop these-    sampleI key DoNotAddHostDimension dims 100 instr-    sleepFlush-    let worker = work conn 1 (AggProcess defAggProcessConfig (liftIO . putMVar agg))-    withAsync worker $ \_ -> do-      Aggregated { aggPayload = AggStats Stats {..} } <- takeMVar agg-      assertEqual "throws away newer data exceeding bounds"-                  (2, 1, 1, 2)-                  (scount, smin, smax, ssum)-+  conn <- mkConn+  instr <- initInstrument redisCI icfg+  agg <- newEmptyMVar+  replicateM_ 2 (sampleI key DoNotAddHostDimension dims 1 instr >> sleepFlush)+  -- redis queue slots now full of the above aggregate+  -- bounds will drop these+  sampleI key DoNotAddHostDimension dims 100 instr+  sleepFlush+  let worker = work conn 1 (AggProcess defAggProcessConfig (liftIO . putMVar agg))+  withAsync worker $ \_ -> do+    Aggregated {aggPayload = AggStats Stats {..}} <- takeMVar agg+    assertEqual+      "throws away newer data exceeding bounds"+      (2, 1, 1, 2)+      (scount, smin, smax, ssum)  ------------------------------------------------------------------------------- sleepFlush :: IO () sleepFlush = threadDelay 1100000 -data TimeException =-  TimeException+data TimeException+  = TimeException   deriving (Show)  instance Exception TimeException@@ -97,35 +102,35 @@    sleepFlush -  let collectAggs = work conn 1 (AggProcess defAggProcessConfig (\agg -> liftIO (atomically (modifyTVar' aggsRef (agg:)))))+  let collectAggs = work conn 1 (AggProcess defAggProcessConfig (\agg -> liftIO (atomically (modifyTVar' aggsRef (agg :)))))    withAsync collectAggs $ \worker -> do-    maggs <- timeout 2000000 $ atomically $ do-      aggs <- readTVar aggsRef-      check (length aggs == 3)-      return aggs+    maggs <- timeout 2000000 $+      atomically $ do+        aggs <- readTVar aggsRef+        check (length aggs == 3)+        return aggs     cancel worker     case maggs of       Nothing -> assertFailure "Waited 2 seconds and never received any aggs!"       Just aggs -> do         let getCount payload =               case payload of-              (AggStats stats) -> scount stats-              (AggCount n) -> n+                (AggStats stats) -> scount stats+                (AggCount n) -> fromIntegral n              countMatches name n = do               void $ step name-              case filter ((== MetricName ("time." <> name)). aggName) aggs of-                  [agg] -> getCount (aggPayload agg) @?= n-                  _ -> assertFailure (name <> "has multiple aggregations")+              case filter ((== MetricName ("time." <> name)) . aggName) aggs of+                [agg] -> getCount (aggPayload agg) @?= n+                _ -> assertFailure (name <> "has multiple aggregations")          countMatches "instrument-test-timeex" 1         countMatches "instrument-test-timeex-error" 1         countMatches "instrument-test-time" 1          void $ step "instrument-test-time-error"-        length (filter ((== MetricName "time.instrument-test-time-error"). aggName) aggs) @?= 0-+        length (filter ((== MetricName "time.instrument-test-time-error") . aggName) aggs) @?= 0  ------------------------------------------------------------------------------- multi_key_test :: IO Connection -> IO ()@@ -136,12 +141,13 @@   sampleI "instrument-test1" DoNotAddHostDimension Monoid.mempty 1 instr   sampleI "instrument-test2" DoNotAddHostDimension mempty 2 instr   sleepFlush-  let collectAggs = work conn 1 (AggProcess defAggProcessConfig (\agg -> liftIO (atomically (modifyTVar' aggsRef (agg:)))))+  let collectAggs = work conn 1 (AggProcess defAggProcessConfig (\agg -> liftIO (atomically (modifyTVar' aggsRef (agg :)))))   withAsync collectAggs $ \worker -> do-    maggs <- timeout 2000000 $ atomically $ do-      aggs <- readTVar aggsRef-      check (length aggs >= 2)-      return aggs+    maggs <- timeout 2000000 $+      atomically $ do+        aggs <- readTVar aggsRef+        check (length aggs >= 2)+        return aggs     cancel worker     case maggs of       Nothing -> assertFailure "Waited 2 seconds and never received any aggs!"@@ -151,51 +157,48 @@               let magg = find ((== MetricName n) . aggName) aggs               case magg of                 Nothing -> assertFailure ("Expected to find agg with name " <> n <> " but could not")-                Just (Aggregated { aggPayload = AggStats (Stats { smean = actualMean})}) -> actualMean @?= expectedMean+                Just (Aggregated {aggPayload = AggStats (Stats {smean = actualMean})}) -> actualMean @?= expectedMean                 Just _ -> assertFailure ("Expected agg with name " <> n <> " to contain Stats but it did not.")         findAgg "instrument-test1" 1.0         findAgg "instrument-test2" 2.0 - ------------------------------------------------------------------------------- timerMetricNameTests :: TestTree-timerMetricNameTests = testGroup "timerMetricName"-  [ testProperty "is idempotent" $ \mn ->-      let r1 = timerMetricName mn-          r2 =  timerMetricName r1-      in r1 === r2-  , testProperty "is idempotent for timers" $ \(MetricName mnBase) ->-      let mn = MetricName (timerMetricNamePrefix <> mnBase)-          r1 = timerMetricName mn-          r2 =  timerMetricName r1-      in r1 === r2-  , testCase "adds time. prefix" $ do-      timerMetricName (MetricName "foo") @?= MetricName "time.foo"-  ]-+timerMetricNameTests =+  testGroup+    "timerMetricName"+    [ testProperty "is idempotent" $ \mn ->+        let r1 = timerMetricName mn+            r2 = timerMetricName r1+         in r1 === r2,+      testProperty "is idempotent for timers" $ \(MetricName mnBase) ->+        let mn = MetricName (timerMetricNamePrefix <> mnBase)+            r1 = timerMetricName mn+            r2 = timerMetricName r1+         in r1 === r2,+      testCase "adds time. prefix" $ do+        timerMetricName (MetricName "foo") @?= MetricName "time.foo"+    ]  ------------------------------------------------------------------------------- withRedisCleanup :: (IO Connection -> TestTree) -> TestTree withRedisCleanup = withResource (connect redisCI) cleanup   where-    cleanup conn = void $ runRedis conn $ do-      ks <- either mempty id <$> smembers packetsKey-      _ <- del (packetsKey:ks)-      quit-+    cleanup conn = void $+      runRedis conn $ do+        ks <- either mempty id <$> smembers packetsKey+        _ <- del (packetsKey : ks)+        quit  ------------------------------------------------------------------------------- redisCI :: ConnectInfo redisCI = defaultConnectInfo - icfg :: InstrumentConfig-icfg = def { redisQueueBound = Just 2 }-+icfg = def {redisQueueBound = Just 2}  key :: MetricName key = MetricName "instrument-test"-  dims :: Dimensions dims = M.fromList [(DimensionName "server", DimensionValue "app1")]
+ test/src/Instrument/Tests/Counter.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# OPTIONS_GHC -Wno-name-shadowing #-}++module Instrument.Tests.Counter+  ( tests,+  )+where++import Instrument.Counter+import Test.Tasty+import Test.Tasty.HUnit+import Test.Tasty.QuickCheck+import Control.Monad++tests :: TestTree+tests =+  testGroup+    "Instrument.Counter"+    [ testCase "is zero after initialized" $ do+        c <- newCounter+        val <- readCounter c+        assertEqual "should be zero after initialized" 0 val,++      testProperty "registers arbitrary number of hits: increment" $ \(NonNegative (n :: Integer)) -> ioProperty $ do+        c <- newCounter+        forM_ [1..n] (\_ -> increment c)+        val <- readCounter c+        pure $ fromIntegral n === val,++      testProperty "registers arbitrary number of hits: add" $ \(NonNegative (n :: Integer)) -> ioProperty $ do+        c <- newCounter+        add (fromIntegral n) c+        val <- readCounter c+        pure $ fromIntegral n === val,++      testProperty "reset brings back to zero" $ \(NonNegative (n :: Integer)) -> ioProperty $ do+        c <- newCounter+        add (fromIntegral n) c+        val <- resetCounter c+        assertEqual "should match" (fromIntegral n) val++        val <- readCounter c+        pure $ 0 === val,++      testProperty "registers arbitrary number of hits close to rollover" $ \(NonNegative (n :: Integer)) -> ioProperty $ do+        c <- newCounter+        let offset = maxBound - 5+        add offset c+        val <- resetCounter c+        assertEqual "offset should match" (fromIntegral offset) val++        val <- readCounter c+        assertEqual "should be zero after reset" 0 val++        forM_ [1..n] (\_ -> increment c)+        val <- readCounter c+        pure $ fromIntegral n === val+    ]
test/src/Instrument/Tests/Types.hs view
@@ -1,33 +1,37 @@ {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell   #-}-module Instrument.Tests.Types-    ( tests-    ) where+{-# LANGUAGE TemplateHaskell #-} +module Instrument.Tests.Types+  ( tests,+  )+where  --------------------------------------------------------------------------------import qualified Data.Map            as M-import           Path-import           Test.HUnit.SafeCopy-import           Test.Tasty-import           Test.Tasty.HUnit+import qualified Data.Map as M --------------------------------------------------------------------------------import           Instrument.Types+import Instrument.Types+import Path+import Test.HUnit.SafeCopy+import Test.Tasty+import Test.Tasty.HUnit+ -------------------------------------------------------------------------------  --TODO: test parse of .serialize files  tests :: TestTree-tests = testGroup "Instrument.Types"-  [ testCase "Stats SafeCopy" $-      testSafeCopy FailMissingFiles $(mkRelFile "test/data/Instrument/Types/Stats.safecopy") stats-  , testCase "Payload SafeCopy" $-      testSafeCopy FailMissingFiles $(mkRelFile "test/data/Instrument/Types/Payload.safecopy") payload-  , testCase "SubmissionPacket SafeCopy" $-      testSafeCopy FailMissingFiles $(mkRelFile "test/data/Instrument/Types/SubmissionPacket.safecopy") submissionPacket-  , testCase "Aggregated SafeCopy" $-      testSafeCopy FailMissingFiles $(mkRelFile "test/data/Instrument/Types/Aggregated.safecopy") aggregated-  ]+tests =+  testGroup+    "Instrument.Types"+    [ testCase "Stats SafeCopy" $+        testSafeCopy FailMissingFiles $(mkRelFile "test/data/Instrument/Types/Stats.safecopy") stats,+      testCase "Payload SafeCopy" $+        testSafeCopy FailMissingFiles $(mkRelFile "test/data/Instrument/Types/Payload.safecopy") payload,+      testCase "SubmissionPacket SafeCopy" $+        testSafeCopy FailMissingFiles $(mkRelFile "test/data/Instrument/Types/SubmissionPacket.safecopy") submissionPacket,+      testCase "Aggregated SafeCopy" $+        testSafeCopy FailMissingFiles $(mkRelFile "test/data/Instrument/Types/Aggregated.safecopy") aggregated+    ]   where     stats = Stats 1 2 3 4 5 6 7 8 9 (M.singleton 10 11)     payload = Samples [1.2, 2.3, 4.5]
test/src/Instrument/Tests/Utils.hs view
@@ -1,51 +1,55 @@-{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TemplateHaskell     #-}+{-# LANGUAGE TemplateHaskell #-}+ module Instrument.Tests.Utils-    ( tests-    ) where+  ( tests,+  )+where  --------------------------------------------------------------------------------import           Control.Concurrent-import           Control.Monad-import qualified Data.ByteString       as B-import           Data.IORef-import qualified Data.Map              as M-import           Data.SafeCopy-import           Data.Serialize-import           Path-import qualified Path.IO               as PIO-import           System.Timeout-import           Test.Tasty-import           Test.Tasty.HUnit-import           Test.Tasty.QuickCheck---------------------------------------------------------------------------------import           Instrument.Types-import           Instrument.Utils+import Control.Concurrent+import Control.Monad+import qualified Data.ByteString as B+import Data.IORef+import qualified Data.Map as M+import Data.SafeCopy+import Data.Serialize --------------------------------------------------------------------------------+import Instrument.Types+import Instrument.Utils+import Path+import qualified Path.IO as PIO+import System.Timeout+import Test.Tasty+import Test.Tasty.HUnit+import Test.Tasty.QuickCheck +-------------------------------------------------------------------------------  tests :: TestTree-tests = testGroup "Instrument.Utils"-    [ encodeDecodeTests-    , withResource spawnWorker killWorker $ testCase "indefinitely retries" . indefinitely_retry+tests =+  testGroup+    "Instrument.Utils"+    [ encodeDecodeTests,+      withResource spawnWorker killWorker $ testCase "indefinitely retries" . indefinitely_retry     ] - ------------------------------------------------------------------------------- encodeDecodeTests :: TestTree-encodeDecodeTests = testGroup "encodeCompress/decodeCompress"-  [ testProperty "encode/decode compress roundtrip" encode_decode_roundtrip-  , testCase "Stats decode" $-      testDecode $(mkRelFile "test/data/Instrument/Types/Stats.gz") stats-  , testCase "Payload decode" $-      testDecode $(mkRelFile "test/data/Instrument/Types/Payload.gz") payload-  , testCase "SubmissionPacket decode" $-      testDecode $(mkRelFile "test/data/Instrument/Types/SubmissionPacket.gz") submissionPacket-  , testCase "Aggregated decode" $-      testDecode $(mkRelFile "test/data/Instrument/Types/Aggregated.gz") aggregated-  ]+encodeDecodeTests =+  testGroup+    "encodeCompress/decodeCompress"+    [ testProperty "encode/decode compress roundtrip" encode_decode_roundtrip,+      testCase "Stats decode" $+        testDecode $(mkRelFile "test/data/Instrument/Types/Stats.gz") stats,+      testCase "Payload decode" $+        testDecode $(mkRelFile "test/data/Instrument/Types/Payload.gz") payload,+      testCase "SubmissionPacket decode" $+        testDecode $(mkRelFile "test/data/Instrument/Types/SubmissionPacket.gz") submissionPacket,+      testCase "Aggregated decode" $+        testDecode $(mkRelFile "test/data/Instrument/Types/Aggregated.gz") aggregated+    ]   where     stats = Stats 1 2 3 4 5 6 7 8 9 (M.singleton 10 11)     payload = Samples [1.2, 2.3, 4.5]@@ -60,23 +64,20 @@       res <- decodeCompress <$> B.readFile (toFilePath fp)       (res :: Either String a) @?= Right v - ------------------------------------------------------------------------------- encode_decode_roundtrip :: String -> Property encode_decode_roundtrip a = roundtrip a === Right a   where     roundtrip = decodeCompress . encodeCompress - ------------------------------------------------------------------------------- indefinitely_retry :: IO (MVar (), t1, t) -> IO () indefinitely_retry setup = do-    (called, _signal, _) <- setup-    wasCalled <- timeout (milliseconds 100) $ takeMVar called-    assertEqual "retries until success" (Just ()) wasCalled+  (called, _signal, _) <- setup+  wasCalled <- timeout (milliseconds 100) $ takeMVar called+  assertEqual "retries until success" (Just ()) wasCalled   where - ------------------------------------------------------------------------------- spawnWorker :: IO (MVar (), IO (), ThreadId) spawnWorker = do@@ -84,21 +85,18 @@   tid <- forkIO $ indefinitely "Test Worker" 0 signal   return (called, signal, tid) - ------------------------------------------------------------------------------- killWorker :: (t1, t, ThreadId) -> IO () killWorker (_, _, tid) = killThread tid - ------------------------------------------------------------------------------- nopeNopeYep :: IO (MVar (), IO ()) nopeNopeYep = do-    counter <- newIORef (1 :: Int)-    mv <- newEmptyMVar-    return $ (mv, go counter mv)+  counter <- newIORef (1 :: Int)+  mv <- newEmptyMVar+  return $ (mv, go counter mv)   where     go counter mv = do       count <- readIORef counter-      modifyIORef' counter (+1)+      modifyIORef' counter (+ 1)       when (count > 2) $ void $ tryPutMVar mv ()-
test/src/Instrument/Tests/Worker.hs view
@@ -1,62 +1,68 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings          #-}-module Instrument.Tests.Worker-    ( tests-    ) where+{-# LANGUAGE OverloadedStrings #-} +module Instrument.Tests.Worker+  ( tests,+  )+where  --------------------------------------------------------------------------------import qualified Data.Map                   as M-import           Data.Monoid                as Monoid-import           Test.Tasty-import           Test.Tasty.HUnit-import           Test.Tasty.QuickCheck---------------------------------------------------------------------------------import           Instrument.Tests.Arbitrary ()-import           Instrument.Types-import           Instrument.Worker+import qualified Data.Map as M+import Data.Monoid as Monoid -------------------------------------------------------------------------------+import Instrument.Tests.Arbitrary ()+import Instrument.Types+import Instrument.Worker+import Test.Tasty+import Test.Tasty.HUnit+import Test.Tasty.QuickCheck +-------------------------------------------------------------------------------  tests :: TestTree-tests = testGroup "Instrument.Worker"-  [ expandDimsTests-  ]-+tests =+  testGroup+    "Instrument.Worker"+    [ expandDimsTests+    ]  ------------------------------------------------------------------------------- expandDimsTests :: TestTree-expandDimsTests = testGroup "expandDims"-  [ testProperty "input is a submap of output" $ \(Dims dims) ->-      let res = expandDims dims-      in counterexample-           ("Expected " <> show dims <> " to be a submap of " <> show res)-           (dims `M.isSubmapOf` res)-  , testProperty "always includes an aggregate with no dimensions" $ \(Dims dims) ->-      let res = expandDims dims-      in M.member Monoid.mempty res-  --TODO: more and then a test of the worked example-  , testProperty "no one member exceeds the total number of packets" $ \(Dims dims) ->-      let totalPacketCount = sum (length <$> dims)-          res = expandDims dims-          lengths = length <$> res-      in all (<= totalPacketCount) lengths-  , testCase "worked example from the readme" $ do-      let m = M.fromList [-             (M.fromList [(d1, d1v1), (d2, d2v1)], [p1])-           , (M.fromList [(d1, d1v1), (d2, d2v2)], [p2])-           ]-      let expected = M.fromList [-             (M.fromList [(d1, d1v1), (d2, d2v1)], [p1])-           , (M.fromList [(d1, d1v1), (d2, d2v2)], [p2])-           -- additions-           , (M.fromList [(d1, d1v1)], [p1, p2])-           , (M.fromList [(d2, d2v1)], [p1])-           , (M.fromList [(d2, d2v2)], [p2])-           , ((M.fromList []), [p1, p2])-           ]-      expandDims m @?= expected-  ]+expandDimsTests =+  testGroup+    "expandDims"+    [ testProperty "input is a submap of output" $ \(Dims dims) ->+        let res = expandDims dims+         in counterexample+              ("Expected " <> show dims <> " to be a submap of " <> show res)+              (dims `M.isSubmapOf` res),+      testProperty "always includes an aggregate with no dimensions" $ \(Dims dims) ->+        let res = expandDims dims+         in M.member Monoid.mempty res,+      --TODO: more and then a test of the worked example+      testProperty "no one member exceeds the total number of packets" $ \(Dims dims) ->+        let totalPacketCount = sum (length <$> dims)+            res = expandDims dims+            lengths = length <$> res+         in all (<= totalPacketCount) lengths,+      testCase "worked example from the readme" $ do+        let m =+              M.fromList+                [ (M.fromList [(d1, d1v1), (d2, d2v1)], [p1]),+                  (M.fromList [(d1, d1v1), (d2, d2v2)], [p2])+                ]+        let expected =+              M.fromList+                [ (M.fromList [(d1, d1v1), (d2, d2v1)], [p1]),+                  (M.fromList [(d1, d1v1), (d2, d2v2)], [p2]),+                  -- additions+                  (M.fromList [(d1, d1v1)], [p1, p2]),+                  (M.fromList [(d2, d2v1)], [p1]),+                  (M.fromList [(d2, d2v2)], [p2]),+                  ((M.fromList []), [p1, p2])+                ]+        expandDims m @?= expected+    ]   where     d1 = DimensionName "d1"     d2 = DimensionName "d2"@@ -67,8 +73,6 @@     p1 = "p1"     p2 :: String     p2 = "p2"--  -- | Fixes the packet type for type inference newtype Dims = Dims (M.Map Dimensions [Char])
test/src/Main.hs view
@@ -1,18 +1,25 @@ module Main (main) where  --------------------------------------------------------------------------------import           Test.Tasty+ ------------------------------------------------------------------------------- import qualified Instrument.Tests.Client import qualified Instrument.Tests.Types import qualified Instrument.Tests.Utils import qualified Instrument.Tests.Worker+import qualified Instrument.Tests.Counter+import Test.Tasty+ -------------------------------------------------------------------------------  main :: IO ()-main = defaultMain $ testGroup "tests"-    [ Instrument.Tests.Utils.tests-    , Instrument.Tests.Client.tests-    , Instrument.Tests.Types.tests-    , Instrument.Tests.Worker.tests-    ]+main =+  defaultMain $+    testGroup+      "tests"+      [ Instrument.Tests.Utils.tests,+        Instrument.Tests.Client.tests,+        Instrument.Tests.Types.tests,+        Instrument.Tests.Worker.tests,+        Instrument.Tests.Counter.tests+      ]