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 +3/−0
- LICENSE +30/−0
- Setup.hs +2/−0
- ekg-bosun.cabal +32/−0
- src/System/Remote/Monitoring/Bosun.hs +157/−0
+ 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+ ]+ _ -> []