Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial commit.

  • Loading branch information...
commit 831f485dd3ef12c5ce264cf3a56ba842fb173da1 0 parents
@mbostock mbostock authored
Showing with 11,097 additions and 0 deletions.
  1. +1 −0  .gitignore
  2. +14 −0 Makefile
  3. +10 −0 README.md
  4. +7 −0 config/collector.js
  5. +7 −0 config/evaluator.js
  6. +5 −0 examples/emitter/random/random-config.js
  7. +44 −0 examples/emitter/random/random.js
  8. +12 −0 lib/cube/client/board.css
  9. +231 −0 lib/cube/client/board.js
  10. +87 −0 lib/cube/client/body.css
  11. +1 −0  lib/cube/client/cube.js
  12. +1 −0  lib/cube/client/end.js
  13. +58 −0 lib/cube/client/header.js
  14. +12 −0 lib/cube/client/palette.css
  15. +63 −0 lib/cube/client/palette.js
  16. +233 −0 lib/cube/client/piece-area.js
  17. +146 −0 lib/cube/client/piece-sum.js
  18. +73 −0 lib/cube/client/piece-text.js
  19. +116 −0 lib/cube/client/piece.css
  20. +275 −0 lib/cube/client/piece.js
  21. +1 −0  lib/cube/client/semicolon.js
  22. +45 −0 lib/cube/client/squares.js
  23. +1 −0  lib/cube/client/start.js
  24. +24 −0 lib/cube/client/visualizer.html
  25. +76 −0 lib/cube/server/collectd.js
  26. +42 −0 lib/cube/server/collector.js
  27. +67 −0 lib/cube/server/emitter.js
  28. +83 −0 lib/cube/server/endpoint.js
  29. +14 −0 lib/cube/server/evaluator.js
  30. +2,809 −0 lib/cube/server/event-expression.js
  31. +196 −0 lib/cube/server/event-expression.peg
  32. +112 −0 lib/cube/server/event.js
  33. +3,961 −0 lib/cube/server/metric-expression.js
  34. +271 −0 lib/cube/server/metric-expression.peg
  35. +304 −0 lib/cube/server/metric.js
  36. +52 −0 lib/cube/server/reduces.js
  37. +90 −0 lib/cube/server/server.js
  38. +57 −0 lib/cube/server/tiers.js
  39. +18 −0 lib/cube/server/types.js
  40. +180 −0 lib/cube/server/visualizer.js
  41. +12 −0 package.json
  42. +11 −0 schema/schema-create.js
  43. +6 −0 schema/schema-drop.js
  44. +205 −0 test/event-expression-test.js
  45. +291 −0 test/metric-expression-test.js
  46. +209 −0 test/metric-test.js
  47. +138 −0 test/reduces-test.js
  48. +49 −0 test/test.js
  49. +340 −0 test/tiers-test.js
  50. +37 −0 test/types-test.js
1  .gitignore
@@ -0,0 +1 @@
+node_modules
14 Makefile
@@ -0,0 +1,14 @@
+JS_TESTER = ./node_modules/vows/bin/vows
+PEG_COMPILER = ./node_modules/pegjs/bin/pegjs
+
+.PHONY: test
+
+%.js: %.peg Makefile
+ $(PEG_COMPILER) < $< > $@
+
+all: \
+ lib/cube/server/event-expression.js \
+ lib/cube/server/metric-expression.js
+
+test: all
+ @$(JS_TESTER)
10 README.md
@@ -0,0 +1,10 @@
+# Cube
+ __
+ /\ \
+ ___ __ __\ \ \____ ____
+ / ___\/\ \/\ \\ \ __ \ / __ \
+ /\ \__/\ \ \_\ \\ \ \_\ \/\ __/
+ \ \____\\ \____/ \ \____/\ \____\
+ \/____/ \/___/ \/___/ \/____/
+
+See <http://square.github.com/cube>.
7 config/collector.js
@@ -0,0 +1,7 @@
+// Default configuration for development.
+module.exports = {
+ "mongo-host": "127.0.0.1",
+ "mongo-port": 27017,
+ "mongo-database": "cube_development",
+ "http-port": 1080
+};
7 config/evaluator.js
@@ -0,0 +1,7 @@
+// Default configuration for development.
+module.exports = {
+ "mongo-host": "127.0.0.1",
+ "mongo-port": 27017,
+ "mongo-database": "cube_development",
+ "http-port": 1081
+};
5 examples/emitter/random/random-config.js
@@ -0,0 +1,5 @@
+// Default configuration for development.
+module.exports = {
+ "http-host": "127.0.0.1",
+ "http-port": 1080
+};
44 examples/emitter/random/random.js
@@ -0,0 +1,44 @@
+var util = require("util"),
+ emitter = require("../../../lib/cube/server/emitter"),
+ options = require("./random-config"),
+ count = 0,
+ batch = 10,
+ hour = 60 * 60 * 1000,
+ start = Date.now(),
+ offset = -Math.abs(random()) * 24 * hour;
+
+// Connect to websocket.
+util.log("starting websocket client");
+var client = emitter().open(options["http-host"], options["http-port"]);
+
+// Emit random values.
+var interval = setInterval(function() {
+ for (var i = -1; ++i < batch;) {
+ client.send({
+ type: "random",
+ time: new Date(Date.now() + random() * 2 * hour + offset),
+ data: {random: (offset & 1 ? 1 : -1) * Math.random()}
+ });
+ count++;
+ }
+ var duration = Date.now() - start;
+ console.log(count + " events in " + duration + " ms: " + Math.round(1000 * count / duration) + " sps");
+}, 10);
+
+// Display stats on shutdown.
+process.on("SIGINT", function() {
+ console.log("stopping websocket client");
+ client.close();
+ clearInterval(interval);
+});
+
+// Sample from a normal distribution with mean 0, stddev 1.
+function random() {
+ var x = 0, y = 0, r;
+ do {
+ x = Math.random() * 2 - 1;
+ y = Math.random() * 2 - 1;
+ r = x * x + y * y;
+ } while (!r || r > 1);
+ return x * Math.sqrt(-2 * Math.log(r) / r);
+}
12 lib/cube/client/board.css
@@ -0,0 +1,12 @@
+.squares rect {
+ fill: none;
+ stroke: #ddd;
+}
+
+.squares rect.black {
+ fill: #fafafa;
+}
+
+.squares rect.shadow {
+ fill: #e7e7e7;
+}
231 lib/cube/client/board.js
@@ -0,0 +1,231 @@
+cube.board = function(url, id) {
+ var board = {id: cube_board_formatId(id)},
+ socket,
+ interval,
+ pieceId = 0,
+ palette,
+ squares,
+ pieces = [],
+ size = [32, 18], // in number of squares
+ squareSize = 40, // in pixels
+ squareRadius = 4, // in pixels
+ padding = 9.5; // half-pixel for crisp strokes
+
+ var event = d3.dispatch(
+ "size",
+ "squareSize",
+ "squareRadius",
+ "padding",
+ "view"
+ );
+
+ var svg = document.createElementNS(d3.ns.prefix.svg, "svg");
+
+ d3.select(svg)
+ .attr("class", "board");
+
+ event.size.add(resize);
+ event.squareSize.add(resize);
+ event.squareRadius.add(resize);
+ event.padding.add(resize);
+
+ function message(message) {
+ var e = JSON.parse(message.data);
+ switch (e.type) {
+ case "view": {
+ event.view.dispatch.call(board, e);
+ break;
+ }
+ case "add": {
+ var piece = board.add(cube.piece.type[e.piece.type])
+ .fromJSON(e.piece)
+ .off("move", add)
+ .on("move", move);
+
+ d3.select(piece.node())
+ .style("opacity", 1e-6)
+ .transition()
+ .duration(500)
+ .style("opacity", 1);
+
+ pieceId = Math.max(pieceId, piece.id = e.piece.id);
+ break;
+ }
+ case "edit": {
+ pieces.some(function(piece) {
+ if (piece.id == e.piece.id) {
+ piece
+ .off("move", move)
+ .transition(d3.transition().duration(500))
+ .fromJSON(e.piece);
+
+ // Renable events after transition starts.
+ d3.timer(function() { piece.on("move", move); }, 250);
+ return true;
+ }
+ });
+ break;
+ }
+ case "move": {
+ pieces.some(function(piece) {
+ if (piece.id == e.piece.id) {
+ piece
+ .off("move", move)
+ .transition(d3.transition().duration(500))
+ .size(e.piece.size)
+ .position(e.piece.position);
+
+ // Bring to front.
+ svg.parentNode.appendChild(piece.node());
+
+ // Renable events after transition starts.
+ d3.timer(function() { piece.on("move", move); }, 250);
+ return true;
+ }
+ });
+ break;
+ }
+ case "remove": {
+ pieces.some(function(piece) {
+ if (piece.id == e.piece.id) {
+ board.remove(piece, true);
+ return true;
+ }
+ });
+ break;
+ }
+ }
+ }
+
+ function reopen() {
+ if (socket) {
+ pieces.slice().forEach(function(piece) { board.remove(piece, true); });
+ socket.close();
+ }
+ socket = new WebSocket(url);
+ socket.onopen = load;
+ socket.onmessage = message;
+ if (!interval) interval = setInterval(ping, 5000);
+ }
+
+ function load() {
+ if (id && socket && socket.readyState == 1) {
+ socket.send(JSON.stringify({type: "load", id: id}));
+ }
+ }
+
+ function ping() {
+ if (socket.readyState == 1) {
+ socket.send(JSON.stringify({type: "ping", id: id}));
+ } else if (socket.readyState > 1) {
+ reopen();
+ }
+ }
+
+ // A one-time listener to send an add event on mouseup.
+ function add() {
+ socket.send(JSON.stringify({type: "add", id: id, piece: this}));
+ this.off("move", add).on("move", move);
+ }
+
+ function move() {
+ socket.send(JSON.stringify({type: "move", id: id, piece: this}));
+ }
+
+ function edit() {
+ socket.send(JSON.stringify({type: "edit", id: id, piece: this}));
+ }
+
+ function resize() {
+ d3.select(svg)
+ .attr("width", size[0] * squareSize + 2 * padding)
+ .attr("height", (size[1] + 2) * squareSize + 2 * padding);
+
+ d3.select(palette.node())
+ .attr("transform", "translate(" + padding + "," + padding + ")");
+
+ d3.select(squares.node())
+ .attr("transform", "translate(" + padding + "," + (1.5 * squareSize + padding) + ")");
+ }
+
+ board.node = function() {
+ return svg;
+ };
+
+ board.on = function(type, listener) {
+ event[type].add(listener);
+ return board;
+ };
+
+ board.off = function(type, listener) {
+ event[type].remove(listener);
+ return board;
+ };
+
+ board.size = function(x) {
+ if (!arguments.length) return size;
+ event.size.dispatch.call(board, size = x);
+ return board;
+ };
+
+ board.squareSize = function(x) {
+ if (!arguments.length) return squareSize;
+ event.squareSize.dispatch.call(board, squareSize = x);
+ return board;
+ };
+
+ board.squareRadius = function(x) {
+ if (!arguments.length) return squareRadius;
+ event.squareRadius.dispatch.call(board, squareRadius = x);
+ return board;
+ };
+
+ board.padding = function(x) {
+ if (!arguments.length) return padding;
+ event.padding.dispatch.call(board, padding = x);
+ return board;
+ };
+
+ board.add = function(type) {
+ var piece = type(board);
+ piece.id = ++pieceId;
+ piece.on("move", add).on("edit", edit);
+ svg.parentNode.appendChild(piece.node());
+ pieces.push(piece);
+ return piece;
+ };
+
+ board.remove = function(piece, silent) {
+ piece.off("move", move).off("edit", edit);
+ if (silent) {
+ d3.select(piece.node())
+ .style("opacity", 1)
+ .transition()
+ .duration(500)
+ .style("opacity", 1e-6)
+ .remove();
+ } else {
+ socket.send(JSON.stringify({type: "remove", id: id, piece: {id: piece.id}}));
+ svg.parentNode.removeChild(piece.node());
+ }
+ pieces.splice(pieces.indexOf(piece), 1);
+ return piece;
+ };
+
+ board.toJSON = function() {
+ return {id: id, size: size, pieces: pieces};
+ };
+
+ svg.appendChild((palette = cube.palette(board)).node());
+ svg.appendChild((squares = cube.squares(board)).node());
+ resize();
+ reopen();
+
+ return board;
+};
+
+function cube_board_formatId(id) {
+ id = id.toString(36);
+ if (id.length < 6) id = new Array(7 - id.length).join("0") + id;
+ return id;
+}
87 lib/cube/client/body.css
@@ -0,0 +1,87 @@
+body {
+ overflow-x: hidden;
+ overflow-y: scroll;
+ margin-top: 50px;
+}
+
+body, button {
+ font: 14px "Helvetica Neue";
+}
+
+#header {
+ display: block;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 40px;
+ background: #222;
+ background: -webkit-gradient(linear,left top,left bottom,from(#333),to(#111));
+ color: #fff;
+ -webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.5);
+ z-index: 1;
+}
+
+.view #header {
+ top: -60px;
+}
+
+.header {
+ height: 40px;
+ line-height: 40px;
+ width: 1281px;
+}
+
+.header, #board {
+ display: block;
+ margin: auto;
+ position: relative;
+}
+
+#board {
+ width: 1299px;
+}
+
+.left {
+ float: left;
+}
+
+.right {
+ float: right;
+}
+
+.right button {
+ margin: 0 0 0 4px;
+}
+
+svg {
+ display: block;
+}
+
+button {
+ background: #333;
+ background: -webkit-gradient(linear,left top,left bottom,from(#444),to(#222));
+ color: #fff;
+ cursor: pointer;
+ height: 30px;
+ width: 113px;
+ margin: 0 4px 0 0;
+ padding: 0;
+ border: solid 1px #000;
+ border-radius: 4px;
+}
+
+.view {
+ margin-top: -60px;
+ overflow: hidden;
+}
+
+.view .squares {
+ display: none;
+}
+
+@media screen and (min-width: 1920px) {
+ .view {
+ -webkit-transform: scale(1.48,1.48)translate(0px,110px);
+ }
+}
1  lib/cube/client/cube.js
@@ -0,0 +1 @@
+cube = {version: "0.0.1"};
1  lib/cube/client/end.js
@@ -0,0 +1 @@
+})();
58 lib/cube/client/header.js
@@ -0,0 +1,58 @@
+cube.header = function(board) {
+ var header = {};
+
+ var div = document.createElement("div");
+
+ var selection = d3.select(div)
+ .attr("class", "header");
+
+ var left = selection.append("div")
+ .attr("class", "left");
+
+ left.append("a")
+ .attr("href", "/" + board.id)
+ .append("button")
+ .text("View");
+
+ left.append("a")
+ .attr("href", "/" + board.id + "/edit")
+ .append("button")
+ .text("Edit");
+
+ var viewers = selection.append("div")
+ .attr("class", "right");
+
+ board.on("view", function(e) {
+ viewers.text(e.count > 1 ? e.count - 1 + " other" + (e.count > 2 ? "s" : "") + " viewing" : null);
+ });
+
+ header.node = function() {
+ return div;
+ };
+
+ if (mode == "view") {
+ var shown = false;
+
+ d3.select(window)
+ .on("mouseout", mouseout)
+ .on("mousemove", mousemove);
+
+ function show(show) {
+ if (show != shown) {
+ d3.select(div.parentNode).transition()
+ .style("top", ((shown = show) ? 0 : -60) + "px");
+ }
+ }
+
+ function mouseout() {
+ if (d3.event.relatedTarget == null) show(false);
+ }
+
+ function mousemove() {
+ if (d3.event.pageY > 120) show(false);
+ else if (d3.event.pageY < 60) show(true);
+ }
+ }
+
+ return header;
+};
12 lib/cube/client/palette.css
@@ -0,0 +1,12 @@
+.palette rect {
+ fill: #eee;
+ stroke: #999;
+}
+
+.palette rect:hover {
+ fill: #ddd;
+}
+
+.palette text {
+ pointer-events: none;
+}
63 lib/cube/client/palette.js
@@ -0,0 +1,63 @@
+cube.palette = function(board) {
+ var palette = {};
+
+ var g = document.createElementNS(d3.ns.prefix.svg, "g");
+
+ var type = d3.select(g)
+ .attr("class", "palette")
+ .selectAll(".piece-type")
+ .data(d3.entries(cube.piece.type))
+ .enter().append("svg:g")
+ .attr("class", "piece-type")
+ .on("mousedown", mousedown);
+
+ type.append("svg:rect");
+
+ type.append("svg:text")
+ .attr("dy", ".35em")
+ .attr("text-anchor", "middle")
+ .text(function(d) { return d.key; });
+
+ board
+ .on("squareSize", resize)
+ .on("squareRadius", resize);
+
+ function resize() {
+ var size = board.squareSize(),
+ radius = board.squareRadius();
+
+ type
+ .attr("transform", function(d, i) { return "translate(" + (i * size + size / 2) + "," + (size / 2) + ")"; })
+ .select("rect")
+ .attr("x", -size / 2)
+ .attr("y", -size / 2)
+ .attr("width", size)
+ .attr("height", size)
+ .attr("rx", radius)
+ .attr("ry", radius);
+ }
+
+ function mousedown(d) {
+ var piece = board.add(d.value),
+ pieceSize = piece.size(),
+ squareSize = board.squareSize(),
+ mouse = d3.svg.mouse(g);
+
+ piece.position([
+ mouse[0] / squareSize - pieceSize[0] / 2,
+ mouse[1] / squareSize - pieceSize[1] / 2 - 1.5
+ ]);
+
+ // Simulate mousedown on the piece to start dragging.
+ var div = d3.select(piece.node());
+ div.each(div.on("mousedown.piece"));
+ }
+
+ palette.node = function() {
+ return g;
+ };
+
+ resize();
+
+ return palette;
+};
233 lib/cube/client/piece-area.js
@@ -0,0 +1,233 @@
+cube.piece.type.area = function(board) {
+ var timeout,
+ data = [],
+ dt0;
+
+ var area = cube.piece(board)
+ .on("size", resize)
+ .on("serialize", serialize)
+ .on("deserialize", deserialize);
+
+ var div = d3.select(area.node())
+ .classed("area", true);
+
+ if (mode == "edit") {
+ div.append("h3")
+ .attr("class", "title")
+ .text("Area Chart");
+
+ var query = div.append("textarea")
+ .attr("class", "query")
+ .style("margin-left", "4px")
+ .style("height", "20px")
+ .style("border-radius", "4px")
+ .style("border", "solid 1px #ccc")
+ .style("resize", "none")
+ .attr("placeholder", "query expression…")
+ .on("keyup.area", querychange)
+ .on("focus.area", area.focus)
+ .on("blur.area", area.blur);
+
+ var time = div.append("div")
+ .attr("class", "time")
+ .style("margin-top", "1px")
+ .style("margin-left", "4px")
+ .style("line-height", "20px")
+ .text("Time Range:")
+ .append("select")
+ .style("margin-right", "4px")
+ .style("float", "right")
+ .style("height", "20px")
+ .style("border-radius", "4px")
+ .style("border", "solid 1px #ccc")
+ .on("change.area", area.edit)
+ .on("focus.area", area.focus)
+ .on("blur.area", area.blur);
+
+ time.selectAll("option")
+ .data([
+ {description: "1 Day / 5-Minute", value: 864e5 + "/" + 3e5},
+ {description: "3 Days / 5-Minute", value: 3 * 864e5 + "/" + 3e5},
+ {description: "7 Days / Hour", value: 7 * 864e5 + "/" + 36e5},
+ {description: "14 Days / Hour", value: 14 * 864e5 + "/" + 36e5},
+ {description: "30 Days / Hour", value: 30 * 864e5 + "/" + 36e5},
+ {description: "30 Days / Day", value: 30 * 864e5 + "/" + 864e5}
+ ])
+ .enter().append("option")
+ .property("selected", function(d, i) { return i == 1; })
+ .attr("value", cube_piece_areaValue)
+ .text(function(d) { return d.description; });
+ } else {
+ var m = [6, 40, 14, 10],
+ socket;
+
+ var svg = div.append("svg:svg");
+
+ var x = d3.time.scale(),
+ y = d3.scale.linear(),
+ xAxis = d3.svg.axis().scale(x).tickSubdivide(true),
+ yAxis = d3.svg.axis().scale(y).orient("right");
+
+ var a = d3.svg.area()
+ .interpolate("step-after")
+ .x(function(d) { return x(d.time); })
+ .y1(function(d) { return y(d.value); });
+
+ var l = d3.svg.line()
+ .interpolate("step-after")
+ .x(function(d) { return x(d.time); })
+ .y(function(d) { return y(d.value); });
+
+ var g = svg.append("svg:g")
+ .attr("transform", "translate(" + m[3] + "," + m[0] + ")");
+
+ g.append("svg:g").attr("class", "y axis").call(yAxis);
+ g.append("svg:path").attr("class", "area");
+ g.append("svg:g").attr("class", "x axis").call(xAxis);
+ g.append("svg:path").attr("class", "line");
+ }
+
+ function resize() {
+ var transition = area.transition();
+
+ if (mode == "edit") {
+ var innerSize = area.innerSize();
+
+ transition.select(".query")
+ .style("width", innerSize[0] - 12 + "px")
+ .style("height", innerSize[1] - 58 + "px");
+
+ transition.select(".time select")
+ .style("width", innerSize[0] - 100 + "px");
+
+ } else {
+ var z = board.squareSize(),
+ w = area.size()[0] * z - m[1] - m[3],
+ h = area.size()[1] * z - m[0] - m[2];
+
+ x.range([0, w]);
+ y.range([h, 0]);
+ xAxis.ticks(w / 80).tickSize(-h);
+ yAxis.ticks(h / 25).tickSize(-w);
+ a.y0(h);
+
+ transition.select("svg")
+ .attr("width", w + m[1] + m[3])
+ .attr("height", h + m[0] + m[2]);
+
+ transition.select(".area")
+ .attr("d", a(data));
+
+ transition.select(".x.axis")
+ .attr("transform", "translate(0," + h + ")")
+ .call(xAxis);
+
+ transition.select(".y.axis")
+ .attr("transform", "translate(" + w + ",0)")
+ .call(yAxis);
+
+ transition.select(".line")
+ .attr("d", l(data));
+ }
+ }
+
+ function redraw() {
+ if (data.length > 1) data[data.length - 1].value = data[data.length - 2].value;
+ y.domain([0, d3.max(data, cube_piece_areaValue) || 1]).nice();
+ div.select(".area").attr("d", a(data));
+ div.select(".y.axis").call(yAxis);
+ div.select(".line").attr("d", l(data));
+ return true;
+ }
+
+ function querychange() {
+ if (timeout) clearTimeout(timeout);
+ timeout = setTimeout(area.edit, 750);
+ }
+
+ function serialize(json) {
+ var t = time.property("value").split("/");
+ json.type = "area";
+ json.query = query.property("value");
+ json.time = {range: +t[0], step: +t[1]};
+ }
+
+ function deserialize(json) {
+ if (mode == "edit") {
+ query.property("value", json.query);
+ time.property("value", json.time.range + "/" + json.time.step);
+ } else {
+ var dt1 = json.time.step,
+ t1 = new Date(Math.floor(Date.now() / dt1) * dt1),
+ t0 = new Date(t1 - json.time.range),
+ d0 = x.domain(),
+ d1 = [t0, t1];
+
+ if (dt0 != dt1) {
+ data = [];
+ dt0 = dt1;
+ }
+
+ if (d0 != d1 + "") {
+ x.domain(d1);
+ resize();
+ var times = data.map(cube_piece_areaTime);
+ data = data.slice(d3.bisectLeft(times, t0), d3.bisectLeft(times, t1));
+ data.push({time: t1, value: 0});
+ }
+
+ if (timeout) timeout = clearTimeout(timeout);
+ if (socket) socket.close();
@mnutt
mnutt added a note

I've noticed that when you decrease the refresh interval, this websocket gets closed and opened a lot. Is there a reason we reopen the socket here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ socket = new WebSocket("ws://" + location.host + "/1.0/metric/get");
+ socket.onopen = load;
+ socket.onmessage = store;
+
+ function load() {
+ timeout = setTimeout(function() {
+ socket.send(JSON.stringify({
+ expression: json.query,
+ start: t0,
+ stop: t1,
+ step: dt1
+ }));
+ timeout = setTimeout(function() {
+ deserialize(json);
+ }, t1 - Date.now() + dt1 + 4500 + 1000 * Math.random());
+ }, 500);
+ }
+
+ // TODO use a skip list to insert more efficiently
+ // TODO compute contiguous segments on the fly
+ function store(message) {
+ var d = JSON.parse(message.data),
+ i = d3.bisectLeft(data.map(cube_piece_areaTime), d.time = new Date(d.time));
+ if (i < 0 || data[i].time - d.time) {
+ if (d.value != null) {
+ data.splice(i, 0, d);
+ }
+ } else if (d.value == null) {
+ data.splice(i, 1);
+ } else {
+ data[i] = d;
+ }
+ d3.timer(redraw);
+ }
+ }
+ }
+
+ area.copy = function() {
+ return board.add(cube.piece.type.area);
+ };
+
+ resize();
+
+ return area;
+};
+
+function cube_piece_areaTime(d) {
+ return d.time;
+}
+
+function cube_piece_areaValue(d) {
+ return d.value;
+}
146 lib/cube/client/piece-sum.js
@@ -0,0 +1,146 @@
+cube.piece.type.sum = function(board) {
+ var timeout,
+ socket,
+ data = 0,
+ format = d3.format(",.0f");
+
+ var sum = cube.piece(board)
+ .on("size", resize)
+ .on("serialize", serialize)
+ .on("deserialize", deserialize);
+
+ var div = d3.select(sum.node())
+ .classed("sum", true);
+
+ if (mode == "edit") {
+ div.append("h3")
+ .attr("class", "title")
+ .text("Rolling Sum");
+
+ var query = div.append("textarea")
+ .attr("class", "query")
+ .style("margin-left", "4px")
+ .style("height", "20px")
+ .style("border-radius", "4px")
+ .style("border", "solid 1px #ccc")
+ .style("resize", "none")
+ .attr("placeholder", "query expression…")
+ .on("keyup.sum", querychange)
+ .on("focus.sum", sum.focus)
+ .on("blur.sum", sum.blur);
+
+ var time = div.append("div")
+ .attr("class", "time")
+ .style("margin-top", "1px")
+ .style("margin-left", "4px")
+ .style("line-height", "20px")
+ .text("Time Range:")
+ .append("select")
+ .style("margin-right", "4px")
+ .style("float", "right")
+ .style("height", "20px")
+ .style("border-radius", "4px")
+ .style("border", "solid 1px #ccc")
+ .on("change.sum", sum.edit)
+ .on("focus.sum", sum.focus)
+ .on("blur.sum", sum.blur);
+
+ time.selectAll("option")
+ .data([
+ {description: "5 Minutes / 5-Minute", value: 3e5 + "/" + 3e5},
+ {description: "1 Hour / 5-Minute", value: 36e5 + "/" + 3e5},
+ {description: "1 Hour / Hour", value: 36e5 + "/" + 36e5},
+ {description: "1 Day / 5-Minute", value: 864e5 + "/" + 3e5},
+ {description: "1 Day / Hour", value: 864e5 + "/" + 36e5},
+ {description: "1 Day / Day", value: 864e5 + "/" + 864e5}
+ ])
+ .enter().append("option")
+ .property("selected", function(d, i) { return i == 1; })
+ .attr("value", cube_piece_areaValue)
+ .text(function(d) { return d.description; });
+ } else {
+ div
+ .style("text-align", "right");
+ }
+
+ function resize() {
+ var innerSize = sum.innerSize(),
+ transition = sum.transition();
+
+ if (mode == "edit") {
+ transition.select(".query")
+ .style("width", innerSize[0] - 12 + "px")
+ .style("height", innerSize[1] - 58 + "px");
+
+ transition.select(".time select")
+ .style("width", innerSize[0] - 100 + "px");
+ } else {
+ transition
+ .style("font-size", innerSize[0] / 5 + "px")
+ .style("line-height", innerSize[1] + "px")
+ .text(format(data));
+ }
+ }
+
+ function redraw() {
+ div.text(format(data));
+ return true;
+ }
+
+ function querychange() {
+ if (timeout) clearTimeout(timeout);
+ timeout = setTimeout(sum.edit, 750);
+ }
+
+ function serialize(json) {
+ var t = time.property("value").split("/");
+ json.type = "sum";
+ json.query = query.property("value");
+ json.time = {range: +t[0], step: +t[1]};
+ }
+
+ function deserialize(json) {
+ if (!json.time.range) json.time = {range: json.time, step: 3e5};
+ if (mode == "edit") {
+ query.property("value", json.query);
+ time.property("value", json.time.range + "/" + json.time.step);
+ } else {
+ var dt = json.time.step,
+ t1 = new Date(Math.floor(Date.now() / dt) * dt),
+ t0 = new Date(t1 - json.time.range);
+
+ data = 0;
+
+ if (timeout) timeout = clearTimeout(timeout);
+ if (socket) socket.close();
+ socket = new WebSocket("ws://" + location.host + "/1.0/metric/get");
+ socket.onopen = load;
+ socket.onmessage = store;
+
+ function load() {
+ socket.send(JSON.stringify({
+ expression: json.query,
+ start: t0,
+ stop: t1,
+ step: dt
+ }));
+ timeout = setTimeout(function() {
+ deserialize(json);
+ }, t1 - Date.now() + dt + 4500 + 1000 * Math.random());
+ }
+
+ function store(message) {
+ data += JSON.parse(message.data).value;
+ d3.timer(redraw);
+ }
+ }
+ }
+
+ sum.copy = function() {
+ return board.add(cube.piece.type.sum);
+ };
+
+ resize();
+
+ return sum;
+};
73 lib/cube/client/piece-text.js
@@ -0,0 +1,73 @@
+cube.piece.type.text = function(board) {
+ var timeout;
+
+ var text = cube.piece(board)
+ .on("size", resize)
+ .on("serialize", serialize)
+ .on("deserialize", deserialize);
+
+ var div = d3.select(text.node())
+ .classed("text", true);
+
+ if (mode == "edit") {
+ div.append("h3")
+ .attr("class", "title")
+ .text("Text Label");
+
+ var content = div.append("textarea")
+ .attr("class", "content")
+ .style("margin-left", "4px")
+ .style("height", "20px")
+ .style("border-radius", "4px")
+ .style("border", "solid 1px #ccc")
+ .style("resize", "none")
+ .attr("placeholder", "text content…")
+ .on("keyup.text", textchange)
+ .on("focus.text", text.focus)
+ .on("blur.text", text.blur);
+ } else {
+ div
+ .style("text-align", "right");
+ }
+
+ function resize() {
+ var transition = text.transition(),
+ innerSize = text.innerSize();
+
+ if (mode == "edit") {
+ transition.select(".content")
+ .style("width", innerSize[0] - 12 + "px")
+ .style("height", innerSize[1] - 34 + "px");
+ } else {
+ transition
+ .style("font-size", innerSize[0] / 12 + "px")
+ .style("margin-top", innerSize[1] / 2 + innerSize[0] / 5 - innerSize[0] / 12 + "px");
+ }
+ }
+
+ function textchange() {
+ if (timeout) clearTimeout(timeout);
+ timeout = setTimeout(text.edit, 750);
+ }
+
+ function serialize(json) {
+ json.type = "text";
+ json.content = content.property("value");
+ }
+
+ function deserialize(json) {
+ if (mode == "edit") {
+ content.property("value", json.content);
+ } else {
+ div.text(json.content);
+ }
+ }
+
+ text.copy = function() {
+ return board.add(cube.piece.type.text);
+ };
+
+ resize();
+
+ return text;
+};
116 lib/cube/client/piece.css
@@ -0,0 +1,116 @@
+.piece {
+ position: absolute;
+ text-shadow: 0 1px 1px #fff;
+ font-size: 12px;
+}
+
+.view .piece {
+ font: 10px sans-serif;
+ padding: 4px 4px 3px 3px;
+}
+
+.edit .piece {
+ background: #eee;
+ border: solid 2px #ccc;
+ border-radius: 4px;
+ margin: -2px 0 0 -2px;
+}
+
+.piece:focus, .piece.active {
+ box-shadow: 0 4px 8px rgba(0,0,0,.3);
+ outline-style: none;
+ border-color: #999;
+ background-color: #e7e7e7;
+}
+
+.piece h3 {
+ pointer-events: none;
+ font-size: 14px;
+ margin: 4px;
+}
+
+.resize {
+ position: absolute;
+ padding: 6px;
+}
+
+.resize.n {
+ top: -8px;
+ left: -8px;
+ width: 100%;
+ cursor: ns-resize;
+}
+
+.resize.e {
+ top: -8px;
+ right: -8px;
+ height: 100%;
+ cursor: ew-resize;
+}
+
+.resize.s {
+ bottom: -8px;
+ left: -8px;
+ width: 100%;
+ cursor: ns-resize;
+}
+
+.resize.w {
+ top: -8px;
+ left: -8px;
+ height: 100%;
+ cursor: ew-resize;
+}
+
+.resize.nw {
+ top: -8px;
+ left: -8px;
+ cursor: nwse-resize;
+}
+
+.resize.ne {
+ top: -8px;
+ right: -8px;
+ cursor: nesw-resize;
+}
+
+.resize.se {
+ bottom: -8px;
+ right: -8px;
+ cursor: nwse-resize;
+}
+
+.resize.sw {
+ bottom: -8px;
+ left: -8px;
+ cursor: nesw-resize;
+}
+
+.area path.area {
+ fill: #e7e7e7;
+}
+
+.area path.line {
+ fill: none;
+ stroke: #666;
+}
+
+.axis .domain {
+ display: none;
+}
+
+.x.axis line {
+ stroke: #fff;
+}
+
+.x.axis .minor {
+ stroke-opacity: .5;
+}
+
+.y.axis line {
+ stroke: #eee;
+}
+
+.y.axis g:first-child {
+ display: none;
+}
275 lib/cube/client/piece.js
@@ -0,0 +1,275 @@
+cube.piece = function(board) {
+ var piece = {},
+ size = [8, 3],
+ position = [0, 0],
+ padding = 4;
+
+ var event = d3.dispatch(
+ "position",
+ "size",
+ "move",
+ "edit",
+ "serialize",
+ "deserialize"
+ );
+
+ var div = document.createElement("div"),
+ selection = d3.select(div),
+ transition = selection;
+
+ var selection = d3.select(div)
+ .attr("class", "piece");
+
+ if (mode == "edit") {
+ selection
+ .attr("tabindex", 1)
+ .on("keydown.piece", keydown)
+ .on("mousedown.piece", mousedrag)
+ .selectAll(".resize")
+ .data(["n", "e", "s", "w", "nw", "ne", "se", "sw"])
+ .enter().append("div")
+ .attr("class", function(d) { return "resize " + d; })
+ .on("mousedown.piece", mouseresize);
+
+ d3.select(window)
+ .on("keydown.piece", cube_piece_keydown)
+ .on("mousemove.piece", cube_piece_mousemove)
+ .on("mouseup.piece", cube_piece_mouseup);
+ }
+
+ board
+ .on("padding", resize)
+ .on("squareSize", resize);
+
+ event.position.add(resize);
+ event.size.add(resize);
+ event.deserialize.add(deserialize);
+
+ function resize() {
+ var squareSize = board.squareSize(),
+ boardPadding = board.padding() | 0;
+
+ piece.transition()
+ .style("left", (padding + boardPadding + position[0] * squareSize) + "px")
+ .style("top", (padding + boardPadding + (1.5 + position[1]) * squareSize) + "px")
+ .style("width", size[0] * squareSize - 2 * padding + 1 + "px")
+ .style("height", size[1] * squareSize - 2 * padding + 1 + "px");
+ }
+
+ function deserialize(x) {
+ piece
+ .size(x.size)
+ .position(x.position);
+ }
+
+ function keydown() {
+ if (d3.event.target !== this) return d3.event.stopPropagation();
+ if (cube_piece_dragPiece) return;
+ if (d3.event.keyCode === 8) {
+ board.remove(piece);
+ d3.event.preventDefault();
+ }
+ }
+
+ piece.node = function() {
+ return div;
+ };
+
+ piece.on = function(type, listener) {
+ event[type].add(listener);
+ return piece;
+ };
+
+ piece.off = function(type, listener) {
+ event[type].remove(listener);
+ return piece;
+ };
+
+ piece.size = function(x) {
+ if (!arguments.length) return size;
+ event.size.dispatch.call(piece, size = x);
+ return piece;
+ };
+
+ piece.innerSize = function() {
+ var squareSize = board.squareSize();
+ return [
+ size[0] * squareSize - 2 * padding,
+ size[1] * squareSize - 2 * padding
+ ];
+ };
+
+ piece.position = function(x) {
+ if (!arguments.length) return position;
+ event.position.dispatch.call(piece, position = x);
+ return piece;
+ };
+
+ piece.toJSON = function() {
+ var x = {id: piece.id, size: size, position: position};
+ event.serialize.dispatch.call(piece, x);
+ return x;
+ };
+
+ piece.fromJSON = function(x) {
+ event.deserialize.dispatch.call(piece, x);
+ return piece;
+ };
+
+ piece.edit = function() {
+ event.edit.dispatch.call(piece);
+ return piece;
+ };
+
+ function mousedrag() {
+ if (/^(INPUT|TEXTAREA|SELECT)$/.test(d3.event.target.tagName)) return;
+ if (d3.event.target === this && d3.event.altKey) {
+ cube_piece_dragPiece = piece.copy().fromJSON(piece.toJSON());
+ cube_piece_dragPiece.node().focus();
+ } else {
+ d3.select(this).transition(); // cancel transition, if any
+ this.parentNode.appendChild(this).focus();
+ cube_piece_dragPiece = piece;
+ }
+ cube_piece_dragOrigin = [d3.event.pageX, d3.event.pageY];
+ cube_piece_dragPosition = position.slice();
+ cube_piece_dragSize = size.slice();
+ cube_piece_dragBoard = board;
+ cube_piece_mousemove();
+ }
+
+ function mouseresize(d) {
+ cube_piece_dragResize = d;
+ }
+
+ piece.transition = function(x) {
+ if (!arguments.length) return transition;
+ if (x == null) {
+ transition = selection;
+ } else {
+ transition = x.select(function() { return div; });
+ d3.timer(function() {
+ event.move.dispatch.call(piece);
+ return transition = selection;
+ });
+ }
+ return piece;
+ };
+
+ piece.focus = function() {
+ selection.classed("active", true);
+ return piece;
+ };
+
+ piece.blur = function() {
+ selection.classed("active", false);
+ return piece;
+ };
+
+ resize();
+
+ return piece;
+};
+
+cube.piece.type = {};
+
+var cube_piece_dragPiece,
+ cube_piece_dragBoard,
+ cube_piece_dragOrigin,
+ cube_piece_dragPosition,
+ cube_piece_dragSize,
+ cube_piece_dragResize;
+
+function cube_piece_mousePosition() {
+ var squareSize = cube_piece_dragBoard.squareSize();
+ return cube_piece_dragResize ? [
+ cube_piece_dragPosition[0] + /w$/.test(cube_piece_dragResize) * Math.min(cube_piece_dragSize[0] - 5, (d3.event.pageX - cube_piece_dragOrigin[0]) / squareSize),
+ cube_piece_dragPosition[1] + /^n/.test(cube_piece_dragResize) * Math.min(cube_piece_dragSize[1] - 3, (d3.event.pageY - cube_piece_dragOrigin[1]) / squareSize)
+ ] : [
+ cube_piece_dragPosition[0] + (d3.event.pageX - cube_piece_dragOrigin[0]) / squareSize,
+ cube_piece_dragPosition[1] + (d3.event.pageY - cube_piece_dragOrigin[1]) / squareSize
+ ];
+}
+
+function cube_piece_mouseSize() {
+ var squareSize = cube_piece_dragBoard.squareSize();
+ return cube_piece_dragResize ? [
+ Math.max(5, cube_piece_dragSize[0] + (/e$/.test(cube_piece_dragResize) ? 1 : /w$/.test(cube_piece_dragResize) ? -1 : 0) * (d3.event.pageX - cube_piece_dragOrigin[0]) / squareSize),
+ Math.max(3, cube_piece_dragSize[1] + (/^s/.test(cube_piece_dragResize) ? 1 : /^n/.test(cube_piece_dragResize) ? -1 : 0) * (d3.event.pageY - cube_piece_dragOrigin[1]) / squareSize)
+ ] : cube_piece_dragSize;
+}
+
+function cube_piece_clamp(position, size) {
+ var boardSize = cube_piece_dragBoard.size();
+ if (cube_piece_dragResize) {
+ if (/e$/.test(cube_piece_dragResize)) {
+ size[0] = Math.max(0, Math.min(boardSize[0] - position[0], Math.round(size[0])));
+ } else if (/w$/.test(cube_piece_dragResize)) {
+ size[0] = Math.round(size[0] + position[0] - (position[0] = Math.max(0, Math.min(boardSize[0], Math.round(position[0])))));
+ }
+ if (/^s/.test(cube_piece_dragResize)) {
+ size[1] = Math.max(0, Math.min(boardSize[1] - position[1], Math.round(size[1])));
+ } else if (/^n/.test(cube_piece_dragResize)) {
+ size[1] = Math.round(size[1] + position[1] - (position[1] = Math.max(0, Math.min(boardSize[1], Math.round(position[1])))));
+ }
+ } else {
+ position[0] = Math.max(0, Math.min(boardSize[0] - size[0], Math.round(position[0])));
+ position[1] = Math.max(0, Math.min(boardSize[1] - size[1], Math.round(position[1])));
+ }
+}
+
+function cube_piece_mousemove() {
+ if (cube_piece_dragPiece) {
+ var position = cube_piece_mousePosition(),
+ size = cube_piece_mouseSize();
+
+ cube_piece_dragPiece
+ .position(position.slice())
+ .size(size.slice());
+
+ cube_piece_clamp(position, size);
+
+ var x0 = position[0],
+ y0 = position[1],
+ x1 = x0 + size[0],
+ y1 = y0 + size[1];
+
+ d3.select(cube_piece_dragBoard.node()).selectAll(".squares rect")
+ .classed("shadow", function(d, i) { return d.x >= x0 && d.x < x1 && d.y >= y0 && d.y < y1; });
+
+ d3.event.preventDefault();
+ }
+}
+
+function cube_piece_mouseup() {
+ if (cube_piece_dragPiece) {
+ var position = cube_piece_mousePosition(),
+ size = cube_piece_mouseSize();
+
+ cube_piece_clamp(position, size);
+
+ d3.select(cube_piece_dragBoard.node()).selectAll(".squares rect")
+ .classed("shadow", false);
+
+ cube_piece_dragPiece
+ .transition(d3.transition().ease("elastic").duration(500))
+ .position(position)
+ .size(size);
+
+ cube_piece_dragPiece =
+ cube_piece_dragBoard =
+ cube_piece_dragEvent =
+ cube_piece_dragOrigin =
+ cube_piece_dragPosition =
+ cube_piece_dragSize =
+ cube_piece_dragResize = null;
+ d3.event.preventDefault();
+ }
+}
+
+// Disable delete as the back key, since we use it to delete pieces.
+function cube_piece_keydown() {
+ if (d3.event.keyCode == 8) {
+ d3.event.preventDefault();
+ }
+}
1  lib/cube/client/semicolon.js
@@ -0,0 +1 @@
+;
45 lib/cube/client/squares.js
@@ -0,0 +1,45 @@
+cube.squares = function(board) {
+ var squares = {};
+
+ var g = document.createElementNS(d3.ns.prefix.svg, "g");
+
+ d3.select(g)
+ .attr("class", "squares");
+
+ board
+ .on("size", resize)
+ .on("squareSize", resize)
+ .on("squareRadius", resize);
+
+ function resize() {
+ var boardSize = board.size(),
+ squareSize = board.squareSize(),
+ squareRadius = board.squareRadius();
+
+ var square = d3.select(g).selectAll(".square")
+ .data(d3.range(boardSize[0] * boardSize[1])
+ .map(function(d) {return { x: d % boardSize[0], y: d / boardSize[0] | 0}; }));
+
+ square.enter().append("svg:rect")
+ .attr("class", "square");
+
+ square
+ .attr("rx", squareRadius)
+ .attr("ry", squareRadius)
+ .attr("class", function(d, i) { return (i - d.y & 1 ? "black" : "white") + " square"; })
+ .attr("x", function(d) { return d.x * squareSize; })
+ .attr("y", function(d) { return d.y * squareSize; })
+ .attr("width", squareSize)
+ .attr("height", squareSize);
+
+ square.exit().remove();
+ }
+
+ squares.node = function() {
+ return g;
+ };
+
+ resize();
+
+ return squares;
+};
1  lib/cube/client/start.js
@@ -0,0 +1 @@
+(function(){
24 lib/cube/client/visualizer.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Cube</title>
+ <script type="text/javascript" src="/d3/d3.js"></script>
+ <script type="text/javascript" src="/cube.js"></script>
+ <link type="text/css" rel="stylesheet" href="/cube.css"/>
+ </head>
+ <body>
+ <div id="header"></div>
+ <div id="board"></div>
+ <script type="text/javascript">
+
+var id = parseInt(location.pathname.substring(1), 36),
+ mode = document.body.className = location.pathname.substring(8) || "view",
+ board = cube.board("ws://" + location.host + "/board", id),
+ header = cube.header(board);
+
+d3.select("#header").node().appendChild(header.node());
+d3.select("#board").node().appendChild(board.node());
+
+ </script>
+ </body>
+</html>
76 lib/cube/server/collectd.js
@@ -0,0 +1,76 @@
+exports.putter = function(putter) {
+ var values = {},
+ queue = [],
+ flushInterval,
+ flushDelay = 5000;
+
+ function store(value, i, event, name) {
+ var v1 = value.values[i];
+ switch (value.dstypes[i]) {
+ case "gauge": {
+ event[name] = v1;
+ break;
+ }
+ case "derive": {
+ var k = value.host
+ + "/" + value.plugin + "/" + value.plugin_instance
+ + "/" + value.type + "/" + value.type_instance
+ + "/" + name;
+ event[name] = k in values
+ ? -(values[k] - (values[k] = v1))
+ : (values[k] = v1, 0);
+ break;
+ }
+ }
+ }
+
+ flushInterval = setInterval(function() {
+ var hosts = {},
+ latest = Date.now() - 2 * flushDelay, // to coalesce
+ retries = [];
+
+ queue.forEach(function(value) {
+ if (value.time > latest) {
+ retries.push(value);
+ } else {
+ var host = hosts[value.host] || (hosts[value.host] = {}),
+ event = host[value.time] || (host[value.time] = {});
+ event = event[value.plugin] || (event[value.plugin] = {host: value.host});
+ if (value.plugin_instance) event = event[value.plugin_instance] || (event[value.plugin_instance] = {});
+ if (value.type != value.plugin) event = event[value.type] || (event[value.type] = {});
+ if (value.values.length == 1) store(value, 0, event, value.type_instance);
+ else value.values.forEach(function(d, i) { store(value, i, event, value.dsnames[i]); });
+ }
+ });
+
+ queue = retries;
+
+ for (var host in hosts) {
+ for (var time in hosts[host]) {
+ for (var type in hosts[host][time]) {
+ putter({
+ type: "collectd_" + type,
+ time: new Date(+time),
+ data: hosts[host][time][type]
+ });
+ }
+ }
+ }
+ }, flushDelay);
+
+ return function(request, response) {
+ var content = "";
+ request.on("data", function(chunk) {
+ content += chunk;
+ });
+ request.on("end", function() {
+ JSON.parse(content).forEach(function(value) {
+ value.time = Math.round(value.time / 1073741824) * 1000;
+ queue.push(value);
+ });
+ response.writeHead(200);
+ response.end();
+ });
+ };
+};
+
42 lib/cube/server/collector.js
@@ -0,0 +1,42 @@
+var options = require("../../../config/collector"),
+ server = require("./server")(options),
+ endpoint = require("./endpoint");
+
+server.register = function(db, endpoints) {
+ var putter = require("./event").putter(db);
+ endpoints.ws.push(
+ endpoint.exact("/1.0/event/put", putter)
+ );
+ endpoints.http.push(
+ endpoint.exact("POST", "/1.0/event/put", post(putter)),
+ endpoint.exact("POST", "/collectd", require("./collectd").putter(putter))
+ );
+};
+
+function post(putter) {
+ return function(request, response) {
+ var content = "";
+ request.on("data", function(chunk) {
+ content += chunk;
+ });
+ request.on("end", function() {
+ try {
+ JSON.parse(content).forEach(putter);
+ } catch (e) {
+ util.log(e);
+ response.writeHead(400, {
+ "Content-Type": "application/json",
+ "Access-Control-Allow-Origin": "*"
+ });
+ return response.end("{\"status\":400}");
+ }
+ response.writeHead(200, {
+ "Content-Type": "application/json",
+ "Access-Control-Allow-Origin": "*"
+ });
+ response.end("{\"status\":200}");
+ });
+ };
+}
+
+server.start();
67 lib/cube/server/emitter.js
@@ -0,0 +1,67 @@
+var util = require("util"),
+ WebSocket = require("websocket-client").WebSocket;
+
+module.exports = function() {
+ var emitter = {},
+ queue = [],
+ url,
+ socket,
+ timeout;
+
+ function close() {
+ if (socket) {
+ util.log("closing socket");
+ socket.onclose = null;
+ socket.close();
+ socket = null;
+ }
+ }
+
+ function open() {
+ timeout = 0;
+ close();
+ util.log("opening socket: " + url);
+ socket = new WebSocket(url);
+ socket.onopen = flush;
+ socket.onclose = reopen;
+ }
+
+ function reopen() {
+ if (!timeout) {
+ util.log("reopening soon");
+ timeout = setTimeout(open, 1000);
+ }
+ }
+
+ function flush() {
+ var event;
+ while (event = queue.pop()) {
+ try {
+ socket.send(JSON.stringify(event));
+ } catch (e) {
+ util.log(e.stack);
+ reopen();
+ return queue.push(event);
+ }
+ }
+ }
+
+ emitter.open = function(host, port) {
+ url = "ws://" + host + ":" + port + "/1.0/event/put";
+ open();
+ return emitter;
+ };
+
+ emitter.send = function(event) {
+ queue.push(event);
+ flush();
+ return emitter;
+ };
+
+ emitter.close = function() {
+ close();
+ return emitter;
+ };
+
+ return emitter;
+};
83 lib/cube/server/endpoint.js
@@ -0,0 +1,83 @@
+var fs = require("fs"),
+ url = require("url"),
+ path = require("path");
+
+exports.re = re;
+exports.exact = exact;
+exports.file = file;
+
+var types = {
+ html: "text/html",
+ css: "text/css",
+ js: "text/javascript"
+};
+
+function exact(method, path, dispatch) {
+ return {
+ match: arguments.length < 3
+ ? (dispatch = path, path = method, function(p) { return p == path; })
+ : function(p, m) { return m == method && p == path; },
+ dispatch: dispatch
+ };
+}
+
+function re(re, dispatch) {
+ return {
+ match: function(p) { return re.test(p); },
+ dispatch: dispatch
+ };
+}
+
+function file() {
+ var files = Array.prototype.map.call(arguments, resolve),
+ type = types[files[0].substring(files[0].lastIndexOf(".") + 1)];
+ return function(request, response) {
+ var modified = -Infinity,
+ size = 0,
+ n = files.length;
+
+ files.forEach(function(file) {
+ fs.stat(file, function(error, stats) {
+ if (error) return fiveohoh(request, response);
+ size += stats.size;
+ var time = new Date(stats.mtime);
+ if (time > modified) modified = time;
+ if (!--n) respond();
+ });
+ });
+
+ function respond() {
+ var status = modified <= new Date(request.headers["if-modified-since"]) ? 304 : 200;
+
+ response.writeHead(status, {
+ "Content-Type": type + ";charset=utf-8",
+ "Content-Length": size,
+ "Last-Modified": modified.toUTCString()
+ });
+
+ if ((status === 200) && (request.method !== "HEAD")) {
+ return read(0);
+ }
+
+ return response.end();
+ }
+
+ function read(i) {
+ fs.readFile(files[i], "UTF-8", function(error, data) {
+ if (error) return fiveohoh(request, response);
+ response.write(data);
+ if (i < files.length - 1) read(i + 1);
+ else response.end();
+ });
+ }
+ };
+};
+
+function resolve(name) {
+ return path.join(process.cwd(), name);
+}
+
+function fiveohoh(request, response) {
+ response.writeHead(500, {"Content-Type": "text/plain"});
+ response.end("500 Server Error");
+}
14 lib/cube/server/evaluator.js
@@ -0,0 +1,14 @@
+var options = require("../../../config/evaluator"),
+ server = require("./server")(options),
+ endpoint = require("./endpoint"),
+ visualizer = require("./visualizer");
+
+server.register = function(db, endpoints) {
+ endpoints.ws.push(
+ endpoint.exact("/1.0/event/get", require("./event").getter(db)),
+ endpoint.exact("/1.0/metric/get", require("./metric").getter(db))
+ );
+ visualizer.register(db, endpoints);
+};
+
+server.start();
2,809 lib/cube/server/event-expression.js
@@ -0,0 +1,2809 @@
+module.exports = (function(){
+ /* Generated by PEG.js 0.6.2 (http://pegjs.majda.cz/). */
+
+ var result = {
+ /*
+ * Parses the input with a generated parser. If the parsing is successfull,
+ * returns a value explicitly or implicitly specified by the grammar from
+ * which the parser was generated (see |PEG.buildParser|). If the parsing is
+ * unsuccessful, throws |PEG.parser.SyntaxError| describing the error.
+ */
+ parse: function(input, startRule) {
+ var parseFunctions = {
+ "_": parse__,
+ "character_escape_sequence": parse_character_escape_sequence,
+ "digit": parse_digit,
+ "digit19": parse_digit19,
+ "digits": parse_digits,
+ "double_string_char": parse_double_string_char,
+ "e": parse_e,
+ "escape_character": parse_escape_character,
+ "escape_sequence": parse_escape_sequence,
+ "event_expression": parse_event_expression,
+ "event_filter_expression": parse_event_filter_expression,
+ "event_member_expression": parse_event_member_expression,
+ "event_value_expression": parse_event_value_expression,
+ "exp": parse_exp,
+ "filter_operator": parse_filter_operator,
+ "frac": parse_frac,
+ "hex_digit": parse_hex_digit,
+ "hex_escape_sequence": parse_hex_escape_sequence,
+ "identifier": parse_identifier,
+ "int": parse_int,
+ "literal": parse_literal,
+ "non_escape_character": parse_non_escape_character,
+ "number": parse_number,
+ "single_escape_character": parse_single_escape_character,
+ "single_string_char": parse_single_string_char,
+ "start": parse_start,
+ "string": parse_string,
+ "unicode_escape_sequence": parse_unicode_escape_sequence,
+ "whitespace": parse_whitespace
+ };
+
+ if (startRule !== undefined) {
+ if (parseFunctions[startRule] === undefined) {
+ throw new Error("Invalid rule name: " + quote(startRule) + ".");
+ }
+ } else {
+ startRule = "start";
+ }
+
+ var pos = 0;
+ var reportMatchFailures = true;
+ var rightmostMatchFailuresPos = 0;
+ var rightmostMatchFailuresExpected = [];
+ var cache = {};
+
+ function padLeft(input, padding, length) {
+ var result = input;
+
+ var padLength = length - input.length;
+ for (var i = 0; i < padLength; i++) {
+ result = padding + result;
+ }
+
+ return result;
+ }
+
+ function escape(ch) {
+ var charCode = ch.charCodeAt(0);
+
+ if (charCode <= 0xFF) {
+ var escapeChar = 'x';
+ var length = 2;
+ } else {
+ var escapeChar = 'u';
+ var length = 4;
+ }
+
+ return '\\' + escapeChar + padLeft(charCode.toString(16).toUpperCase(), '0', length);
+ }
+
+ function quote(s) {
+ /*
+ * ECMA-262, 5th ed., 7.8.4: All characters may appear literally in a
+ * string literal except for the closing quote character, backslash,
+ * carriage return, line separator, paragraph separator, and line feed.
+ * Any character may appear in the form of an escape sequence.
+ */
+ return '"' + s
+ .replace(/\\/g, '\\\\') // backslash
+ .replace(/"/g, '\\"') // closing quote character
+ .replace(/\r/g, '\\r') // carriage return
+ .replace(/\n/g, '\\n') // line feed
+ .replace(/[\x80-\uFFFF]/g, escape) // non-ASCII characters
+ + '"';
+ }
+
+ function matchFailed(failure) {
+ if (pos < rightmostMatchFailuresPos) {
+ return;
+ }
+
+ if (pos > rightmostMatchFailuresPos) {
+ rightmostMatchFailuresPos = pos;
+ rightmostMatchFailuresExpected = [];
+ }
+
+ rightmostMatchFailuresExpected.push(failure);
+ }
+
+ function parse_start() {
+ var cacheKey = 'start@' + pos;
+ var cachedResult = cache[cacheKey];
+ if (cachedResult) {
+ pos = cachedResult.nextPos;
+ return cachedResult.result;
+ }
+
+
+ var savedPos0 = pos;
+ var savedPos1 = pos;
+ var result3 = parse__();
+ if (result3 !== null) {
+ var result4 = parse_event_expression();
+ if (result4 !== null) {
+ var result5 = parse__();
+ if (result5 !== null) {
+ var result1 = [result3, result4, result5];
+ } else {
+ var result1 = null;
+ pos = savedPos1;
+ }
+ } else {
+ var result1 = null;
+ pos = savedPos1;
+ }
+ } else {
+ var result1 = null;
+ pos = savedPos1;
+ }
+ var result2 = result1 !== null
+ ? (function(expression) { return expression; })(result1[1])
+ : null;
+ if (result2 !== null) {
+ var result0 = result2;
+ } else {
+ var result0 = null;
+ pos = savedPos0;
+ }
+
+
+
+ cache[cacheKey] = {
+ nextPos: pos,
+ result: result0
+ };
+ return result0;
+ }
+
+ function parse_event_expression() {
+ var cacheKey = 'event_expression@' + pos;
+ var cachedResult = cache[cacheKey];
+ if (cachedResult) {
+ pos = cachedResult.nextPos;
+ return cachedResult.result;
+ }
+
+
+ var savedPos0 = pos;
+ var savedPos1 = pos;
+ var result3 = parse_event_value_expression();
+ if (result3 !== null) {
+ var result4 = [];
+ var savedPos2 = pos;
+ var result6 = parse__();
+ if (result6 !== null) {
+ if (input.substr(pos, 1) === ".") {
+ var result7 = ".";
+ pos += 1;
+ } else {
+ var result7 = null;
+ if (reportMatchFailures) {
+ matchFailed("\".\"");
+ }
+ }
+ if (result7 !== null) {
+ var result8 = parse__();
+ if (result8 !== null) {
+ var result9 = parse_event_filter_expression();
+ if (result9 !== null) {
+ var result5 = [result6, result7, result8, result9];
+ } else {
+ var result5 = null;
+ pos = savedPos2;
+ }
+ } else {
+ var result5 = null;
+ pos = savedPos2;
+ }
+ } else {
+ var result5 = null;
+ pos = savedPos2;
+ }
+ } else {
+ var result5 = null;
+ pos = savedPos2;
+ }
+ while (result5 !== null) {
+ result4.push(result5);
+ var savedPos2 = pos;
+ var result6 = parse__();
+ if (result6 !== null) {
+ if (input.substr(pos, 1) === ".") {
+ var result7 = ".";
+ pos += 1;
+ } else {
+ var result7 = null;
+ if (reportMatchFailures) {
+ matchFailed("\".\"");
+ }
+ }
+ if (result7 !== null) {
+ var result8 = parse__();
+ if (result8 !== null) {
+ var result9 = parse_event_filter_expression();
+ if (result9 !== null) {
+ var result5 = [result6, result7, result8, result9];
+ } else {
+ var result5 = null;
+ pos = savedPos2;
+ }
+ } else {
+ var result5 = null;
+ pos = savedPos2;
+ }
+ } else {
+ var result5 = null;
+ pos = savedPos2;
+ }
+ } else {
+ var result5 = null;
+ pos = savedPos2;
+ }
+ }
+ if (result4 !== null) {
+ var result1 = [result3, result4];
+ } else {
+ var result1 = null;
+ pos = savedPos1;
+ }
+ } else {
+ var result1 = null;
+ pos = savedPos1;
+ }
+ var result2 = result1 !== null
+ ? (function(value, filters) {
+ value.filter = function(filter) {
+ var i = -1, n = filters.length;
+ while (++i < n) filters[i][3](filter);
+ value.exists(filter);
+ };
+ return value;
+ })(result1[0], result1[1])
+ : null;
+ if (result2 !== null) {
+ var result0 = result2;
+ } else {
+ var result0 = null;
+ pos = savedPos0;
+ }
+
+
+
+ cache[cacheKey] = {
+ nextPos: pos,
+ result: result0
+ };
+ return result0;
+ }
+
+ function parse_event_filter_expression() {
+ var cacheKey = 'event_filter_expression@' + pos;
+ var cachedResult = cache[cacheKey];
+ if (cachedResult) {
+ pos = cachedResult.nextPos;
+ return cachedResult.result;
+ }
+
+
+ var savedPos0 = pos;
+ var savedPos1 = pos;
+ var result3 = parse_filter_operator();
+ if (result3 !== null) {
+ var result4 = parse__();
+ if (result4 !== null) {
+ if (input.substr(pos, 1) === "(") {
+ var result5 = "(";
+ pos += 1;
+ } else {
+ var result5 = null;
+ if (reportMatchFailures) {
+ matchFailed("\"(\"");
+ }
+ }
+ if (result5 !== null) {
+ var result6 = parse__();
+ if (result6 !== null) {
+ var result7 = parse_event_member_expression();
+ if (result7 !== null) {
+ var result8 = parse__();
+ if (result8 !== null) {
+ if (input.substr(pos, 1) === ",") {
+ var result9 = ",";
+ pos += 1;
+ } else {
+ var result9 = null;
+ if (reportMatchFailures) {
+ matchFailed("\",\"");
+ }
+ }
+ if (result9 !== null) {
+ var result10 = parse__();
+ if (result10 !== null) {
+ var result11 = parse_literal();
+ if (result11 !== null) {
+ var result12 = parse__();
+ if (result12 !== null) {
+ if (input.substr(pos, 1) === ")") {
+ var result13 = ")";
+ pos += 1;
+ } else {
+ var result13 = null;
+ if (reportMatchFailures) {
+ matchFailed("\")\"");
+ }
+ }
+ if (result13 !== null) {
+ var result1 = [result3, result4, result5, result6, result7, result8, result9, result10, result11, result12, result13];
+ } else {
+ var result1 = null;
+ pos = savedPos1;
+ }
+ } else {
+ var result1 = null;
+ pos = savedPos1;
+ }
+ } else {
+ var result1 = null;
+ pos = savedPos1;
+ }
+ } else {
+ var result1 = null;
+ pos = savedPos1;
+ }
+ } else {
+ var result1 = null;
+ pos = savedPos1;
+ }
+ } else {
+ var result1 = null;
+ pos = savedPos1;
+ }
+ } else {
+ var result1 = null;
+ pos = savedPos1;
+ }
+ } else {
+ var result1 = null;
+ pos = savedPos1;
+ }
+ } else {
+ var result1 = null;
+ pos = savedPos1;
+ }
+ } else {
+ var result1 = null;
+ pos = savedPos1;
+ }
+ } else {
+ var result1 = null;
+ pos = savedPos1;
+ }
+ var result2 = result1 !== null
+ ? (function(op, member, value) { return function(o) { op(o, member.field, value); }; })(result1[0], result1[4], result1[8])
+ : null;
+ if (result2 !== null) {
+ var result0 = result2;
+ } else {
+ var result0 = null;
+ pos = savedPos0;
+ }
+
+
+
+ cache[cacheKey] = {
+ nextPos: pos,
+ result: result0
+ };
+ return result0;
+ }
+
+ function parse_event_value_expression() {
+ var cacheKey = 'event_value_expression@' + pos;
+ var cachedResult = cache[cacheKey];
+ if (cachedResult) {
+ pos = cachedResult.nextPos;
+ return cachedResult.result;
+ }
+
+
+ var savedPos1 = pos;
+ var savedPos2 = pos;
+ var result7 = parse_identifier();
+ if (result7 !== null) {
+ var result8 = parse__();
+ if (result8 !== null) {
+ if (input.substr(pos, 1) === "(") {
+ var result9 = "(";
+ pos += 1;
+ } else {
+ var result9 = null;
+ if (reportMatchFailures) {
+ matchFailed("\"(\"");
+ }
+ }
+ if (result9 !== null) {
+ var result10 = parse__();
+ if (result10 !== null) {
+ var result11 = parse_event_member_expression();
+ if (result11 !== null) {
+ var result12 = [];
+ var savedPos3 = pos;
+ var result16 = parse__();
+ if (result16 !== null) {
+ if (input.substr(pos, 1) === ",") {
+ var result17 = ",";
+ pos += 1;
+ } else {
+ var result17 = null;
+ if (reportMatchFailures) {
+ matchFailed("\",\"");
+ }
+ }
+ if (result17 !== null) {