ekg 0.3.1.4 → 0.4.0.0
raw patch · 16 files changed
+621/−894 lines, 16 filesdep +ekg-coredep −containersdep ~aesondep ~network
Dependencies added: ekg-core
Dependencies removed: containers
Dependency ranges changed: aeson, network
Files
- CHANGES.md +97/−0
- System/Remote/Common.hs +0/−408
- System/Remote/Counter.hs +10/−20
- System/Remote/Counter/Internal.hs +0/−20
- System/Remote/Gauge.hs +13/−47
- System/Remote/Gauge/Internal.hs +0/−20
- System/Remote/Json.hs +108/−0
- System/Remote/Label.hs +10/−19
- System/Remote/Label/Internal.hs +0/−21
- System/Remote/Monitoring.hs +157/−116
- System/Remote/Snap.hs +62/−74
- assets/bootstrap-1.4.0.min.css +21/−21
- assets/index.html +2/−26
- assets/monitor.js +111/−87
- ekg.cabal +15/−14
- examples/Basic.hs +15/−1
+ CHANGES.md view
@@ -0,0 +1,97 @@+## 0.4.0.0 (2014-05-01)++ * Lots of the internals were split off into a new package, ekg-core.++ * The `Gauge.modify` function was removed, as it can't be supported+ by the new, more efficient implementation of gauges.++ * The JSON API was significantly overhauled. The the Haddock+ documentation for details.++ * The metric store used internally by the server is now exposed and+ can be used to share the same metric store between ekg and e.g.+ ekg-statsd.++ * It's now possible to provide a custom metric store to the server.++ * The getDistribution function was added.++ * The UI now has less special treatment for built-in metrics.++## 0.3.1.3 (2013-02-22)++ * Fixed security issue where ekg would always listen to all incoming+ requests, even if "localhost" was specified.++ * Always export par_tot_bytes_copied. Previously it was only exported+ if using base-4.6 and later.++## 0.3.1.2 (2012-09-18)++ * Support GHC 7.6++## 0.3.1.1 (2012-06-25)++ * Bump dependencies.++## 0.3.1.0 (2012-04-17)++ * Add labels, which are free-form string values exported by the+ monitoring server. Labels allow you to export e.g. the command+ line arguments used to start the executable or the host name it's+ running on.++## 0.3.0.4 (2012-04-03)++ * Add original JavaScript files to tarball to ease distribution+ packaging.++## 0.3.0.4 (2012-04-03)++ * Change icons to Creative Commons Attribution 3.0 licensed one++## 0.3.0.3 (2012-03-19)++ * Support Snap 0.8++## 0.3.0.2 (2012-03-07)++ * Don't require an internet connection, by serving Bootstrap CSS and+ jQuery from the monitoring server.++## 0.3.0.1 (2012-01-26)++ * Switch from Blueprint to Bootstrap CSS++ * Overhaul look-and-feel++## 0.3.0.0 (2012-01-01)++ * Add gauges and change counters to always be monotonically increasing++ * Add web interface column headers++ * Reorganize the web interface to show counters and gauges in separate sections++ * Change REST API to allow separate access to counters and gauges++ * Return the server time in the JSON response it and use server time+ instead of client time when graphing++ * Make it possible to graph user-defined counters and gauges++ * Format numbers using comma separators++ * Show a message box when the server can't be reached++## 0.2.0.0 (2011-12-30)++ * Add user-defined counters++ * Suppress Snap logging to stdio++ * Add REST-style API for accessing single counters++## 0.1.0.0 (2011-12-27)++ * First EKG release
− System/Remote/Common.hs
@@ -1,408 +0,0 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE FunctionalDependencies #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}--module System.Remote.Common- (- -- * Types- Counters- , Gauges- , Labels- , Server(..)-- , Ref(..)-- -- * User-defined counters, gauges, and labels- , getCounter- , getGauge- , getLabel-- , buildMany- , buildAll- , buildCombined- , buildOne-- , parseHttpAccept- ) where--import Control.Concurrent (ThreadId)-import Control.Monad (forM)-import Control.Monad.IO.Class (liftIO)-import qualified Data.Aeson.Encode as A-import Data.Aeson.Types ((.=))-import qualified Data.Aeson.Types as A-import qualified Data.ByteString as S-import qualified Data.ByteString.Char8 as S8-import qualified Data.ByteString.Lazy as L-import Data.Function (on)-import qualified Data.HashMap.Strict as M-import Data.IORef (IORef, atomicModifyIORef, readIORef)-import Data.Int (Int64)-import qualified Data.List as List-import qualified Data.Map as Map-import qualified Data.Text as T-import Data.Time.Clock.POSIX (getPOSIXTime)-import Data.Word (Word8)-import qualified GHC.Stats as Stats-import Prelude hiding (read)--import System.Remote.Counter (Counter)-import qualified System.Remote.Counter.Internal as Counter-import System.Remote.Gauge (Gauge)-import qualified System.Remote.Gauge.Internal as Gauge-import System.Remote.Label (Label)-import qualified System.Remote.Label.Internal as Label------------------------------------------------------------------------------ Map of user-defined counters.-type Counters = M.HashMap T.Text Counter---- Map of user-defined gauges.-type Gauges = M.HashMap T.Text Gauge---- Map of user-defined labels.-type Labels = M.HashMap T.Text Label---- | A handle that can be used to control the monitoring server.--- Created by 'forkServer'.-data Server = Server {- threadId :: {-# UNPACK #-} !ThreadId- , userCounters :: !(IORef Counters)- , userGauges :: !(IORef Gauges)- , userLabels :: !(IORef Labels)- }----------------------------------------------------------------------------- * User-defined counters, gauges and labels--class Ref r t | r -> t where- new :: IO r- read :: r -> IO t--instance Ref Counter Int where- new = Counter.new- read = Counter.read--instance Ref Gauge Int where- new = Gauge.new- read = Gauge.read--instance Ref Label T.Text where- new = Label.new- read = Label.read---- | 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.-getRef :: Ref r t- => T.Text -- ^ 'Ref' name- -> IORef (M.HashMap T.Text r) -- ^ Server that will serve the 'Ref'- -> IO r-getRef name mapRef = do- empty <- new- ref <- atomicModifyIORef mapRef $ \ m ->- case M.lookup name m of- Nothing -> let m' = M.insert name empty m- in (m', empty)- Just ref -> (m, ref)- return ref-{-# INLINABLE getRef #-}---- | Return the counter associated with the given name and server.--- Multiple calls to 'getCounter' with the same arguments will return--- the same counter. The first time 'getCounter' is called for a--- given name and server, a new, zero-initialized counter will be--- returned.-getCounter :: T.Text -- ^ Counter name- -> Server -- ^ Server that will serve the counter- -> IO Counter-getCounter name server = getRef name (userCounters server)---- | Return the gauge associated with the given name and server.--- Multiple calls to 'getGauge' with the same arguments will return--- the same gauge. The first time 'getGauge' is called for a given--- name and server, a new, zero-initialized gauge will be returned.-getGauge :: T.Text -- ^ Gauge name- -> Server -- ^ Server that will serve the gauge- -> IO Gauge-getGauge name server = getRef name (userGauges server)---- | Return the label associated with the given name and server.--- Multiple calls to 'getLabel' with the same arguments will return--- the same label. The first time 'getLabel' is called for a given--- name and server, a new, empty label will be returned.-getLabel :: T.Text -- ^ Label name- -> Server -- ^ Server that will serve the label- -> IO Label-getLabel name server = getRef name (userLabels server)----------------------------------------------------------------------------- * JSON serialization---- | All the stats exported by the server (i.e. GC stats plus user--- defined counters).-data Stats = Stats- !Stats.GCStats -- GC statistics- ![(T.Text, Json)] -- Counters- ![(T.Text, Json)] -- Gauges- ![(T.Text, Json)] -- Labels- {-# UNPACK #-} !Double -- Milliseconds since epoch--instance A.ToJSON Stats where- toJSON (Stats gcStats counters gauges labels t) = A.object $- [ "server_timestamp_millis" .= t- , "counters" .= Assocs (gcCounters ++ counters)- , "gauges" .= Assocs (gcGauges ++ gauges)- , "labels" .= Assocs (labels)- ]- where- (gcCounters, gcGauges) = partitionGcStats gcStats---- | 'Stats' encoded as a flattened JSON object.-newtype Combined = Combined Stats--instance A.ToJSON Combined where- toJSON (Combined (Stats s@(Stats.GCStats {..}) counters gauges labels t)) =- A.object $- [ "server_timestamp_millis" .= t- , "bytes_allocated" .= bytesAllocated- , "num_gcs" .= numGcs- , "max_bytes_used" .= maxBytesUsed- , "num_bytes_usage_samples" .= numByteUsageSamples- , "cumulative_bytes_used" .= cumulativeBytesUsed- , "bytes_copied" .= bytesCopied- , "current_bytes_used" .= currentBytesUsed- , "current_bytes_slop" .= currentBytesSlop- , "max_bytes_slop" .= maxBytesSlop- , "peak_megabytes_allocated" .= peakMegabytesAllocated- , "mutator_cpu_seconds" .= mutatorCpuSeconds- , "mutator_wall_seconds" .= mutatorWallSeconds- , "gc_cpu_seconds" .= gcCpuSeconds- , "gc_wall_seconds" .= gcWallSeconds- , "cpu_seconds" .= cpuSeconds- , "wall_seconds" .= wallSeconds- , "par_tot_bytes_copied" .= gcParTotBytesCopied s- , "par_avg_bytes_copied" .= gcParTotBytesCopied s- , "par_max_bytes_copied" .= parMaxBytesCopied- ] ++- map (uncurry (.=)) counters ++- map (uncurry (.=)) gauges ++- map (uncurry (.=)) labels---- | A list of string keys and JSON-encodable values. Used to render--- a list of key-value pairs as a JSON object.-newtype Assocs = Assocs [(T.Text, Json)]--instance A.ToJSON Assocs where- toJSON (Assocs xs) = A.object $ map (uncurry (.=)) xs---- | A group of either counters or gauges.-data Group = Group- ![(T.Text, Json)]- {-# UNPACK #-} !Double -- Milliseconds since epoch--instance A.ToJSON Group where- toJSON (Group xs t) =- A.object $ ("server_timestamp_millis" .= t) : map (uncurry (.=)) xs------------------------------------------------------------------------------ | Get a snapshot of all values. Note that we're not guaranteed to--- see a consistent snapshot of the whole map.-readAllRefs :: (Ref r t, A.ToJSON t) => IORef (M.HashMap T.Text r)- -> IO [(T.Text, Json)]-readAllRefs mapRef = do- m <- readIORef mapRef- forM (M.toList m) $ \ (name, ref) -> do- val <- read ref- return (name, Json val)-{-# INLINABLE readAllRefs #-}----- Existential wrapper used for OO-style polymorphism.-data Json = forall a. A.ToJSON a => Json a--instance A.ToJSON Json where- toJSON (Json x) = A.toJSON x---- | Partition GC statistics into counters and gauges.-partitionGcStats :: Stats.GCStats -> ([(T.Text, Json)], [(T.Text, Json)])-partitionGcStats s@(Stats.GCStats {..}) = (counters, gauges)- where- counters = [- ("bytes_allocated" , Json bytesAllocated)- , ("num_gcs" , Json numGcs)- , ("num_bytes_usage_samples" , Json numByteUsageSamples)- , ("cumulative_bytes_used" , Json cumulativeBytesUsed)- , ("bytes_copied" , Json bytesCopied)- , ("mutator_cpu_seconds" , Json mutatorCpuSeconds)- , ("mutator_wall_seconds" , Json mutatorWallSeconds)- , ("gc_cpu_seconds" , Json gcCpuSeconds)- , ("gc_wall_seconds" , Json gcWallSeconds)- , ("cpu_seconds" , Json cpuSeconds)- , ("wall_seconds" , Json wallSeconds)- ]- gauges = [- ("max_bytes_used" , Json maxBytesUsed)- , ("current_bytes_used" , Json currentBytesUsed)- , ("current_bytes_slop" , Json currentBytesSlop)- , ("max_bytes_slop" , Json maxBytesSlop)- , ("peak_megabytes_allocated" , Json peakMegabytesAllocated)- , ("par_tot_bytes_copied" , Json (gcParTotBytesCopied s))- , ("par_avg_bytes_copied" , Json (gcParTotBytesCopied s))- , ("par_max_bytes_copied" , Json parMaxBytesCopied)- ]------------------------------------------------------------------------------ | Serve a collection of counters or gauges, as a JSON object.-buildMany :: (Ref r t, A.ToJSON t) => IORef (M.HashMap T.Text r)- -> IO L.ByteString-buildMany mapRef = do- list <- readAllRefs mapRef- time <- getTimeMillis- return $ A.encode $ A.toJSON $ Group list time-{-# INLINABLE buildMany #-}---- | Serve all counter, gauges and labels, built-in or not, as a--- nested JSON object.-buildAll :: IORef Counters -> IORef Gauges -> IORef Labels -> IO L.ByteString-buildAll counters gauges labels = do- gcStats <- getGcStats- counterList <- readAllRefs counters- gaugeList <- readAllRefs gauges- labelList <- readAllRefs labels- time <- getTimeMillis- return $ A.encode $ A.toJSON $ Stats gcStats counterList gaugeList- labelList time--buildCombined :: IORef Counters -> IORef Gauges -> IORef Labels -> IO L.ByteString-buildCombined counters gauges labels = do- gcStats <- getGcStats- counterList <- readAllRefs counters- gaugeList <- readAllRefs gauges- labelList <- readAllRefs labels- time <- getTimeMillis- return $ A.encode $ A.toJSON $ Combined $- Stats gcStats counterList gaugeList labelList time--buildOne :: (Ref r t, Show t)- => IORef (M.HashMap T.Text r) -> T.Text- -> IO (Maybe S.ByteString)-buildOne refs name = do- m <- readIORef refs- case M.lookup name m of- Just counter -> do- val <- read counter- return $ Just $ S8.pack $ show val- Nothing ->- -- Try built-in (e.g. GC) refs- case Map.lookup name builtinCounters of- Just f -> do- gcStats <- liftIO getGcStats- return $ Just $ S8.pack $ f gcStats- Nothing -> return Nothing-{-# INLINABLE buildOne #-}--getGcStats :: IO Stats.GCStats-#if MIN_VERSION_base(4,6,0)-getGcStats = do- enabled <- Stats.getGCStatsEnabled- if enabled- then Stats.getGCStats- else return emptyGCStats--emptyGCStats :: Stats.GCStats-emptyGCStats = Stats.GCStats- { bytesAllocated = 0- , numGcs = 0- , maxBytesUsed = 0- , numByteUsageSamples = 0- , cumulativeBytesUsed = 0- , bytesCopied = 0- , currentBytesUsed = 0- , currentBytesSlop = 0- , maxBytesSlop = 0- , peakMegabytesAllocated = 0- , mutatorCpuSeconds = 0- , mutatorWallSeconds = 0- , gcCpuSeconds = 0- , gcWallSeconds = 0- , cpuSeconds = 0- , wallSeconds = 0- , parTotBytesCopied = 0- , parMaxBytesCopied = 0- }-#else-getGcStats = Stats.getGCStats-#endif---- | A list of all built-in (e.g. GC) counters, together with a--- pretty-printing function for each.-builtinCounters :: Map.Map T.Text (Stats.GCStats -> String)-builtinCounters = Map.fromList [- ("bytes_allocated" , show . Stats.bytesAllocated)- , ("num_gcs" , show . Stats.numGcs)- , ("max_bytes_used" , show . Stats.maxBytesUsed)- , ("num_bytes_usage_samples" , show . Stats.numByteUsageSamples)- , ("cumulative_bytes_used" , show . Stats.cumulativeBytesUsed)- , ("bytes_copied" , show . Stats.bytesCopied)- , ("current_bytes_used" , show . Stats.currentBytesUsed)- , ("current_bytes_slop" , show . Stats.currentBytesSlop)- , ("max_bytes_slop" , show . Stats.maxBytesSlop)- , ("peak_megabytes_allocated" , show . Stats.peakMegabytesAllocated)- , ("mutator_cpu_seconds" , show . Stats.mutatorCpuSeconds)- , ("mutator_wall_seconds" , show . Stats.mutatorWallSeconds)- , ("gc_cpu_seconds" , show . Stats.gcCpuSeconds)- , ("gc_wall_seconds" , show . Stats.gcWallSeconds)- , ("cpu_seconds" , show . Stats.cpuSeconds)- , ("wall_seconds" , show . Stats.wallSeconds)- , ("par_tot_bytes_copied" , show . gcParTotBytesCopied)- , ("par_avg_bytes_copied" , show . gcParTotBytesCopied)- , ("par_max_bytes_copied" , show . Stats.parMaxBytesCopied)- ]------------------------------------------------------------------------------ Utilities for working with accept headers---- | Parse the HTTP accept string to determine supported content types.-parseHttpAccept :: S.ByteString -> [S.ByteString]-parseHttpAccept = List.map fst- . List.sortBy (rcompare `on` snd)- . List.map grabQ- . S.split 44 -- comma- where- rcompare :: Double -> Double -> Ordering- rcompare = flip compare- grabQ s =- let (s', q) = breakDiscard 59 s -- semicolon- (_, q') = breakDiscard 61 q -- equals sign- in (trimWhite s', readQ $ trimWhite q')- readQ s = case reads $ S8.unpack s of- (x, _):_ -> x- _ -> 1.0- trimWhite = S.dropWhile (== 32) -- space--breakDiscard :: Word8 -> S.ByteString -> (S.ByteString, S.ByteString)-breakDiscard w s =- let (x, y) = S.break (== w) s- in (x, S.drop 1 y)----------------------------------------------------------------------------- Utilities for working with timestamps---- | Return the number of milliseconds since epoch.-getTimeMillis :: IO Double-getTimeMillis = (realToFrac . (* 1000)) `fmap` getPOSIXTime---- | Helper to work around rename in GHC.Stats in base-4.6.-gcParTotBytesCopied :: Stats.GCStats -> Int64-#if MIN_VERSION_base(4,6,0)-gcParTotBytesCopied = Stats.parTotBytesCopied-#else-gcParTotBytesCopied = Stats.parAvgBytesCopied-#endif
System/Remote/Counter.hs view
@@ -1,28 +1,18 @@-{-# LANGUAGE BangPatterns #-}+{-# OPTIONS_HADDOCK not-home #-}+ -- | This module defines a type for mutable, integer-valued counters. -- Counters are non-negative, monotonically increasing values and can -- be used to track e.g. the number of requests served since program -- start. All operations on counters are thread-safe.+--+-- N.B. This module exists to maintain backwards compatibility with+-- older versions of this library. New code should use the+-- @System.Metrics.Counter@ module from the ekg-core package instead. module System.Remote.Counter (- Counter- , inc- , add+ Counter.Counter+ , Counter.inc+ , Counter.add ) where -import Data.IORef (atomicModifyIORef)-import Prelude hiding (subtract)--import System.Remote.Counter.Internal---- | Increase the counter by one.-inc :: Counter -> IO ()-inc (C ref) = do- !_ <- atomicModifyIORef ref $ \ n -> let n' = n + 1 in (n', n')- return ()---- | Increase the counter by the given amount.-add :: Counter -> Int -> IO ()-add (C ref) i = do- !_ <- atomicModifyIORef ref $ \ n -> let n' = n + i in (n', n')- return ()+import qualified System.Metrics.Counter as Counter
− System/Remote/Counter/Internal.hs
@@ -1,20 +0,0 @@-{-# OPTIONS_HADDOCK not-home #-}-module System.Remote.Counter.Internal- (- Counter(..)- , new- , read- ) where--import Data.IORef (IORef, newIORef, readIORef)-import Prelude hiding (read)---- | A mutable, integer-valued counter.-newtype Counter = C { unC :: IORef Int }---- | Create a new, zero initialized, counter.-new :: IO Counter-new = C `fmap` newIORef 0--read :: Counter -> IO Int-read = readIORef . unC
System/Remote/Gauge.hs view
@@ -1,55 +1,21 @@-{-# LANGUAGE BangPatterns #-}+{-# OPTIONS_HADDOCK not-home #-}+ -- | This module defines a type for mutable, integer-valued gauges. -- Gauges are variable values and can be used to track e.g. the -- current number of concurrent connections. All operations on gauges -- are thread-safe.+--+-- N.B. This module exists to maintain backwards compatibility with+-- older versions of this library. New code should use the+-- @System.Metrics.Gauge@ module from the ekg-core package instead. module System.Remote.Gauge (- Gauge- , inc- , dec- , add- , subtract- , set- , modify+ Gauge.Gauge+ , Gauge.inc+ , Gauge.dec+ , Gauge.add+ , Gauge.subtract+ , Gauge.set ) where -import Data.IORef (atomicModifyIORef)-import Prelude hiding (subtract)--import System.Remote.Gauge.Internal---- | Increase the gauge by one.-inc :: Gauge -> IO ()-inc (C ref) = do- !_ <- atomicModifyIORef ref $ \ n -> let n' = n + 1 in (n', n')- return ()---- | Decrease the gauge by one.-dec :: Gauge -> IO ()-dec (C ref) = do- !_ <- atomicModifyIORef ref $ \ n -> let n' = n - 1 in (n', n')- return ()---- | Increase the gauge by the given amount.-add :: Gauge -> Int -> IO ()-add (C ref) i = do- !_ <- atomicModifyIORef ref $ \ n -> let n' = n + i in (n', n')- return ()---- | Decrease the gauge by the given amount.-subtract :: Gauge -> Int -> IO ()-subtract (C ref) i = do- !_ <- atomicModifyIORef ref $ \ n -> let n' = n - i in (n', n')- return ()---- | Set the gauge to the given value.-set :: Gauge -> Int -> IO ()-set (C ref) !i = atomicModifyIORef ref $ \ _ -> (i, ())---- | Set the gauge to the result of applying the given function to the--- value.-modify :: (Int -> Int) -> Gauge -> IO ()-modify f (C ref) = do- !_ <- atomicModifyIORef ref $ \ i -> let i' = f i in (i', i')- return ()+import qualified System.Metrics.Gauge as Gauge
− System/Remote/Gauge/Internal.hs
@@ -1,20 +0,0 @@-{-# OPTIONS_HADDOCK not-home #-}-module System.Remote.Gauge.Internal- (- Gauge(..)- , new- , read- ) where--import Data.IORef (IORef, newIORef, readIORef)-import Prelude hiding (read)---- | A mutable, integer-valued gauge.-newtype Gauge = C { unC :: IORef Int }---- | Create a new, zero initialized, gauge.-new :: IO Gauge-new = C `fmap` newIORef 0--read :: Gauge -> IO Int-read = readIORef . unC
+ System/Remote/Json.hs view
@@ -0,0 +1,108 @@+{-# LANGUAGE OverloadedStrings #-}++module System.Remote.Json+ (+ encodeAll+ , encodeOne+ ) where++import qualified Data.Aeson.Encode as A+import qualified Data.Aeson.Types as A+import qualified Data.ByteString.Lazy as L+import qualified Data.HashMap.Strict as M+import Data.Int (Int64)+import qualified Data.Text as T+import Prelude hiding (read)++import System.Metrics+import qualified System.Metrics.Distribution as Distribution++------------------------------------------------------------------------+-- * JSON serialization++data MetricType =+ CounterType+ | GaugeType+ | LabelType+ | DistributionType++metricType :: MetricType -> T.Text+metricType CounterType = "c"+metricType GaugeType = "g"+metricType LabelType = "l"+metricType DistributionType = "d"++-- | Encode metrics as nested JSON objects. Each "." in the metric+-- name introduces a new level of nesting. For example, the metrics+-- @[("foo.bar", 10), ("foo.baz", "label")]@ are encoded as+--+-- > {+-- > "foo": {+-- > "bar": {+-- > "type:", "c",+-- > "val": 10+-- > },+-- > "baz": {+-- > "type": "l",+-- > "val": "label"+-- > }+-- > }+-- > }+--+encodeAll :: Sample -> L.ByteString+encodeAll metrics =+ A.encode $ buildOne metrics $ A.emptyObject+ where+ buildOne :: M.HashMap T.Text Value -> A.Value -> A.Value+ buildOne m o = M.foldlWithKey' build o m++ build :: A.Value -> T.Text -> Value -> A.Value+ build m name val = go m (T.splitOn "." name) val++ go :: A.Value -> [T.Text] -> Value -> A.Value+ go (A.Object m) [str] val = A.Object $ M.insert str metric m+ where metric = buildOneM val+ go (A.Object m) (str:rest) val = case M.lookup str m of+ Nothing -> A.Object $ M.insert str (go A.emptyObject rest val) m+ Just m' -> A.Object $ M.insert str (go m' rest val) m+ go v _ _ = typeMismatch "Object" v++buildOneM :: Value -> A.Value+buildOneM (Counter n) = goOne n CounterType+buildOneM (Gauge n) = goOne n GaugeType+buildOneM (Label l) = goOne l LabelType+buildOneM (Distribution l) = goDistribution l++goDistribution :: Distribution.Stats -> A.Value+goDistribution stats = A.object [+ ("mean", A.toJSON $! Distribution.mean stats),+ ("variance", A.toJSON $! Distribution.variance stats),+ ("count", A.toJSON $! Distribution.count stats),+ ("sum", A.toJSON $! Distribution.sum stats),+ ("min", A.toJSON $! Distribution.min stats),+ ("max", A.toJSON $! Distribution.max stats),+ ("type", A.toJSON (metricType DistributionType))]++goOne :: A.ToJSON a => a -> MetricType -> A.Value+goOne val ty = A.object [+ ("val", A.toJSON val), ("type", A.toJSON (metricType ty))]+{-# SPECIALIZE goOne :: Int64 -> MetricType -> A.Value #-}+{-# SPECIALIZE goOne :: T.Text -> MetricType -> A.Value #-}++encodeOne :: Value -> L.ByteString+encodeOne = A.encode . buildOneM++typeMismatch :: String -- ^ The expected type+ -> A.Value -- ^ The actual value encountered+ -> a+typeMismatch expected actual =+ error $ "when expecting a " ++ expected ++ ", encountered " ++ name +++ " instead"+ where+ name = case actual of+ A.Object _ -> "Object"+ A.Array _ -> "Array"+ A.String _ -> "String"+ A.Number _ -> "Number"+ A.Bool _ -> "Boolean"+ A.Null -> "Null"
System/Remote/Label.hs view
@@ -1,27 +1,18 @@-{-# LANGUAGE BangPatterns #-}+{-# OPTIONS_HADDOCK not-home #-}+ -- | This module defines a type for mutable, string-valued labels. -- Labels are variable values and can be used to track e.g. the -- command line arguments or other free-form values. All operations on -- labels are thread-safe.+--+-- N.B. This module exists to maintain backwards compatibility with+-- older versions of this library. New code should use the+-- @System.Metrics.Label@ module from the ekg-core package instead. module System.Remote.Label (- Label- , set- , modify+ Label.Label+ , Label.set+ , Label.modify ) where -import Data.IORef (atomicModifyIORef)-import qualified Data.Text as T--import System.Remote.Label.Internal---- | Set the label to the given value.-set :: Label -> T.Text -> IO ()-set (C ref) !i = atomicModifyIORef ref $ \ _ -> (i, ())---- | Set the label to the result of applying the given function to the--- value.-modify :: (T.Text -> T.Text) -> Label -> IO ()-modify f (C ref) = do- !_ <- atomicModifyIORef ref $ \ i -> let i' = f i in (i', i')- return ()+import qualified System.Metrics.Label as Label
− System/Remote/Label/Internal.hs
@@ -1,21 +0,0 @@-{-# OPTIONS_HADDOCK not-home #-}-module System.Remote.Label.Internal- (- Label(..)- , new- , read- ) where--import Data.IORef (IORef, newIORef, readIORef)-import qualified Data.Text as T-import Prelude hiding (read)---- | A mutable, text-valued label.-newtype Label = C { unC :: IORef T.Text }---- | Create a new empty label.-new :: IO Label-new = C `fmap` newIORef T.empty--read :: Label -> IO T.Text-read = readIORef . unC
System/Remote/Monitoring.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-} -- | This module provides remote monitoring of a running process over -- HTTP. It can be used to run an HTTP server that provides both a@@ -31,30 +31,37 @@ -- * The monitoring server Server , serverThreadId+ , serverMetricStore , forkServer+ , forkServerWith - -- * User-defined counters, gauges, and labels+ -- * Defining metrics -- $userdefined , getCounter , getGauge , getLabel+ , getDistribution ) where import Control.Concurrent (ThreadId, forkIO) import qualified Data.ByteString as S-import qualified Data.HashMap.Strict as M-import Data.IORef (newIORef)+import Data.Int (Int64)+import qualified Data.Text as T+import Data.Time.Clock.POSIX (getPOSIXTime) import Prelude hiding (read) -import System.Remote.Common-+import qualified System.Metrics as Metrics+import qualified System.Metrics.Counter as Counter+import qualified System.Metrics.Distribution as Distribution+import qualified System.Metrics.Gauge as Gauge+import qualified System.Metrics.Label as Label import System.Remote.Snap -- $configuration ----- To use this module you must first enable GC statistics collection--- in the run-time system. To enable GC statistics collection, either--- run your program with+-- To make full use out of this this module you must first enable GC+-- statistics collection in the run-time system. To enable GC+-- statistics collection, either run your program with -- -- > +RTS -T --@@ -73,118 +80,79 @@ -- $api -- To use the machine-readable REST API, send an HTTP GET request to--- the host and port passed to 'forkServer'. The following resources--- (i.e. URLs) are available:------ [\/] JSON object containing all counters, gauges and labels.--- Counters, gauges, and labels are stored as nested objects under the--- @counters@, @gauges@, and @labels@ attributes, respectively.--- Content types: \"text\/html\" (default), \"application\/json\"------ [\/combined] Flattened JSON object containing all counters, gauges,--- and labels. Content types: \"application\/json\"------ [\/counters] JSON object containing all counters. Content types:--- \"application\/json\"------ [\/counters/\<counter name\>] Value of a single counter, as a--- string. The name should be UTF-8 encoded. Content types:--- \"text\/plain\"+-- the host and port passed to 'forkServer'. ----- [\/gauges] JSON object containing all gauges. Content types:--- \"application\/json\"+-- The API is versioned to allow for API evolution. This document is+-- for version 1. To ensure you're using this version, append @?v=1@+-- to your resource URLs. Omitting the version number will give you+-- the latest version of the API. ----- [\/gauges/\<gauge name\>] Value of a single gauge, as a string.--- The name should be UTF-8 encoded. Content types: \"text\/plain\"+-- The following resources (i.e. URLs) are available: ----- [\/labels] JSON object containing all labels. Content types:+-- [\/] JSON object containing all metrics. Metrics are stored as+-- nested objects, with one new object layer per \".\" in the metric+-- name (see example below.) Content types: \"text\/html\" (default), -- \"application\/json\" ----- [\/labels/\<label name\>] Value of a single label, as a string.--- The name should be UTF-8 encoded. Content types: \"text\/plain\"------ Counters, gauges and labels are stored as attributes of the--- returned JSON objects, one attribute per counter, gauge or label.--- In addition to user-defined counters, gauges, and labels, the below--- built-in counters and gauges are also returned. Furthermore, the--- top-level JSON object of any resource contains the--- @server_timestamp_millis@ attribute, which indicates the server--- time, in milliseconds, when the sample was taken.------ Built-in counters:------ [@bytes_allocated@] Total number of bytes allocated------ [@num_gcs@] Number of garbage collections performed------ [@num_bytes_usage_samples@] Number of byte usage samples taken------ [@cumulative_bytes_used@] Sum of all byte usage samples, can be--- used with @numByteUsageSamples@ to calculate averages with--- arbitrary weighting (if you are sampling this record multiple--- times).------ [@bytes_copied@] Number of bytes copied during GC------ [@mutator_cpu_seconds@] CPU time spent running mutator threads.--- This does not include any profiling overhead or initialization.------ [@mutator_wall_seconds@] Wall clock time spent running mutator--- threads. This does not include initialization.------ [@gc_cpu_seconds@] CPU time spent running GC------ [@gc_wall_seconds@] Wall clock time spent running GC------ [@cpu_seconds@] Total CPU time elapsed since program start+-- [\/\<namespace\>/\<metric\>] JSON object for a single metric. The+-- metric name is created by converting all \"\/\" to \".\". Example:+-- \"\/foo\/bar\" corresponds to the metric \"foo.bar\". Content+-- types: \"application\/json\" ----- [@wall_seconds@] Total wall clock time elapsed since start+-- Each metric is returned as an object containing a @type@ field. Available types+-- are: ----- Built-in gauges:+-- * \"c\" - 'Counter.Counter' ----- [@max_bytes_used@] Maximum number of live bytes seen so far+-- * \"g\" - 'Gauge.Gauge' ----- [@current_bytes_used@] Current number of live bytes+-- * \"l\" - 'Label.Label' ----- [@current_bytes_slop@] Current number of bytes lost to slop+-- * \"d\" - 'Distribution.Distribution' ----- [@max_bytes_slop@] Maximum number of bytes lost to slop at any one time so far+-- In addition to the @type@ field, there are metric specific fields: ----- [@peak_megabytes_allocated@] Maximum number of megabytes allocated+-- * Counters, gauges, and labels: the @val@ field contains the+-- actual value (i.e. an integer or a string). ----- [@par_tot_bytes_copied@] Number of bytes copied during GC, minus--- space held by mutable lists held by the capabilities. Can be used--- with 'parMaxBytesCopied' to determine how well parallel GC utilized--- all cores.+-- * Distributions: the @mean@, @variance@, @count@, @sum@, @min@,+-- and @max@ fields contain their statistical equivalents. ----- [@par_avg_bytes_copied@] Deprecated alias for--- @par_tot_bytes_copied@.+-- Example of a response containing the metrics \"myapp.visitors\" and+-- \"myapp.args\": ----- [@par_max_bytes_copied@] Sum of number of bytes copied each GC by--- the most active GC thread each GC. The ratio of--- @par_tot_bytes_copied@ divided by @par_max_bytes_copied@ approaches--- 1 for a maximally sequential run and approaches the number of--- threads (set by the RTS flag @-N@) for a maximally parallel run.+-- > {+-- > "myapp": {+-- > "visitors": {+-- > "val": 10,+-- > "type": "c"+-- > },+-- > "args": {+-- > "val": "--a-flag",+-- > "type": "l"+-- > }+-- > }+-- > } -- $userdefined--- The monitoring server can store and serve user-defined,--- integer-valued counters and gauges, and string-valued labels. A+-- The monitoring server can store and serve integer-valued counters+-- and gauges, string-valued labels, and statistical distributions. A -- counter is a monotonically increasing value (e.g. TCP connections--- established since program start.) A gauge is a variable value--- (e.g. the current number of concurrent connections.) A label is a+-- established since program start.) A gauge is a variable value (e.g.+-- the current number of concurrent connections.) A label is a -- free-form string value (e.g. exporting the command line arguments--- or host name.) Each counter, gauge, and label is associated with a--- name, which is used when it is displayed in the UI or returned in a--- JSON object.+-- or host name.) A distribution is a statistic summary of events+-- (e.g. processing time per request.) Each metric is associated with+-- a name, which is used when it is displayed in the UI or returned in+-- a JSON object. ----- Even though it's technically possible to have a counter and a gauge--- with the same name, associated with the same server, it's not--- recommended as it might make it harder for clients to distinguish--- the two.+-- Metrics share the same namespace so it's not possible to create+-- e.g. a counter and a gauge with the same. Attempting to do so will+-- result in an 'error'. -- -- To create and use a counter, simply call 'getCounter' to create it--- and then call e.g. 'System.Remote.Counter.inc' or--- 'System.Remote.Counter.add' to modify its value. Example:+-- and then call e.g. 'Counter.inc' or 'Counter.add' to modify its+-- value. Example: -- -- > main = do -- > handle <- forkServer "localhost" 8000@@ -195,31 +163,104 @@ -- > loop -- -- To create a gauge, use 'getGauge' instead of 'getCounter' and then--- call e.g. 'System.Remote.Gauge.set' or--- 'System.Remote.Gauge.modify'. Similar for labels.+-- call e.g. 'System.Remote.Gauge.set'. Similar for the other metric+-- types.+--+-- It's also possible to register metrics directly using the+-- @System.Metrics@ module in the ekg-core package. This gives you a+-- bit more control over how metric values are retrieved. ------------------------------------------------------------------------ -- * The monitoring server --- | The thread ID of the server. You can kill the server by killing--- this thread (i.e. by throwing it an asynchronous exception.)-serverThreadId :: Server -> ThreadId-serverThreadId = threadId+-- | A handle that can be used to control the monitoring server.+-- Created by 'forkServer'.+data Server = Server {+ -- | The thread ID of the server. You can kill the server by+ -- killing this thread (i.e. by throwing it an asynchronous+ -- exception.)+ serverThreadId :: {-# UNPACK #-} !ThreadId + -- | The metric store associated with the server. If you want to+ -- add metric to the default store created by 'forkServer' you+ -- need to use this function to retrieve it.+ , serverMetricStore :: {-# UNPACK #-} !Metrics.Store+ }++-- | Like 'forkServerWith', but creates a default metric store with+-- some predefined metrics. The predefined metrics are those given in+-- 'System.Metrics.registerGcMetrics'.+forkServer :: S.ByteString -- ^ Host to listen on (e.g. \"localhost\")+ -> Int -- ^ Port to listen on (e.g. 8000)+ -> IO Server+forkServer host port = do+ store <- Metrics.newStore+ forkServerWith store host port+ -- | Start an HTTP server in a new thread. The server replies to GET -- requests to the given host and port. The host argument can be -- either a numeric network address (dotted quad for IPv4, -- colon-separated hex for IPv6) or a hostname (e.g. \"localhost\".) -- The client can control the Content-Type used in responses by -- setting the Accept header. At the moment three content types are--- available: \"application\/json\", \"text\/html\", and--- \"text\/plain\".-forkServer :: S.ByteString -- ^ Host to listen on (e.g. \"localhost\")- -> Int -- ^ Port to listen on (e.g. 8000)- -> IO Server-forkServer host port = do- counters <- newIORef M.empty- gauges <- newIORef M.empty- labels <- newIORef M.empty- tid <- forkIO $ startServer counters gauges labels host port- return $! Server tid counters gauges labels+-- available: \"application\/json\" and \"text\/html\".+--+-- Registers the following counter, used by the UI:+--+-- [@ekg.server_time_ms@] The server time when the sample was taken,+-- in milliseconds.+--+-- Note that this function, unlike 'forkServer', doesn't register any+-- other predefined metrics. This allows other libraries to create and+-- provide a metric store for use with this library. If the metric+-- store isn't created by you and the creator doesn't register the+-- metrics registered by 'forkServer', you might want to register them+-- yourself.+forkServerWith :: Metrics.Store -- ^ Metric store+ -> S.ByteString -- ^ Host to listen on (e.g. \"localhost\")+ -> Int -- ^ Port to listen on (e.g. 8000)+ -> IO Server+forkServerWith store host port = do+ Metrics.registerGcMetrics store+ Metrics.registerCounter "ekg.server_timestamp_ms" getTimeMs store+ tid <- forkIO $ startServer store host port+ return $! Server tid store+ where+ getTimeMs :: IO Int64+ getTimeMs = (round . (* 1000)) `fmap` getPOSIXTime++------------------------------------------------------------------------+-- * Defining metrics++-- | Return a new, zero-initialized counter associated with the given+-- name and server. Multiple calls to 'getCounter' with the same+-- arguments will result in an 'error'.+getCounter :: T.Text -- ^ Counter name+ -> Server -- ^ Server that will serve the counter+ -> IO Counter.Counter+getCounter name server = Metrics.createCounter name (serverMetricStore server)++-- | Return a new, zero-initialized gauge associated with the given+-- name and server. Multiple calls to 'getGauge' with the same+-- arguments will result in an 'error'.+getGauge :: T.Text -- ^ Gauge name+ -> Server -- ^ Server that will serve the gauge+ -> IO Gauge.Gauge+getGauge name server = Metrics.createGauge name (serverMetricStore server)++-- | Return a new, empty label associated with the given name and+-- server. Multiple calls to 'getLabel' with the same arguments will+-- result in an 'error'.+getLabel :: T.Text -- ^ Label name+ -> Server -- ^ Server that will serve the label+ -> IO Label.Label+getLabel name server = Metrics.createLabel name (serverMetricStore server)++-- | Return a new distribution associated with the given name and+-- server. Multiple calls to 'getDistribution' with the same arguments+-- will result in an 'error'.+getDistribution :: T.Text -- ^ Distribution name+ -> Server -- ^ Server that will serve the distribution+ -> IO Distribution.Distribution+getDistribution name server =+ Metrics.createDistribution name (serverMetricStore server)
System/Remote/Snap.hs view
@@ -6,32 +6,29 @@ import Control.Applicative ((<$>), (<|>)) import Control.Exception (throwIO)-import Control.Monad (join, unless) import Control.Monad.IO.Class (liftIO)-import qualified Data.Aeson.Types as A import qualified Data.ByteString as S import qualified Data.ByteString.Char8 as S8+import Data.Function (on) import qualified Data.HashMap.Strict as M-import Data.IORef (IORef) import qualified Data.List as List-import qualified Data.Map as Map-import Data.Maybe (listToMaybe)-import qualified Data.Text as T import qualified Data.Text.Encoding as T+import Data.Word (Word8) import Network.Socket (NameInfoFlag(NI_NUMERICHOST), addrAddress, getAddrInfo, getNameInfo) import Paths_ekg (getDataDir) import Prelude hiding (read) import Snap.Core (MonadSnap, Request, Snap, finishWith, getHeaders, getRequest,- getResponse, method, Method(GET), modifyResponse, pass, route,- rqParams, rqPathInfo, setContentType, setResponseStatus,- writeBS, writeLBS)+ getResponse, method, Method(GET), modifyResponse, pass,+ rqPathInfo, setContentType, setResponseStatus,+ writeLBS) import Snap.Http.Server (httpServe) import qualified Snap.Http.Server.Config as Config import Snap.Util.FileServe (serveDirectory) import System.FilePath ((</>)) -import System.Remote.Common+import System.Metrics+import System.Remote.Json ------------------------------------------------------------------------ @@ -51,11 +48,11 @@ unsupportedAddressError = throwIO $ userError $ "unsupported address: " ++ S8.unpack host -startServer :: IORef Counters -> IORef Gauges -> IORef Labels+startServer :: Store -> S.ByteString -- ^ Host to listen on (e.g. \"localhost\") -> Int -- ^ Port to listen on (e.g. 8000) -> IO ()-startServer counters gauges labels host port = do+startServer store host port = do -- Snap doesn't allow for non-numeric host names in -- 'Snap.setBind'. We work around that limitation by converting a -- possible non-numeric host name to a numeric address.@@ -67,31 +64,17 @@ Config.setHostname host $ Config.setBind numericHost $ Config.defaultConfig- httpServe conf (monitor counters gauges labels)+ httpServe conf (monitor store) -- | A handler that can be installed into an existing Snap application.-monitor :: IORef Counters -> IORef Gauges -> IORef Labels -> Snap ()-monitor counters gauges labels = do+monitor :: Store -> Snap ()+monitor store = do dataDir <- liftIO getDataDir- route [- ("", method GET (format "application/json"- (serveAll counters gauges labels)))- , ("combined", method GET (format "application/json"- (serveCombined counters gauges labels)))- , ("counters", method GET (format "application/json"- (serveMany counters)))- , ("counters/:name", method GET (format "text/plain"- (serveOne counters)))- , ("gauges", method GET (format "application/json"- (serveMany gauges)))- , ("gauges/:name", method GET (format "text/plain"- (serveOne gauges)))- , ("labels", method GET (format "application/json"- (serveMany labels)))- , ("labels/:name", method GET (format "text/plain"- (serveOne labels)))- ]+ (jsonHandler $ serve store) <|> serveDirectory (dataDir </> "assets")+ where+ jsonHandler = wrapHandler "application/json"+ wrapHandler fmt handler = method GET $ format fmt $ handler -- | The Accept header of the request. acceptHeader :: Request -> Maybe S.ByteString@@ -107,50 +90,55 @@ Just hdr | hdr == fmt -> action _ -> pass --- | Serve a collection of counters or gauges, as a JSON object.-serveMany :: (Ref r t, A.ToJSON t) => IORef (M.HashMap T.Text r) -> Snap ()-serveMany mapRef = do- modifyResponse $ setContentType "application/json"- bs <- liftIO $ buildMany mapRef- writeLBS bs-{-# INLINABLE serveMany #-}- -- | Serve all counter, gauges and labels, built-in or not, as a -- nested JSON object.-serveAll :: IORef Counters -> IORef Gauges -> IORef Labels -> Snap ()-serveAll counters gauges labels = do+serve :: MonadSnap m => Store -> m ()+serve store = do req <- getRequest- -- Workaround: Snap still matches requests to /foo to this handler- -- if the Accept header is "application/json", even though such- -- requests ought to go to the 'serveOne' handler.- unless (S.null $ rqPathInfo req) pass modifyResponse $ setContentType "application/json"- bs <- liftIO $ buildAll counters gauges labels- writeLBS bs+ if S.null (rqPathInfo req)+ then serveAll+ else serveOne (rqPathInfo req)+ where+ serveAll = do+ metrics <- liftIO $ sampleAll store+ writeLBS $ encodeAll metrics+ serveOne pathInfo = do+ let segments = S8.split '/' pathInfo+ nameBytes = S8.intercalate "." segments+ case T.decodeUtf8' nameBytes of+ Left _ -> do+ modifyResponse $ setResponseStatus 400 "Bad Request"+ r <- getResponse+ finishWith r+ Right name -> do+ metrics <- liftIO $ sampleAll store+ case M.lookup name metrics of+ Nothing -> pass+ Just metric -> writeLBS $ encodeOne metric --- | Serve all counters and gauges, built-in or not, as a flattened--- JSON object.-serveCombined :: IORef Counters -> IORef Gauges -> IORef Labels -> Snap ()-serveCombined counters gauges labels = do- modifyResponse $ setContentType "application/json"- bs <- liftIO $ buildCombined counters gauges labels- writeLBS bs+------------------------------------------------------------------------+-- Utilities for working with accept headers --- | Serve a single counter, as plain text.-serveOne :: (Ref r t, Show t) => IORef (M.HashMap T.Text r) -> Snap ()-serveOne refs = do- modifyResponse $ setContentType "text/plain"- req <- getRequest- let mname = T.decodeUtf8 <$> join- (listToMaybe <$> Map.lookup "name" (rqParams req))- case mname of- Nothing -> pass- Just name -> do- mbs <- liftIO $ buildOne refs name- case mbs of- Just bs -> writeBS bs- Nothing -> do- modifyResponse $ setResponseStatus 404 "Not Found"- r <- getResponse- finishWith r-{-# INLINABLE serveOne #-}+-- | Parse the HTTP accept string to determine supported content types.+parseHttpAccept :: S.ByteString -> [S.ByteString]+parseHttpAccept = List.map fst+ . List.sortBy (rcompare `on` snd)+ . List.map grabQ+ . S.split 44 -- comma+ where+ rcompare :: Double -> Double -> Ordering+ rcompare = flip compare+ grabQ s =+ let (s', q) = breakDiscard 59 s -- semicolon+ (_, q') = breakDiscard 61 q -- equals sign+ in (trimWhite s', readQ $ trimWhite q')+ readQ s = case reads $ S8.unpack s of+ (x, _):_ -> x+ _ -> 1.0+ trimWhite = S.dropWhile (== 32) -- space++breakDiscard :: Word8 -> S.ByteString -> (S.ByteString, S.ByteString)+breakDiscard w s =+ let (x, y) = S.break (== w) s+ in (x, S.drop 1 y)
assets/bootstrap-1.4.0.min.css view
@@ -59,22 +59,22 @@ .span22{width:1300px;} .span23{width:1360px;} .span24{width:1420px;}-.row>.offset1{margin-left:80px;}-.row>.offset2{margin-left:140px;}-.row>.offset3{margin-left:200px;}-.row>.offset4{margin-left:260px;}-.row>.offset5{margin-left:320px;}-.row>.offset6{margin-left:380px;}-.row>.offset7{margin-left:440px;}-.row>.offset8{margin-left:500px;}-.row>.offset9{margin-left:560px;}-.row>.offset10{margin-left:620px;}-.row>.offset11{margin-left:680px;}-.row>.offset12{margin-left:740px;}+.row >.offset1{margin-left:80px;}+.row >.offset2{margin-left:140px;}+.row >.offset3{margin-left:200px;}+.row >.offset4{margin-left:260px;}+.row >.offset5{margin-left:320px;}+.row >.offset6{margin-left:380px;}+.row >.offset7{margin-left:440px;}+.row >.offset8{margin-left:500px;}+.row >.offset9{margin-left:560px;}+.row >.offset10{margin-left:620px;}+.row >.offset11{margin-left:680px;}+.row >.offset12{margin-left:740px;} .span-one-third{width:300px;} .span-two-thirds{width:620px;}-.row>.offset-one-third{margin-left:340px;}-.row>.offset-two-thirds{margin-left:660px;}+.offset-one-third{margin-left:340px;}+.offset-two-thirds{margin-left:660px;} p{font-size:13px;font-weight:normal;line-height:18px;margin-bottom:9px;}p small{font-size:11px;color:#bfbfbf;} h1,h2,h3,h4,h5,h6{font-weight:bold;color:#404040;}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{color:#bfbfbf;} h1{margin-bottom:18px;font-size:30px;line-height:36px;}h1 small{font-size:18px;}@@ -122,7 +122,7 @@ .uneditable-input{background-color:#ffffff;display:block;border-color:#eee;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);cursor:not-allowed;} :-moz-placeholder{color:#bfbfbf;} ::-webkit-input-placeholder{color:#bfbfbf;}-input,textarea{-webkit-transition:border linear 0.2s,box-shadow linear 0.2s;-moz-transition:border linear 0.2s,box-shadow linear 0.2s;-ms-transition:border linear 0.2s,box-shadow linear 0.2s;-o-transition:border linear 0.2s,box-shadow linear 0.2s;transition:border linear 0.2s,box-shadow linear 0.2s;-webkit-box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1);}+input,textarea{-webkit-transform-style:preserve-3d;-webkit-transition:border linear 0.2s,box-shadow linear 0.2s;-moz-transition:border linear 0.2s,box-shadow linear 0.2s;-ms-transition:border linear 0.2s,box-shadow linear 0.2s;-o-transition:border linear 0.2s,box-shadow linear 0.2s;transition:border linear 0.2s,box-shadow linear 0.2s;-webkit-box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1);} input:focus,textarea:focus{outline:0;border-color:rgba(82, 168, 236, 0.8);-webkit-box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1),0 0 8px rgba(82, 168, 236, 0.6);-moz-box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1),0 0 8px rgba(82, 168, 236, 0.6);box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1),0 0 8px rgba(82, 168, 236, 0.6);} input[type=file]:focus,input[type=checkbox]:focus,select:focus{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;outline:1px dotted #666;} form .clearfix.error>label,form .clearfix.error .help-block,form .clearfix.error .help-inline{color:#b94a48;}@@ -235,7 +235,7 @@ .topbar p{margin:0;line-height:40px;}.topbar p a:hover{background-color:transparent;color:#ffffff;} .topbar form{float:left;margin:5px 0 0 0;position:relative;filter:alpha(opacity=100);-khtml-opacity:1;-moz-opacity:1;opacity:1;} .topbar form.pull-right{float:right;}-.topbar input{background-color:#444;background-color:rgba(255, 255, 255, 0.3);font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:normal;font-weight:13px;line-height:1;padding:4px 9px;color:#ffffff;color:rgba(255, 255, 255, 0.75);border:1px solid #111;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.25);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.25);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.25);-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none;}.topbar input:-moz-placeholder{color:#e6e6e6;}+.topbar input{background-color:#444;background-color:rgba(255, 255, 255, 0.3);font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:normal;font-weight:13px;line-height:1;padding:4px 9px;color:#ffffff;color:rgba(255, 255, 255, 0.75);border:1px solid #111;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.25);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.25);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.25);-webkit-transform-style:preserve-3d;-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none;}.topbar input:-moz-placeholder{color:#e6e6e6;} .topbar input::-webkit-input-placeholder{color:#e6e6e6;} .topbar input:hover{background-color:#bfbfbf;background-color:rgba(255, 255, 255, 0.5);color:#ffffff;} .topbar input:focus,.topbar input.focused{outline:0;background-color:#ffffff;color:#404040;text-shadow:0 1px 0 #ffffff;border:0;padding:5px 10px;-webkit-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);-moz-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);box-shadow:0 0 3px rgba(0, 0, 0, 0.15);}@@ -269,7 +269,7 @@ .pills a{margin:5px 3px 5px 0;padding:0 15px;line-height:30px;text-shadow:0 1px 1px #ffffff;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;}.pills a:hover{color:#ffffff;text-decoration:none;text-shadow:0 1px 1px rgba(0, 0, 0, 0.25);background-color:#00438a;} .pills .active a{color:#ffffff;text-shadow:0 1px 1px rgba(0, 0, 0, 0.25);background-color:#0069d6;} .pills-vertical>li{float:none;}-.tab-content>.tab-pane,.pill-content>.pill-pane,.tab-content>div,.pill-content>div{display:none;}+.tab-content>.tab-pane,.pill-content>.pill-pane{display:none;} .tab-content>.active,.pill-content>.active{display:block;} .breadcrumb{padding:7px 14px;margin:0 0 18px;background-color:#f5f5f5;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#ffffff), to(#f5f5f5));background-image:-moz-linear-gradient(top, #ffffff, #f5f5f5);background-image:-ms-linear-gradient(top, #ffffff, #f5f5f5);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, #f5f5f5));background-image:-webkit-linear-gradient(top, #ffffff, #f5f5f5);background-image:-o-linear-gradient(top, #ffffff, #f5f5f5);background-image:linear-gradient(top, #ffffff, #f5f5f5);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0);border:1px solid #ddd;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;}.breadcrumb li{display:inline;text-shadow:0 1px 0 #ffffff;} .breadcrumb .divider{padding:0 5px;color:#bfbfbf;}@@ -283,10 +283,10 @@ .btn.danger,.alert-message.danger,.btn.error,.alert-message.error{background-color:#c43c35;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35));background-image:-moz-linear-gradient(top, #ee5f5b, #c43c35);background-image:-ms-linear-gradient(top, #ee5f5b, #c43c35);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35));background-image:-webkit-linear-gradient(top, #ee5f5b, #c43c35);background-image:-o-linear-gradient(top, #ee5f5b, #c43c35);background-image:linear-gradient(top, #ee5f5b, #c43c35);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);border-color:#c43c35 #c43c35 #882a25;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);} .btn.success,.alert-message.success{background-color:#57a957;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957));background-image:-moz-linear-gradient(top, #62c462, #57a957);background-image:-ms-linear-gradient(top, #62c462, #57a957);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957));background-image:-webkit-linear-gradient(top, #62c462, #57a957);background-image:-o-linear-gradient(top, #62c462, #57a957);background-image:linear-gradient(top, #62c462, #57a957);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);border-color:#57a957 #57a957 #3d773d;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);} .btn.info,.alert-message.info{background-color:#339bb9;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#5bc0de), to(#339bb9));background-image:-moz-linear-gradient(top, #5bc0de, #339bb9);background-image:-ms-linear-gradient(top, #5bc0de, #339bb9);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), color-stop(100%, #339bb9));background-image:-webkit-linear-gradient(top, #5bc0de, #339bb9);background-image:-o-linear-gradient(top, #5bc0de, #339bb9);background-image:linear-gradient(top, #5bc0de, #339bb9);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);border-color:#339bb9 #339bb9 #22697d;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);}-.btn{cursor:pointer;display:inline-block;background-color:#e6e6e6;background-repeat:no-repeat;background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:-moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6);background-image:-ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:-o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);padding:5px 14px 6px;text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);color:#333;font-size:13px;line-height:normal;border:1px solid #ccc;border-bottom-color:#bbb;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-webkit-transition:0.1s linear all;-moz-transition:0.1s linear all;-ms-transition:0.1s linear all;-o-transition:0.1s linear all;transition:0.1s linear all;}.btn:hover{background-position:0 -15px;color:#333;text-decoration:none;}+.btn{cursor:pointer;display:inline-block;background-color:#e6e6e6;background-repeat:no-repeat;background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:-moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6);background-image:-ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:-o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);padding:5px 14px 6px;text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);color:#333;font-size:13px;line-height:normal;border:1px solid #ccc;border-bottom-color:#bbb;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-webkit-transform-style:preserve-3d;-webkit-transition:0.1s linear all;-moz-transition:0.1s linear all;-ms-transition:0.1s linear all;-o-transition:0.1s linear all;transition:0.1s linear all;}.btn:hover{background-position:0 -15px;color:#333;text-decoration:none;} .btn:focus{outline:1px dotted #666;} .btn.primary{color:#ffffff;background-color:#0064cd;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd));background-image:-moz-linear-gradient(top, #049cdb, #0064cd);background-image:-ms-linear-gradient(top, #049cdb, #0064cd);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd));background-image:-webkit-linear-gradient(top, #049cdb, #0064cd);background-image:-o-linear-gradient(top, #049cdb, #0064cd);background-image:linear-gradient(top, #049cdb, #0064cd);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#049cdb', endColorstr='#0064cd', GradientType=0);text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);border-color:#0064cd #0064cd #003f81;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);}-.btn.active,.btn:active{-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.25),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.25),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.25),0 1px 2px rgba(0, 0, 0, 0.05);}+.btn.active,.btn :active{-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.25),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.25),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.25),0 1px 2px rgba(0, 0, 0, 0.05);} .btn.disabled{cursor:default;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=65);-khtml-opacity:0.65;-moz-opacity:0.65;opacity:0.65;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} .btn[disabled]{cursor:default;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=65);-khtml-opacity:0.65;-moz-opacity:0.65;opacity:0.65;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} .btn.large{font-size:15px;line-height:normal;padding:9px 14px 9px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}@@ -320,7 +320,7 @@ .modal-backdrop{background-color:#000000;position:fixed;top:0;left:0;right:0;bottom:0;z-index:10000;}.modal-backdrop.fade{opacity:0;} .modal-backdrop,.modal-backdrop.fade.in{filter:alpha(opacity=80);-khtml-opacity:0.8;-moz-opacity:0.8;opacity:0.8;} .modal{position:fixed;top:50%;left:50%;z-index:11000;width:560px;margin:-250px 0 0 -280px;background-color:#ffffff;border:1px solid #999;border:1px solid rgba(0, 0, 0, 0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.modal .close{margin-top:7px;}-.modal.fade{-webkit-transition:opacity .3s linear, top .3s ease-out;-moz-transition:opacity .3s linear, top .3s ease-out;-ms-transition:opacity .3s linear, top .3s ease-out;-o-transition:opacity .3s linear, top .3s ease-out;transition:opacity .3s linear, top .3s ease-out;top:-25%;}+.modal.fade{-webkit-transform-style:preserve-3d;-webkit-transition:opacity .3s linear, top .3s ease-out;-moz-transition:opacity .3s linear, top .3s ease-out;-ms-transition:opacity .3s linear, top .3s ease-out;-o-transition:opacity .3s linear, top .3s ease-out;transition:opacity .3s linear, top .3s ease-out;top:-25%;} .modal.fade.in{top:50%;} .modal-header{border-bottom:1px solid #eee;padding:5px 15px;} .modal-body{padding:15px;}@@ -344,7 +344,7 @@ .popover .inner{background:#000000;background:rgba(0, 0, 0, 0.8);padding:3px;overflow:hidden;width:280px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);} .popover .title{background-color:#f5f5f5;padding:9px 15px;line-height:1;-webkit-border-radius:3px 3px 0 0;-moz-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0;border-bottom:1px solid #eee;} .popover .content{background-color:#ffffff;padding:14px;-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.popover .content p,.popover .content ul,.popover .content ol{margin-bottom:0;}-.fade{-webkit-transition:opacity 0.15s linear;-moz-transition:opacity 0.15s linear;-ms-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear;opacity:0;}.fade.in{opacity:1;}+.fade{-webkit-transform-style:preserve-3d;-webkit-transition:opacity 0.15s linear;-moz-transition:opacity 0.15s linear;-ms-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear;opacity:0;}.fade.in{opacity:1;} .label{padding:1px 3px 2px;font-size:9.75px;font-weight:bold;color:#ffffff;text-transform:uppercase;white-space:nowrap;background-color:#bfbfbf;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}.label.important{background-color:#c43c35;} .label.warning{background-color:#f89406;} .label.success{background-color:#46a546;}
assets/index.html view
@@ -95,32 +95,8 @@ </tbody> </table> - <h3>Counters</h3>- <table id="counter-table" class="condensed-table">- <thead>- <tr>- <th class="span4">Name</th>- <th class="span1">Value</th>- </tr>- </thead>- <tbody>- </tbody>- </table>-- <h3>Gauges</h3>- <table id="gauge-table" class="condensed-table">- <thead>- <tr>- <th class="span4">Name</th>- <th class="span1">Value</th>- </tr>- </thead>- <tbody>- </tbody>- </table>-- <h3>Labels</h3>- <table id="label-table" class="condensed-table">+ <h3>Metrics</h3>+ <table id="metric-table" class="condensed-table"> <thead> <tr> <th class="span4">Name</th>
assets/monitor.js view
@@ -115,7 +115,7 @@ } alertVisible = false; for (var i = 0; i < listeners.length; i++) {- listeners[i](stats, stats.server_timestamp_millis);+ listeners[i](stats, stats.ekg.server_timestamp_ms.val); } } @@ -160,7 +160,7 @@ prev_stats, prev_time)]); } - // zip lengends with data+ // zip legends with data var res = []; for(var i = 0; i < series.length; i++) res.push({ label: series[i].label, data: data[i] });@@ -195,8 +195,10 @@ return graph_fn(key, stats, time, prev_stats, prev_time); } + // jQuery has problem with IDs containing dots.+ var plotId = key.replace(/\./g, "-") + "-plot"; $("#plots:last").append(- '<div id="' + key + '-plot" class="plot-container">' ++ '<div id="' + plotId + '" class="plot-container">' + '<img src="cross.png" class="close-button"><h3>' + key + '</h3><div class="plot"></div></div>'); var plot = $("#plots > .plot-container:last > div");@@ -204,7 +206,7 @@ [{ label: label_fn(key), fn: getStats }], { yaxis: { tickFormatter: suffixFormatterGeneric } }); - var plotContainer = $("#" + key + "-plot");+ var plotContainer = $("#" + plotId); var closeButton = plotContainer.find("img"); closeButton.hide(); closeButton.click(function () {@@ -223,101 +225,146 @@ ); } - function addDynamicCounters(table, group_fn, graph_fn, label_fn) {- var counters = {};- function onDataReceived(stats, time) {- $.each(group_fn(stats), function (key, value) {- var elem;- if (key in counters) {- elem = counters[key];- } else {- // Add UI element- table.find("tbody:last").append(- '<tr><td>' + key +- ' <img src="chart_line_add.png" class="graph-button"' +- ' width="16" height="16"' +- ' alt="Add graph" title="Add graph"></td>' +- '<td class="value">N/A</td></tr>');- elem = table.find("tbody > tr > td:last");- counters[key] = elem;+ function addMetrics(table) {+ var COUNTER = "c";+ var GAUGE = "g";+ var DISTRIBUTION = "d";+ var metrics = {}; - var button = table.find("tbody > tr:last > td:first > img");- button.click(function () {- addDynamicPlot(key, button, graph_fn, label_fn);- $(this).hide();+ function makeDataGetter(key) {+ var pieces = key.split(".");+ function get(key, stats, time, prev_stats, prev_time) {+ var value = stats;+ $.each(pieces, function(unused_index, piece) {+ value = value[piece];+ });+ if (value.type === COUNTER) {+ if (prev_stats == undefined)+ return null;+ var prev_value = prev_stats;+ $.each(pieces, function(unused_index, piece) {+ prev_value = prev_value[piece]; });+ return 1000 * (value.val - prev_value.val) /+ (time - prev_time);+ } else if (value.type === DISTRIBUTION) {+ return value.mean;+ } else { // value.type === GAUGE || value.type === LABEL+ return value.val; }- if (!paused)- elem.text(commaify(value));- });+ }+ return get; } - subscribe(onDataReceived);- }+ function counterLabel(label) {+ return label + "/s";+ } - function addDynamicLabels(table, group_fn) {- var labels = {};- function onDataReceived(stats, time) {- $.each(group_fn(stats), function (key, value) {- var elem;- if (key in labels) {- elem = labels[key];- } else {- // Add UI element- table.find("tbody:last").append(- '<tr><td>' + key + '</td>' +- '<td class="string">N/A</td></tr>');- elem = table.find("tbody > tr > td:last");- labels[key] = elem;+ function gaugeLabel(label) {+ return label;+ }++ /** Adds the table row. */+ function addElem(key, value) {+ var elem;+ if (key in metrics) {+ elem = metrics[key];+ } else {+ // Add UI element+ table.find("tbody:last").append(+ '<tr><td>' + key ++ ' <img src="chart_line_add.png" class="graph-button"' ++ ' width="16" height="16"' ++ ' alt="Add graph" title="Add graph"></td>' ++ '<td class="value">N/A</td></tr>');+ elem = table.find("tbody > tr > td:last");+ metrics[key] = elem;++ var button = table.find("tbody > tr:last > td:first > img");+ var graph_fn = makeDataGetter(key);+ var label_fn = gaugeLabel;+ if (value.type === COUNTER) {+ label_fn = counterLabel; }- if (!paused)- elem.text(value);- });+ button.click(function () {+ addDynamicPlot(key, button, graph_fn, label_fn);+ $(this).hide();+ });+ }+ if (!paused) {+ if (value.type === DISTRIBUTION) {+ var val = value.mean.toPrecision(8) + '\n+/-' ++ Math.sqrt(value.variance).toPrecision(8) + ' sd';+ } else { // COUNTER, GAUGE, LABEL+ var val = value.val;+ }+ if ($.inArray(value.type, [COUNTER, GAUGE]) !== -1) {+ val = commaify(val);+ }+ elem.text(val);+ } } + /** Updates UI for all metrics. */+ function onDataReceived(stats, time) {+ function build(prefix, obj) {+ $.each(obj, function (suffix, value) {+ if (value.hasOwnProperty("type")) {+ var key = prefix + suffix;+ addElem(key, value);+ } else {+ build(prefix + suffix + '.', value);+ }+ });+ }+ build('', stats);+ }+ subscribe(onDataReceived); } function initAll() { // Metrics var current_bytes_used = function (stats) {- return stats.gauges.current_bytes_used;+ return stats.rts.gc.current_bytes_used.val; }; var max_bytes_used = function (stats) {- return stats.gauges.max_bytes_used;+ return stats.rts.gc.max_bytes_used.val; }; var max_bytes_slop = function (stats) {- return stats.gauges.max_bytes_slop;+ return stats.rts.gc.max_bytes_slop.val; }; var current_bytes_slop = function (stats) {- return stats.gauges.current_bytes_slop;+ return stats.rts.gc.current_bytes_slop.val; }; var productivity_wall_percent = function (stats, time, prev_stats, prev_time) { if (prev_stats == undefined) return null;- var mutator_seconds = stats.counters.mutator_wall_seconds -- prev_stats.counters.mutator_wall_seconds;- var gc_seconds = stats.counters.gc_wall_seconds -- prev_stats.counters.gc_wall_seconds;- return 100 * mutator_seconds / (mutator_seconds + gc_seconds);+ var mutator_ms = stats.rts.gc.mutator_wall_ms.val -+ prev_stats.rts.gc.mutator_wall_ms.val;+ var gc_ms = stats.rts.gc.gc_wall_ms.val -+ prev_stats.rts.gc.gc_wall_ms.val;+ return 100 * mutator_ms / (mutator_ms + gc_ms); }; var productivity_cpu_percent = function (stats, time, prev_stats, prev_time) { if (prev_stats == undefined) return null;- var mutator_seconds = stats.counters.mutator_cpu_seconds -- prev_stats.counters.mutator_cpu_seconds;- var gc_seconds = stats.counters.gc_cpu_seconds -- prev_stats.counters.gc_cpu_seconds;- return 100 * mutator_seconds / (mutator_seconds + gc_seconds);+ var mutator_ms = stats.rts.gc.mutator_cpu_ms.val -+ prev_stats.rts.gc.mutator_cpu_ms.val;+ var gc_ms = stats.rts.gc.gc_cpu_ms.val -+ prev_stats.rts.gc.gc_cpu_ms.val;+ return 100 * mutator_ms / (mutator_ms + gc_ms); }; var allocation_rate = function (stats, time, prev_stats, prev_time) { if (prev_stats == undefined) return null;- return 1000 * (stats.counters.bytes_allocated -- prev_stats.counters.bytes_allocated) /+ return 1000 * (stats.rts.gc.bytes_allocated.val -+ prev_stats.rts.gc.bytes_allocated.val) / (time - prev_time); }; + addMetrics($("#metric-table"));+ // Plots addPlot($("#current-bytes-used-plot > div"), [{ label: "residency", fn: current_bytes_used }],@@ -330,7 +377,7 @@ { label: "cpu time", fn: productivity_cpu_percent }], { yaxis: { tickDecimals: 1, tickFormatter: percentFormatter } }); - // Counters+ // GC and memory statistics addCounter($("#max-bytes-used"), max_bytes_used, formatSuffix); addCounter($("#current-bytes-used"), current_bytes_used, formatSuffix); addCounter($("#max-bytes-slop"), max_bytes_slop, formatSuffix);@@ -338,29 +385,6 @@ addCounter($("#productivity-wall"), productivity_wall_percent, formatPercent); addCounter($("#productivity-cpu"), productivity_cpu_percent, formatPercent); addCounter($("#allocation-rate"), allocation_rate, formatRate);-- addDynamicCounters($("#counter-table"), function (stats) {- return stats.counters;- }, function (key, stats, time, prev_stats, prev_time) {- if (prev_stats == undefined)- return null;- return 1000 * (stats.counters[key] - prev_stats.counters[key]) /- (time - prev_time);- }, function (label) {- return label + "/s";- });-- addDynamicCounters($("#gauge-table"), function (stats) {- return stats.gauges;- }, function (key, stats, time) {- return stats.gauges[key];- }, function (label) {- return label;- });-- addDynamicLabels($("#label-table"), function (stats) {- return stats.labels;- }); } initAll();
ekg.cabal view
@@ -1,11 +1,12 @@ name: ekg-version: 0.3.1.4+version: 0.4.0.0 synopsis: Remote monitoring of processes description: This library lets you remotely monitor a running process over HTTP. It provides a simple way to integrate a monitoring server into any application. homepage: https://github.com/tibbe/ekg+bug-reports: https://github.com/tibbe/ekg/issues license: BSD3 license-file: LICENSE author: Johan Tibell@@ -19,28 +20,28 @@ assets/chart_line_add.png assets/cross.png extra-source-files: LICENSE.icons LICENSE.javascript README.md assets/jquery-1.6.4.js assets/jquery.flot.js- examples/Basic.hs+ examples/Basic.hs CHANGES.md+cabal-version: >= 1.8 library- exposed-modules: System.Remote.Counter- System.Remote.Gauge- System.Remote.Label- System.Remote.Monitoring+ exposed-modules:+ System.Remote.Counter+ System.Remote.Gauge+ System.Remote.Label+ System.Remote.Monitoring - other-modules: Paths_ekg- System.Remote.Common- System.Remote.Counter.Internal- System.Remote.Gauge.Internal- System.Remote.Label.Internal- System.Remote.Snap+ other-modules:+ Paths_ekg+ System.Remote.Json+ System.Remote.Snap build-depends: aeson < 0.8, base >= 4.5 && < 5, bytestring < 1.0,- containers < 0.6,+ ekg-core >= 0.1 && < 0.2, filepath < 1.4,- network < 2.5,+ network < 2.6, snap-core < 0.10, snap-server < 0.10, text < 1.2,
examples/Basic.hs view
@@ -7,6 +7,8 @@ import Control.Concurrent import Control.Exception import Data.List+import Data.Time.Clock.POSIX (getPOSIXTime)+import qualified System.Remote.Event as Event import qualified System.Remote.Counter as Counter import qualified System.Remote.Label as Label import System.Remote.Monitoring@@ -23,10 +25,22 @@ handle <- forkServer "localhost" 8000 counter <- getCounter "iterations" handle label <- getLabel "args" handle+ event <- getEvent "runtime" handle Label.set label "some text string" let loop n = do- evaluate $ mean [1..n]+ t <- timed $ evaluate $ mean [1..n]+ Event.add event t threadDelay 2000 Counter.inc counter loop n loop 1000000++timed :: IO a -> IO Double+timed m = do+ start <- getTime+ m+ end <- getTime+ return $! end - start++getTime :: IO Double+getTime = realToFrac `fmap` getPOSIXTime