Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port Haskell and JavaScript code to dimensional Metrics. #64

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion System/Remote/Json.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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 []
33 changes: 22 additions & 11 deletions System/Remote/Monitoring.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 12 additions & 7 deletions System/Remote/Snap.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
108 changes: 72 additions & 36 deletions assets/monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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(
'<div id="' + plotId + '" class="plot-container">' +
'<img src="cross.png" class="close-button"><h3>' + key +
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
});
}
Expand All @@ -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);
};

Expand Down
7 changes: 4 additions & 3 deletions ekg.cabal
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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,
Expand Down
28 changes: 18 additions & 10 deletions examples/Basic.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it makes sense for a counter to be accessed with only some dimensions. What does it mean to increment a counter that has been given e.g. a URL and a HTTP status code dimension without providing values for those when incrementing it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, what would it mean to sum up all the values in a metric if you have "global" counts and indexed counts?

fizzCounter <- getCounter "iterations" ["dim0:fizz"] handle
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the comment I gave on the pull request in ekg-core. I think its here I'd rather see that the dimension names don't have to be repeated (albeit that does imply they need to be positional).

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