From ec53b2a2c9b8db4085557031854287c00e0bb4fa Mon Sep 17 00:00:00 2001 From: Lucas DiCioccio Date: Thu, 20 Apr 2017 20:16:30 +0000 Subject: [PATCH] Port Haskell and JavaScript code to dimensional Metrics. --- System/Remote/Json.hs | 2 +- System/Remote/Monitoring.hs | 33 +++++++---- System/Remote/Snap.hs | 19 ++++--- assets/monitor.js | 108 ++++++++++++++++++++++++------------ ekg.cabal | 7 ++- examples/Basic.hs | 28 ++++++---- 6 files changed, 129 insertions(+), 68 deletions(-) diff --git a/System/Remote/Json.hs b/System/Remote/Json.hs index 5f6dcd4..3269c8f 100644 --- a/System/Remote/Json.hs +++ b/System/Remote/Json.hs @@ -18,4 +18,4 @@ encodeAll = A.encode . Json.sampleToJson -- | Encode metric a JSON object. See 'Json.valueToJson' -- for a description of the encoding. encodeOne :: Value -> L.ByteString -encodeOne = A.encode . Json.valueToJson +encodeOne = A.encode . Json.valueToJson [] diff --git a/System/Remote/Monitoring.hs b/System/Remote/Monitoring.hs index 5d31658..8e67c01 100644 --- a/System/Remote/Monitoring.hs +++ b/System/Remote/Monitoring.hs @@ -250,35 +250,46 @@ forkServerWith store host port = do -- | 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 +getCounter :: T.Text -- ^ Counter name + -> [T.Text] -- ^ Dimension names + -> Server -- ^ Server that will serve the counter -> IO Counter.Counter -getCounter name server = Metrics.createCounter name (serverMetricStore server) +getCounter name dims server = + Metrics.createCounter (Metrics.dimensional name dims) + (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 +getGauge :: T.Text -- ^ Gauge name + -> [T.Text] -- ^ Dimension names + -> Server -- ^ Server that will serve the gauge -> IO Gauge.Gauge -getGauge name server = Metrics.createGauge name (serverMetricStore server) +getGauge name dims server = + Metrics.createGauge (Metrics.dimensional name dims) + (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 + -> [T.Text] -- ^ Dimension names -> Server -- ^ Server that will serve the label -> IO Label.Label -getLabel name server = Metrics.createLabel name (serverMetricStore server) +getLabel name dims server = + Metrics.createLabel (Metrics.dimensional name dims) + (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 +getDistribution :: T.Text -- ^ Distribution name + -> [T.Text] -- ^ Dimension names + -> Server -- ^ Server that will serve the distribution -> IO Distribution.Distribution -getDistribution name server = - Metrics.createDistribution name (serverMetricStore server) +getDistribution name dims server = + Metrics.createDistribution (Metrics.dimensional name dims) + (serverMetricStore server) ------------------------------------------------------------------------ -- Backwards compatibility shims diff --git a/System/Remote/Snap.hs b/System/Remote/Snap.hs index 6ac25d6..b02e1bb 100644 --- a/System/Remote/Snap.hs +++ b/System/Remote/Snap.hs @@ -12,16 +12,16 @@ import qualified Data.ByteString.Char8 as S8 import Data.Function (on) import qualified Data.HashMap.Strict as M import qualified Data.List as List +import qualified Data.Map.Lazy as LMap 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, getHeader, getRequest, - getResponse, method, Method(GET), modifyResponse, pass, - rqPathInfo, setContentType, setResponseStatus, - writeLBS) +import Snap.Core (MonadSnap, Request, Snap, finishWith, getHeader, getQueryParams, + getRequest, 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) @@ -103,17 +103,22 @@ serve store = do serveAll = do metrics <- liftIO $ sampleAll store writeLBS $ encodeAll metrics + -- consider deprecating this feature? serveOne pathInfo = do + params <- getQueryParams let segments = S8.split '/' pathInfo nameBytes = S8.intercalate "." segments - case T.decodeUtf8' nameBytes of + dims = maybe [] id (LMap.lookup "d" params) + decoded = (,) <$> (T.decodeUtf8' nameBytes) + <*> (traverse T.decodeUtf8' dims) + case decoded of Left _ -> do modifyResponse $ setResponseStatus 400 "Bad Request" r <- getResponse finishWith r - Right name -> do + Right (name, dimensions) -> do metrics <- liftIO $ sampleAll store - case M.lookup name metrics of + case M.lookup (dimensional name dimensions) metrics of Nothing -> pass Just metric -> writeLBS $ encodeOne metric diff --git a/assets/monitor.js b/assets/monitor.js index 558334f..1d88fd8 100644 --- a/assets/monitor.js +++ b/assets/monitor.js @@ -115,7 +115,7 @@ $(document).ready(function () { } alertVisible = false; for (var i = 0; i < listeners.length; i++) { - listeners[i](stats, stats.ekg.server_timestamp_ms.val); + listeners[i](stats, stats.ekg.server_timestamp_ms[0].val); } } @@ -195,11 +195,13 @@ $(document).ready(function () { function addDynamicPlot(key, button, graph_fn, label_fn) { function getStats(stats, time, prev_stats, prev_time) { - return graph_fn(key, stats, time, prev_stats, prev_time); + return graph_fn(stats, time, prev_stats, prev_time); } // jQuery has problem with IDs containing dots. - var plotId = key.replace(/\./g, "-") + "-plot"; + var plotId = key.replace(/\./g, "-") + .replace(/ /g,"__") + .replace(/:/g,"_") + "-plot"; $("#plots:last").append( '
' + '

' + key + @@ -234,20 +236,50 @@ $(document).ready(function () { var DISTRIBUTION = "d"; var metrics = {}; - 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]; - }); + // Utility function to test for arrays of strings equality. + var sameDimensions = function(xs, ys) { + if (xs.length != ys.length) { + return false; + } + var ret = true; + $.each(xs, function(xy_index, x) { + var y = ys[xy_index]; + if (x != y) { + ret = false; + return; + } + }) + return ret; + } + + var lookupStat = function(name, dims, stats) { + var pieces = name.split("."); + // find the nested object + var arrayValues = stats; + $.each(pieces, function(unused_index, piece) { + arrayValues = arrayValues[piece]; + }); + // find the correct dimensional break-down + var value = undefined; + $.each(arrayValues, function(unused_index, obj) { + if (sameDimensions(obj.dims, dims)) { + value = obj; + } + }) + return value; + } + + function makeDataGetter(name, dims) { + function get(stats, time, prev_stats, prev_time) { + // find the nested object + var value = lookupStat(name, dims, stats); + + // do something here if (value.type === COUNTER) { - if (prev_stats == undefined) + if (prev_stats == undefined) { return null; - var prev_value = prev_stats; - $.each(pieces, function(unused_index, piece) { - prev_value = prev_value[piece]; - }); + } + var prev_value = lookupStat(name, dims, prev_stats); return 1000 * (value.val - prev_value.val) / (time - prev_time); } else if (value.type === DISTRIBUTION) { @@ -268,8 +300,9 @@ $(document).ready(function () { } /** Adds the table row. */ - function addElem(key, value) { + function addElem(name, value) { var elem; + var key = name + " " + value.dims.join(" "); if (key in metrics) { elem = metrics[key]; } else { @@ -284,7 +317,7 @@ $(document).ready(function () { metrics[key] = elem; var button = table.find("tbody > tr:last > td:first > img"); - var graph_fn = makeDataGetter(key); + var graph_fn = makeDataGetter(name, value.dims); var label_fn = gaugeLabel; if (value.type === COUNTER) { label_fn = counterLabel; @@ -316,12 +349,15 @@ $(document).ready(function () { /** 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); + $.each(obj, function (suffix, values) { + var name = prefix + suffix; + // leaves are arrays of dimensional break-down + if (Array.isArray(values)) { + $.each(values, function (index, value) { + addElem(name, value); + }); } else { - build(prefix + suffix + '.', value); + build(name + '.', values); } }); } @@ -334,40 +370,40 @@ $(document).ready(function () { function initAll() { // Metrics var current_bytes_used = function (stats) { - return stats.rts.gc.current_bytes_used.val; + return stats.rts.gc.current_bytes_used[0].val; }; var max_bytes_used = function (stats) { - return stats.rts.gc.max_bytes_used.val; + return stats.rts.gc.max_bytes_used[0].val; }; var max_bytes_slop = function (stats) { - return stats.rts.gc.max_bytes_slop.val; + return stats.rts.gc.max_bytes_slop[0].val; }; var current_bytes_slop = function (stats) { - return stats.rts.gc.current_bytes_slop.val; + return stats.rts.gc.current_bytes_slop[0].val; }; var productivity_wall_percent = function (stats, time, prev_stats, prev_time) { if (prev_stats == undefined) return null; - 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; + var mutator_ms = stats.rts.gc.mutator_wall_ms[0].val - + prev_stats.rts.gc.mutator_wall_ms[0].val; + var gc_ms = stats.rts.gc.gc_wall_ms[0].val - + prev_stats.rts.gc.gc_wall_ms[0].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_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; + var mutator_ms = stats.rts.gc.mutator_cpu_ms[0].val - + prev_stats.rts.gc.mutator_cpu_ms[0].val; + var gc_ms = stats.rts.gc.gc_cpu_ms[0].val - + prev_stats.rts.gc.gc_cpu_ms[0].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.rts.gc.bytes_allocated.val - - prev_stats.rts.gc.bytes_allocated.val) / + return 1000 * (stats.rts.gc.bytes_allocated[0].val - + prev_stats.rts.gc.bytes_allocated[0].val) / (time - prev_time); }; diff --git a/ekg.cabal b/ekg.cabal index ab59df8..e069d79 100644 --- a/ekg.cabal +++ b/ekg.cabal @@ -1,5 +1,5 @@ name: ekg -version: 0.4.0.13 +version: 0.5.0.0 synopsis: Remote monitoring of processes description: This library lets you remotely monitor a running process over HTTP. @@ -41,8 +41,9 @@ library aeson < 1.3, base >= 4.5 && < 4.10, bytestring < 1.0, - ekg-core >= 0.1 && < 0.2, - ekg-json >= 0.1 && < 0.2, + containers >= 0.5, + ekg-core >= 0.2 && < 0.3, + ekg-json >= 0.2 && < 0.3, filepath < 1.5, network < 2.7, snap-core < 1.1, diff --git a/examples/Basic.hs b/examples/Basic.hs index 17aee19..b8729d2 100644 --- a/examples/Basic.hs +++ b/examples/Basic.hs @@ -22,18 +22,26 @@ mean xs = sum' xs / fromIntegral (length xs) main :: IO () main = do - handle <- forkServer "localhost" 8000 - counter <- getCounter "iterations" handle - label <- getLabel "args" handle - event <- getDistribution "runtime" handle + handle <- forkServer "0.0.0.0" 8000 + baseCounter <- getCounter "iterations" [] handle + fizzCounter <- getCounter "iterations" ["dim0:fizz"] handle + buzzCounter <- getCounter "iterations" ["dim0:buzz"] handle + fizzbuzzCounter <- getCounter "iterations" ["dim0:fizzbuzz"] handle + label <- getLabel "args" [] handle + event <- getDistribution "runtime" ["123", "456"] handle Label.set label "some text string" - let loop n = do - t <- timed $ evaluate $ mean [1..n] + let counter n + | n `mod` 15 == 0 = fizzbuzzCounter + | n `mod` 5 == 0 = fizzCounter + | n `mod` 3 == 0 = buzzCounter + | otherwise = baseCounter + let loop n m = do + t <- timed $ evaluate $ mean $ fmap fromInteger [1..n] Distribution.add event t - threadDelay 2000 - Counter.inc counter - loop n - loop 1000000 + threadDelay 200 + Counter.inc $ counter m + loop n (m + 1) + loop (1000000 :: Integer) 0 timed :: IO a -> IO Double timed m = do