packages feed

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 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