packages feed

ekg-bosun (empty) → 1.0.0

raw patch · 5 files changed

+224/−0 lines, 5 filesdep +aesondep +basedep +ekg-coresetup-changed

Dependencies added: aeson, base, ekg-core, http-client, lens, network, network-uri, old-locale, text, time, unordered-containers, vector, wreq

Files

+ Changelog.md view
@@ -0,0 +1,3 @@+## 1.0.0++* Initial release.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2014, Oliver Charles++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 Oliver Charles 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.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ ekg-bosun.cabal view
@@ -0,0 +1,32 @@+name: ekg-bosun+version: 1.0.0+synopsis: Send ekg metrics to a Bosun instance+homepage: http://github.com/ocharles/ekg-bosun+license: BSD3+license-file: LICENSE+author: Oliver Charles+maintainer: ollie@ocharles.org.uk+category: System+build-type: Simple+cabal-version: >=1.10+extra-source-files: Changelog.md++library+  exposed-modules: System.Remote.Monitoring.Bosun+  other-extensions: OverloadedStrings+  build-depends:+    aeson >= 0.8.0.2 && < 0.9,+    base >=4.7 && <4.8,+    ekg-core >=0.1 && <0.2,+    http-client >= 0.3.8.2 && < 0.4,+    lens,+    network >=2.6 && <2.7,+    network-uri >= 2.6.0.1 && < 2.7,+    text >=1.2 && <1.3,+    time >=1.4 && <1.5,+    unordered-containers >=0.2 && <0.3,+    vector >=0.10 && <0.11,+    wreq >= 0.2 && < 0.3,+    old-locale+  hs-source-dirs: src+  default-language: Haskell2010
+ src/System/Remote/Monitoring/Bosun.hs view
@@ -0,0 +1,157 @@+{-# LANGUAGE OverloadedStrings #-}++-- | This module lets you periodically flush metrics to a Bosun+-- backend. Example usage:+--+-- > main = do+-- >   store <- newStore+-- >   forkBosun defaultBosunOptions store+--+-- You probably want to include some of the predefined metrics defined+-- in the @ekg-core@ package, by calling e.g. the 'EKG.registerGcMetrics'+-- function defined in that package.+module System.Remote.Monitoring.Bosun+  ( BosunOptions(..)+  , defaultBosunOptions+  , forkBosun+  ) where++import Control.Applicative+import Control.Concurrent (ThreadId, forkFinally, myThreadId, threadDelay, throwTo)+import Control.Exception (try)+import Control.Lens hiding ((.=))+import Control.Monad (forever, when)+import Data.Aeson ((.=))+import Data.Int (Int64)+import Data.Monoid ((<>))+import System.IO.Unsafe (unsafePerformIO)+import System.Locale (defaultTimeLocale)++import qualified Data.Aeson as Aeson+import qualified Data.HashMap.Strict as HashMap+import qualified Data.Text as T+import qualified Data.Time as Time+import qualified Data.Time.Clock.POSIX as Time+import qualified Data.Vector as V+import qualified Network.BSD as Network+import qualified Network.HTTP.Client as HTTP+import qualified Network.Socket as Network+import qualified Network.URI as URI+import qualified Network.Wreq as Wreq+import qualified System.Metrics as EKG+import qualified System.Metrics.Distribution as Stats++--------------------------------------------------------------------------------+-- | Options to control how to connect to the Bosun server and how often to+-- flush metrics.+data BosunOptions = BosunOptions+  { -- | The route URL to Bosun.+    bosunRoot :: !URI.URI++    -- | The amount of time between sampling EKG metrics and pushing to Bosun.+  , flushInterval :: !Int++    -- | Tags to apply to all metrics.+  , tags :: !(HashMap.HashMap T.Text T.Text)+  } deriving (Eq, Show)+++--------------------------------------------------------------------------------+firstHostName :: T.Text+firstHostName = unsafePerformIO (T.pack <$> Network.getHostName)+{-# NOINLINE firstHostName #-}+++--------------------------------------------------------------------------------+-- | Defaults:+--+-- * @bosunRoot@ = @\"http://127.0.0.1:8070/\"@+--+-- * @tags@ = @[("host", hostname)]@+--+-- * @flushInterval@ = @10000@+defaultBosunOptions :: BosunOptions+defaultBosunOptions = BosunOptions+    { bosunRoot = URI.URI { URI.uriScheme = "http:"+                          , URI.uriAuthority = Just (URI.URIAuth { URI.uriUserInfo = ""+                                                                 , URI.uriRegName = "127.0.0.1"+                                                                 , URI.uriPort = ":8070"+                                                                 })+                          , URI.uriPath = "/"+                          , URI.uriQuery = ""+                          , URI.uriFragment = ""+                          }+    , tags          = HashMap.singleton "host" firstHostName+    , flushInterval = 10000+    }+++--------------------------------------------------------------------------------+-- | Create a thread that periodically flushes the metrics in 'EKG.Store' to+-- Bosun.+forkBosun :: BosunOptions -> EKG.Store -> IO ThreadId+forkBosun opts store = do+  parent <- myThreadId+  forkFinally (do manager <- HTTP.newManager HTTP.defaultManagerSettings+                  let wreqOptions = Wreq.defaults & Wreq.manager .~ Right manager+                  loop store wreqOptions opts)+              (\r -> do case r of+                          Left e  -> throwTo parent e+                          Right _ -> return ())+++--------------------------------------------------------------------------------+loop :: EKG.Store -> Wreq.Options -> BosunOptions -> IO ()+loop store httpOptions opts = forever $ do+  start <- time+  sample <- EKG.sampleAll store+  flushSample sample httpOptions opts+  end <- time+  threadDelay (flushInterval opts * 1000 - fromIntegral (end - start))++-- | Microseconds since epoch.+time :: IO Int64+time = (round . (* 1000000.0) . toDouble) `fmap` Time.getPOSIXTime+  where toDouble = realToFrac :: Real a => a -> Double++flushSample :: EKG.Sample -> Wreq.Options -> BosunOptions -> IO ()+flushSample sample httpOptions opts = do+  t <- Time.getCurrentTime+  V.mapM postOne (HashMap.foldlWithKey' (\ms k v -> pure (metrics k v t) <> ms) V.empty sample)+  return ()++  where+  postOne x =+    when (not (null x)) $ do+      res <- try (Wreq.postWith httpOptions+                                (URI.uriToString id ((bosunRoot opts) { URI.uriPath = "/api/put" }) "")+                                (Aeson.Array (V.fromList x)))+      case res of+        Left e -> do+          putStrLn $ "HTTP exception when posting ekg-bosun sample:"+          print (e :: HTTP.HttpException)++        Right _ ->+          return ()++  ametric n v t =+    Aeson.object [ "metric" .= n+                 , "value" .= v+                 , "timestamp" .= (Time.formatTime defaultTimeLocale "%s" t)+                 , "tags" .= Aeson.Object (Aeson.toJSON <$> tags opts)+                 ]++  metrics n v t =+    case v of+      EKG.Counter i -> [ ametric n i t ]+      EKG.Gauge i -> [ ametric n i t ]+      EKG.Distribution stats+        | Stats.count stats > 0+            -> [ ametric (n <> ".count") (Stats.count stats) t+              , ametric (n <> ".sum") (Stats.sum stats) t+              , ametric (n <> ".min") (Stats.min stats) t+              , ametric (n <> ".max") (Stats.max stats) t+              , ametric (n <> ".mean") (Stats.mean stats) t+              , ametric (n <> ".variance") (Stats.variance stats) t+              ]+      _ -> []