packages feed

ekg 0.3.0.5 → 0.3.1.0

raw patch · 8 files changed

+233/−72 lines, 8 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- System.Remote.Monitoring: instance Ref Counter
- System.Remote.Monitoring: instance Ref Gauge
+ System.Remote.Label: data Label
+ System.Remote.Label: modify :: (Text -> Text) -> Label -> IO ()
+ System.Remote.Label: set :: Label -> Text -> IO ()
+ System.Remote.Monitoring: getLabel :: Text -> Server -> IO Label
+ System.Remote.Monitoring: instance Ref Counter Int
+ System.Remote.Monitoring: instance Ref Gauge Int
+ System.Remote.Monitoring: instance Ref Label Text

Files

+ System/Remote/Label.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE BangPatterns #-}+-- | 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.+module System.Remote.Label+    (+      Label+    , set+    , 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 ()
+ System/Remote/Label/Internal.hs view
@@ -0,0 +1,21 @@+{-# 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,9 +1,10 @@-{-# LANGUAGE ExistentialQuantification, OverloadedStrings, RecordWildCards #-}+{-# LANGUAGE ExistentialQuantification, OverloadedStrings, RecordWildCards,+  FunctionalDependencies #-} -- | This module provides remote monitoring of a running process over -- HTTP.  It can be used to run an HTTP server that provides both a--- web-based user interface and a machine-readable API (e.g. JSON).+-- web-based user interface and a machine-readable API (e.g. JSON.) -- The former can be used by a human to get an overview of what the--- program is doing and the latter can be used be automated monitoring+-- program is doing and the latter can be used by automated monitoring -- tools. -- -- Typical usage is to start the monitoring server at program startup@@ -29,10 +30,11 @@     , serverThreadId     , forkServer -      -- * User-defined counters and gauges+      -- * User-defined counters, gauges, and labels       -- $userdefined     , getCounter     , getGauge+    , getLabel     ) where  import Control.Applicative ((<$>), (<|>))@@ -70,6 +72,8 @@ 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  -- $configuration --@@ -96,8 +100,8 @@ -- @gauges@ attributes, respectively.  Content types: \"text\/html\" -- (default), \"application\/json\" ----- [\/combined] Flattened JSON object containing all counters and--- gauges.  Content types: \"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\"@@ -112,14 +116,20 @@ -- [\/gauges/\<gauge name\>] Value of a single gauge, as a string. -- The name should be UTF-8 encoded.  Content types: \"text\/plain\" ----- Counters and gauges are stored as attributes of the returned JSON--- objects, one attribute per counter or gauge.  In addition to--- user-defined counters and gauges, 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.+-- [\/labels] JSON object containing all labels.  Content types:+-- \"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@@ -181,12 +191,16 @@ -- 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)     }  -- | The thread ID of the server.  You can kill the server by killing@@ -197,7 +211,7 @@ -- | 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\").+-- 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@@ -208,8 +222,9 @@ forkServer host port = do     counters <- newIORef M.empty     gauges <- newIORef M.empty-    tid <- forkIO $ httpServe conf (monitor counters gauges)-    return $! Server tid counters gauges+    labels <- newIORef M.empty+    tid <- forkIO $ httpServe conf (monitor counters gauges labels)+    return $! Server tid counters gauges labels   where conf = Config.setVerbose False $                Config.setErrorLog Config.ConfigNoLog $                Config.setAccessLog Config.ConfigNoLog $@@ -218,16 +233,18 @@                Config.defaultConfig  --------------------------------------------------------------------------- * User-defined counters and gauges+-- * User-defined counters, gauges and labels  -- $userdefined -- The monitoring server can store and serve user-defined,--- integer-valued counters and gauges.  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.)  Each counter or gauge is associated with--- a name, which is used when the counter or gauge is displayed in the--- UI or returned in a JSON object.+-- integer-valued counters and gauges, and string-value labels.  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+-- 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. -- -- 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@@ -246,26 +263,30 @@ -- >             loop -- >     loop ----- To create a guage, use 'getGauge' instead of 'getCounter' and then+-- To create a gauge, use 'getGauge' instead of 'getCounter' and then -- call e.g. 'System.Remote.Gauge.set' or--- 'System.Remote.Gauge.modify'.+-- 'System.Remote.Gauge.modify'.  Similar for labels. -class Ref r where+class Ref r t | r -> t where     new :: IO r-    read :: r -> IO Int+    read :: r -> IO t -instance Ref Counter where+instance Ref Counter Int where     new = Counter.new     read = Counter.read -instance Ref Gauge where+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+getRef :: Ref r t        => T.Text                      -- ^ 'Ref' name        -> IORef (M.HashMap T.Text r)  -- ^ Server that will serve the 'Ref'        -> IO r@@ -298,6 +319,15 @@          -> 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 @@ -307,13 +337,15 @@     !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@(Stats.GCStats {..}) counters gauges t) = A.object $+    toJSON (Stats gcStats@(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@@ -322,7 +354,7 @@ newtype Combined = Combined Stats  instance A.ToJSON Combined where-    toJSON (Combined (Stats (Stats.GCStats {..}) counters gauges t)) =+    toJSON (Combined (Stats (Stats.GCStats {..}) counters gauges labels t)) =         A.object $         [ "server_timestamp_millis"  .= t         , "bytes_allocated"          .= bytesAllocated@@ -344,7 +376,8 @@         , "par_avg_bytes_copied"     .= parAvgBytesCopied         , "par_max_bytes_copied"     .= parMaxBytesCopied         ] ++ map (uncurry (.=)) counters ++-        map (uncurry (.=)) gauges+        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.@@ -366,14 +399,14 @@ -- * HTTP request handler  -- | A handler that can be installed into an existing Snap application.-monitor :: IORef Counters -> IORef Gauges -> Snap ()-monitor counters gauges = do+monitor :: IORef Counters -> IORef Gauges -> IORef Labels -> Snap ()+monitor counters gauges labels = do     dataDir <- liftIO getDataDir     route [           ("", method GET (format "application/json"-                           (serveAll counters gauges)))+                           (serveAll counters gauges labels)))         , ("combined", method GET (format "application/json"-                                   (serveCombined counters gauges)))+                                   (serveCombined counters gauges labels)))         , ("counters", method GET (format "application/json"                                    (serveMany counters)))         , ("counters/:name", method GET (format "text/plain"@@ -382,6 +415,10 @@                                  (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)))         ]         <|> serveDirectory (dataDir </> "assets") @@ -401,7 +438,7 @@  -- | Get a snapshot of all values.  Note that we're not guaranteed to -- see a consistent snapshot of the whole map.-readAllRefs :: Ref r => IORef (M.HashMap T.Text r) -> IO [(T.Text, Json)]+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@@ -410,7 +447,7 @@ {-# INLINABLE readAllRefs #-}  -- | Serve a collection of counters or gauges, as a JSON object.-serveMany :: Ref r => IORef (M.HashMap T.Text r) -> Snap ()+serveMany :: (Ref r t, A.ToJSON t) => IORef (M.HashMap T.Text r) -> Snap () serveMany mapRef = do     list <- liftIO $ readAllRefs mapRef     modifyResponse $ setContentType "application/json"@@ -418,10 +455,10 @@     writeLBS $ A.encode $ A.toJSON $ Group list time {-# INLINABLE serveMany #-} --- | Serve all counter and gauges, built-in or not, as a nested JSON--- object.-serveAll :: IORef Counters -> IORef Gauges -> Snap ()-serveAll counters gauges = do+-- | 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     req <- getRequest     -- Workaround: Snap still matches requests to /foo to this handler     -- if the Accept header is "application/json", even though such@@ -431,23 +468,25 @@     gcStats <- liftIO Stats.getGCStats     counterList <- liftIO $ readAllRefs counters     gaugeList <- liftIO $ readAllRefs gauges+    labelList <- liftIO $ readAllRefs labels     time <- liftIO getTimeMillis-    writeLBS $ A.encode $ A.toJSON $ Stats gcStats counterList gaugeList time+    writeLBS $ A.encode $ A.toJSON $ Stats gcStats counterList gaugeList labelList time  -- | Serve all counters and gauges, built-in or not, as a flattened -- JSON object.-serveCombined :: IORef Counters -> IORef Gauges -> Snap ()-serveCombined counters gauges = do+serveCombined :: IORef Counters -> IORef Gauges -> IORef Labels -> Snap ()+serveCombined counters gauges labels = do     modifyResponse $ setContentType "application/json"     gcStats <- liftIO Stats.getGCStats     counterList <- liftIO $ readAllRefs counters     gaugeList <- liftIO $ readAllRefs gauges+    labelList <- liftIO $ readAllRefs labels     time <- liftIO getTimeMillis     writeLBS $ A.encode $ A.toJSON $ Combined $-        Stats gcStats counterList gaugeList time+        Stats gcStats counterList gaugeList labelList time  -- | Serve a single counter, as plain text.-serveOne :: Ref r => IORef (M.HashMap T.Text r) -> Snap ()+serveOne :: (Ref r t, Show t) => IORef (M.HashMap T.Text r) -> Snap () serveOne refs = do     modifyResponse $ setContentType "text/plain"     m <- liftIO $ readIORef refs
assets/index.html view
@@ -118,6 +118,19 @@             <tbody>             </tbody>           </table>++          <h3>Labels</h3>+          <table id="label-table" class="condensed-table">+            <thead>+              <tr>+                <th class="span4">Name</th>+                <th class="span1">Value</th>+              </tr>+            </thead>+            <tbody>+            </tbody>+          </table>+         </div>       </div>     </div>
assets/monitor.css view
@@ -46,6 +46,10 @@     text-align: right; } +.string {+    text-align: left;+}+ .graph-button {     cursor: pointer;     vertical-align: middle;
assets/monitor.js view
@@ -4,15 +4,15 @@     // Number formatters     function commaify(n)     {-	var nStr = n.toString();-	var x = nStr.split('.');-	var x1 = x[0];-	var x2 = x.length > 1 ? '.' + x[1] : '';-	var rgx = /(\d+)(\d{3})/;-	while (rgx.test(x1)) {-	    x1 = x1.replace(rgx, '$1' + ',' + '$2');-	}-	return x1 + x2;+        var nStr = n.toString();+        var x = nStr.split('.');+        var x1 = x[0];+        var x2 = x.length > 1 ? '.' + x[1] : '';+        var rgx = /(\d+)(\d{3})/;+        while (rgx.test(x1)) {+            x1 = x1.replace(rgx, '$1' + ',' + '$2');+        }+        return x1 + x2;     }      function formatSuffix(val, opt_prec) {@@ -94,7 +94,7 @@      // Fetch data periodically and notify interested parties.     var listeners = [];-    +     function subscribe(fn) {         listeners.push(fn);     }@@ -132,7 +132,7 @@         });          setTimeout(fetchData, updateInterval);-    };+    }     fetchData();      function addPlot(elem, series, opts) {@@ -140,7 +140,7 @@             series: { shadowSize: 0 },  // drawing is faster without shadows             xaxis: { mode: "time", tickSize: [10, "second"] }         };-        var options = $.extend(true, {}, defaultOptions, opts)+        var options = $.extend(true, {}, defaultOptions, opts);         var data = new Array(series.length);         var maxPoints = 60;         for(var i = 0; i < series.length; i++) {@@ -161,7 +161,7 @@             }              // zip lengends with data-            var res = []+            var res = [];             for(var i = 0; i < series.length; i++)                 res.push({ label: series[i].label, data: data[i] }); @@ -255,6 +255,29 @@         subscribe(onDataReceived);     } +    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;+                }+                if (!paused)+                    elem.text(value);+            });+        }++        subscribe(onDataReceived);+    }+     function initAll() {         // Metrics         var current_bytes_used = function (stats) {@@ -277,7 +300,7 @@             var gc_seconds = stats.counters.gc_wall_seconds -                 prev_stats.counters.gc_wall_seconds;             return 100 * mutator_seconds / (mutator_seconds + gc_seconds);-        }+        };         var productivity_cpu_percent = function (stats, time, prev_stats, prev_time) {             if (prev_stats == undefined)                 return null;@@ -286,14 +309,14 @@             var gc_seconds = stats.counters.gc_cpu_seconds -                 prev_stats.counters.gc_cpu_seconds;             return 100 * mutator_seconds / (mutator_seconds + gc_seconds);-        }+        };         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) /                 (time - prev_time);-        }+        };          // Plots         addPlot($("#current-bytes-used-plot > div"),@@ -308,13 +331,13 @@                 { yaxis: { tickDecimals: 1, tickFormatter: percentFormatter } });          // Counters-        addCounter($("#max-bytes-used"), max_bytes_used, formatSuffix)-        addCounter($("#current-bytes-used"), current_bytes_used, formatSuffix)-        addCounter($("#max-bytes-slop"), max_bytes_slop, formatSuffix)-        addCounter($("#current-bytes-slop"), current_bytes_slop, formatSuffix)-        addCounter($("#productivity-wall"), productivity_wall_percent, formatPercent)-        addCounter($("#productivity-cpu"), productivity_cpu_percent, formatPercent)-        addCounter($("#allocation-rate"), allocation_rate, formatRate)+        addCounter($("#max-bytes-used"), max_bytes_used, formatSuffix);+        addCounter($("#current-bytes-used"), current_bytes_used, formatSuffix);+        addCounter($("#max-bytes-slop"), max_bytes_slop, formatSuffix);+        addCounter($("#current-bytes-slop"), current_bytes_slop, formatSuffix);+        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;@@ -333,6 +356,10 @@             return stats.gauges[key];         }, function (label) {             return label;+        });++        addDynamicLabels($("#label-table"), function (stats) {+            return stats.labels;         });     } 
ekg.cabal view
@@ -1,5 +1,5 @@ name:                ekg-version:             0.3.0.5+version:             0.3.1.0 synopsis:            Remote monitoring of processes description:   This library lets you remotely monitor a running process over HTTP.@@ -19,15 +19,18 @@                      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  library   exposed-modules:     System.Remote.Counter                        System.Remote.Gauge+                       System.Remote.Label                        System.Remote.Monitoring    other-modules:       Paths_ekg                        System.Remote.Counter.Internal                        System.Remote.Gauge.Internal+                       System.Remote.Label.Internal    build-depends:       aeson < 0.7,                        base >= 4.5 && < 5,
+ examples/Basic.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Example program that continously computes the mean of a list of+-- numbers.+module Main where++import Control.Concurrent+import Control.Exception+import qualified System.Remote.Counter as Counter+import qualified System.Remote.Label as Label+import System.Remote.Monitoring++mean :: Fractional a => [a] -> a+mean xs = sum xs / fromIntegral (length xs)++main :: IO ()+main = do+    handle <- forkServer "localhost" 8000+    counter <- getCounter "iterations" handle+    label <- getLabel "args" handle+    Label.set label "some text string"+    let loop n = do+            evaluate $ mean [1..n]+            threadDelay 2000+            Counter.inc counter+            loop n+    loop 1000000