packages feed

ekg-push (empty) → 0.0.2

raw patch · 9 files changed

+390/−0 lines, 9 filesdep +basedep +bytestringdep +ekg-coresetup-changed

Dependencies added: base, bytestring, ekg-core, ekg-push, text, time, unordered-containers

Files

+ CHANGES.md view
@@ -0,0 +1,7 @@+## 0.0.1 (2015-03-20)++ * Push agents "subscribe" via IO Action callbacks.++## 0.0.2 (2015-04-05)++ * Push agents subscribe via a broadcast channel.
+ LICENSE view
@@ -0,0 +1,23 @@+Copyright (c) 2015, Andrew Darqui+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++* Redistributions of source code must retain the above copyright notice, this+  list of conditions and the following disclaimer.++* Redistributions in binary form must reproduce the above copyright notice,+  this list of conditions and the following disclaimer in the documentation+  and/or other materials provided with the distribution.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ LICENSE.Tibell view
@@ -0,0 +1,30 @@+Copyright (c) 2014, Johan Tibell++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Johan Tibell nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Makefile view
@@ -0,0 +1,3 @@+all:+	cabal sandbox init+	cabal install
+ README.md view
@@ -0,0 +1,51 @@+# Modifications of ekg-statsd by Johan Tibell to provide a generic "push" framework.++You will notice this is almost identical to ekg-statsd. It's just me trying to abstract away the basic functionality found in ekg-statsd, so not to have to duplicate this code anywhere else. Eventually I may try to clean up this "abstraction" and create a PR in ekg-core.++# TODO: Soon++So, the idea of ekg-push is to create a simple framework so that I can create agents such as:+- ekg-push-redis+- ekg-push-file+- ekg-push-statsd+- ekg-push-uberlog++etc..++Push agents simply 'subscribe' to the push handle returned by forkPush. Once subscribed, agents call consume and handle the Metric.Sample data how they want.++# Installation++You can just type 'make' to install ekg-push into a local sandbox. Or you can use cabal:++```+cabal install ekg-push+```++# Getting started++See examples/basic.hs++```+main :: IO ()+main = do+    store <- newStore+    registerGcMetrics store+    iters <- createCounter "iterations" store+    push <- forkPush defaultPushOptions { prefix = "pfx", suffix = "sfx" } store++    ch1 <- subscribe push+    _ <- forkIO $ forever $ do+            msg <- consume ch1+            putStrLn $ "subscription #1: " ++ show  msg++    let loop n = do+            evaluate $ mean [1..n]+            Counter.inc iters+            threadDelay 2000+            loop n++    loop 1000000+```++-- adarqui
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ System/Remote/Monitoring/Push.hs view
@@ -0,0 +1,185 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+-- | This library lets you push metric samples to a broadcast channel.+-- Consumers can then persist the metrics samples as they wish.+-- ekg-push is based heavily off of the ekg-statsd package which+-- can be found at: https://github.com/tibbe/ekg-statsd+--+-- Example usage:+--+-- > main = do+-- >     store <- newStore+-- >     push <- forkPush defaultPushOptions store+-- >     ch <- subscribe push+-- >     sample <- consume ch+-- >     putStrLn $ show sample+--+-- You probably want to include some of the predefined metrics defined+-- in the ekg-core package, by calling e.g. the 'registerGcStats'+-- function defined in that package.+module System.Remote.Monitoring.Push+    (+      Push+    , PushChan+    , PushOptions(..)+    , pushThreadId+    , forkPush+    , defaultPushOptions+    , subscribe+    , consume+    ) where++import Control.Concurrent (ThreadId, myThreadId, threadDelay, throwTo)+import Control.Concurrent.Chan (Chan, newChan, writeChan, readChan, dupChan)+import qualified Data.HashMap.Strict as M+import Data.Int (Int64)+import qualified Data.Text as T+import Data.Time.Clock.POSIX (getPOSIXTime)+import qualified System.Metrics as Metrics++#if __GLASGOW_HASKELL__ >= 706+import Control.Concurrent (forkFinally)+#else+import Control.Concurrent (forkIO)+import Control.Exception (SomeException, mask, try)+import Prelude hiding (catch)+#endif++-- | A handle that can be used to control the push sync thread.+-- Created by 'forkPush'.+data Push = Push+    { threadId :: {-# UNPACK #-} !ThreadId+    , mainCh :: Chan (Metrics.Sample)+    }++-- | A new PushChan is created on every call to subscribe.+-- This is essentially a dupChan of our main channel (mainCh).+data PushChan = PushChan+    { ch :: PushChanType }+    +type PushChanType = Chan Metrics.Sample++-- | The thread ID of the push sync thread. You can stop the sync by+-- killing this thread (i.e. by throwing it an asynchronous+-- exception.)+pushThreadId :: Push -> ThreadId+pushThreadId = threadId++-- | Options to control how to connect to the push server and how+-- often to flush metrics. The flush interval should be shorter than+-- the flush interval push itself uses to flush data to its+-- backends.+data PushOptions = PushOptions+    {+      -- | Data push interval, in ms.+      flushInterval :: !Int++      -- | Print debug output to stderr.+    , debug :: !Bool++      -- | Prefix to add to all metric names.+    , prefix :: !T.Text++      -- | Suffix to add to all metric names. This is particularly+      -- useful for sending per host stats by settings this value to:+      -- @takeWhile (/= \'.\') \<$\> getHostName@, using @getHostName@+      -- from the @Network.BSD@ module in the network package.+    , suffix :: !T.Text+    }++-- | Defaults:+--+-- * @flushInterval@ = @1000@+--+-- * @debug@ = @False@+defaultPushOptions :: PushOptions+defaultPushOptions = PushOptions+    { flushInterval = 1000+    , debug         = False+    , prefix        = ""+    , suffix        = ""+    }++-- | Create a thread that periodically flushes the metrics in the+-- store to push.+forkPush :: PushOptions  -- ^ Options+           -> Metrics.Store  -- ^ Metric store+           -> IO Push      -- ^ Push sync handle+forkPush opts store = do+    me <- myThreadId+    ch <- newChan+    tid <- forkFinally (loop ch store emptySample opts) $ \ r -> do+        case r of+            Left e  -> throwTo me e+            Right _ -> return ()+    return $ Push tid ch+    where+        emptySample = M.empty++loop :: PushChanType -- ^ ekg-push clients subscribe to this channel+     -> Metrics.Store   -- ^ Metric Store+     -> Metrics.Sample  -- ^ Last sampled metrics+     -> PushOptions  -- ^ Options+     -> IO ()+loop ch store lastSample opts = do+    start <- time+    sample <- Metrics.sampleAll store+    let !diff = diffSamples opts lastSample sample+    writeChan ch diff -- Write the Metrics.Sample to our broadcast channel.+    end <- time+    threadDelay (flushInterval opts * 1000 - fromIntegral (end - start))+    loop ch store sample opts++-- | Subscribe to the push broadcast channel.+subscribe :: Push -> IO PushChan+subscribe Push{..} = do+    ch' <- dupChan mainCh+    return $ PushChan {+        ch = ch'+    }++-- | Consume a Metrics.Sample message from a subscribed channel.+consume :: PushChan -> IO Metrics.Sample+consume PushChan{..} = readChan ch++-- | Microseconds since epoch.+time :: IO Int64+time = (round . (* 1000000.0) . toDouble) `fmap` getPOSIXTime+  where toDouble = realToFrac :: Real a => a -> Double++diffSamples :: PushOptions -> Metrics.Sample -> Metrics.Sample -> Metrics.Sample+diffSamples opts prev curr = M.foldlWithKey' combine M.empty curr+  where+    combine m name new = case M.lookup name prev of+        Just old -> case diffMetric old new of+            Just val -> M.insert name' val m+            Nothing  -> m+        _        -> M.insert name' new m+        where+            name' = T.append (prefix opts) (T.append name (suffix opts))++    diffMetric :: Metrics.Value -> Metrics.Value -> Maybe Metrics.Value+    diffMetric (Metrics.Counter n1) (Metrics.Counter n2)+        | n1 == n2  = Nothing+        | otherwise = Just $! Metrics.Counter $ n2 - n1+    diffMetric (Metrics.Gauge n1) (Metrics.Gauge n2)+        | n1 == n2  = Nothing+        | otherwise = Just $ Metrics.Gauge n2+    diffMetric (Metrics.Label n1) (Metrics.Label n2)+        | n1 == n2  = Nothing+        | otherwise = Just $ Metrics.Label n2+    -- Distributions are assumed to be non-equal.+    diffMetric _ _  = Nothing++------------------------------------------------------------------------+-- Backwards compatibility shims++#if __GLASGOW_HASKELL__ < 706+forkFinally :: IO a -> (Either SomeException a -> IO ()) -> IO ThreadId+forkFinally action and_then =+  mask $ \restore ->+    forkIO $ try (restore action) >>= and_then+#endif
+ ekg-push.cabal view
@@ -0,0 +1,40 @@+name:                   ekg-push+version:                0.0.2+synopsis:               Small framework to push metric deltas to a broadcast channel using the ekg-core library.+description:+  This library lets you push metric samples to a broadcast channel. Consumers can then persist the samples as they wish.+homepage:               https://github.com/adarqui/ekg-push+bug-reports:            https://github.com/adarqui/ekg-push/issues+license:                BSD3+license-files:          LICENSE, LICENSE.Tibell+author:                 Andrew Darqui+maintainer:             andrew.darqui@gmail.com+category:               System+build-type:             Simple+extra-source-files:     CHANGES.md+cabal-version:          >=1.18+extra-source-files:     Makefile, README.md++library+    exposed-modules:     +                        System.Remote.Monitoring.Push+    build-depends:+                        base >= 4.5 && < 4.8,+                        bytestring < 1.0,+                        ekg-core == 0.1.0.3,+                        text < 1.3,+                        time < 1.5,+                        unordered-containers < 0.3+    default-language:   Haskell2010+    ghc-options:        -Wall++executable basic+    main-is:            basic.hs+    hs-source-dirs:     examples+    build-depends:      base >= 4.5 && <= 4.9, ekg-core == 0.1.0.3, ekg-push == 0.0.2+    default-language:   Haskell2010+    ghc-options:        -Wall++source-repository head+  type:                 git+  location:             https://github.com/adarqui/ekg-push.git
+ examples/basic.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE OverloadedStrings #-}+module Main+    ( main+    ) where++import Control.Concurrent (threadDelay, forkIO)+import Control.Exception (evaluate)+import Control.Monad (forever)+import Data.List (foldl')+import System.Metrics (newStore, registerGcMetrics, createCounter)+import qualified System.Metrics.Counter as Counter (inc)+import System.Remote.Monitoring.Push (PushOptions (..), forkPush, defaultPushOptions, subscribe, consume)++-- 'sum' is using a non-strict lazy fold and will blow the stack.+sum' :: Num a => [a] -> a+sum' = foldl' (+) 0++mean :: Fractional a => [a] -> a+mean xs = sum' xs / fromIntegral (length xs)++main :: IO ()+main = do+    store <- newStore+    registerGcMetrics store+    iters <- createCounter "iterations" store++    -- Register our main ekg push channel+    push <- forkPush defaultPushOptions { prefix = "pfx_", suffix = "_sfx" } store++    -- Create two different subscriptions (ch1 & ch2)+    ch1 <- subscribe push+    _ <- forkIO $ forever $ do+            msg <- consume ch1+            putStrLn $ "subscription #1: " ++ show  msg++    ch2 <- subscribe push+    _ <- forkIO $ forever $ do+            msg <- consume ch2+            putStrLn $ "subscription #2: " ++ show msg++    let +        loop :: Double -> IO ()+        loop n = do+            _ <- evaluate $ mean [1..n]+            Counter.inc iters+            threadDelay 2000+            loop n++    loop 1000000