pulse-meter 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +19 -0
- data/.rbenv-version +1 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/Procfile +3 -0
- data/README.md +440 -0
- data/Rakefile +53 -0
- data/bin/pulse +6 -0
- data/examples/basic.ru +109 -0
- data/examples/basic_sensor_data.rb +38 -0
- data/examples/full/Procfile +2 -0
- data/examples/full/client.rb +82 -0
- data/examples/full/server.ru +114 -0
- data/examples/minimal/Procfile +2 -0
- data/examples/minimal/client.rb +16 -0
- data/examples/minimal/server.ru +20 -0
- data/examples/readme_client_example.rb +52 -0
- data/lib/cmd.rb +150 -0
- data/lib/pulse-meter.rb +17 -0
- data/lib/pulse-meter/mixins/dumper.rb +72 -0
- data/lib/pulse-meter/mixins/utils.rb +91 -0
- data/lib/pulse-meter/sensor.rb +44 -0
- data/lib/pulse-meter/sensor/base.rb +75 -0
- data/lib/pulse-meter/sensor/counter.rb +36 -0
- data/lib/pulse-meter/sensor/hashed_counter.rb +31 -0
- data/lib/pulse-meter/sensor/indicator.rb +33 -0
- data/lib/pulse-meter/sensor/timeline.rb +180 -0
- data/lib/pulse-meter/sensor/timelined/average.rb +26 -0
- data/lib/pulse-meter/sensor/timelined/counter.rb +16 -0
- data/lib/pulse-meter/sensor/timelined/hashed_counter.rb +22 -0
- data/lib/pulse-meter/sensor/timelined/max.rb +25 -0
- data/lib/pulse-meter/sensor/timelined/median.rb +14 -0
- data/lib/pulse-meter/sensor/timelined/min.rb +25 -0
- data/lib/pulse-meter/sensor/timelined/percentile.rb +31 -0
- data/lib/pulse-meter/version.rb +3 -0
- data/lib/pulse-meter/visualize/app.rb +43 -0
- data/lib/pulse-meter/visualize/dsl.rb +0 -0
- data/lib/pulse-meter/visualize/dsl/errors.rb +46 -0
- data/lib/pulse-meter/visualize/dsl/layout.rb +55 -0
- data/lib/pulse-meter/visualize/dsl/page.rb +50 -0
- data/lib/pulse-meter/visualize/dsl/sensor.rb +21 -0
- data/lib/pulse-meter/visualize/dsl/widget.rb +84 -0
- data/lib/pulse-meter/visualize/layout.rb +54 -0
- data/lib/pulse-meter/visualize/page.rb +30 -0
- data/lib/pulse-meter/visualize/public/css/application.css +19 -0
- data/lib/pulse-meter/visualize/public/css/bootstrap.css +4883 -0
- data/lib/pulse-meter/visualize/public/css/bootstrap.min.css +729 -0
- data/lib/pulse-meter/visualize/public/favicon.ico +0 -0
- data/lib/pulse-meter/visualize/public/img/glyphicons-halflings-white.png +0 -0
- data/lib/pulse-meter/visualize/public/img/glyphicons-halflings.png +0 -0
- data/lib/pulse-meter/visualize/public/js/application.coffee +262 -0
- data/lib/pulse-meter/visualize/public/js/application.js +279 -0
- data/lib/pulse-meter/visualize/public/js/backbone-min.js +38 -0
- data/lib/pulse-meter/visualize/public/js/bootstrap.js +1835 -0
- data/lib/pulse-meter/visualize/public/js/highcharts.js +203 -0
- data/lib/pulse-meter/visualize/public/js/jquery-1.7.2.min.js +4 -0
- data/lib/pulse-meter/visualize/public/js/json2.js +487 -0
- data/lib/pulse-meter/visualize/public/js/underscore-min.js +32 -0
- data/lib/pulse-meter/visualize/sensor.rb +60 -0
- data/lib/pulse-meter/visualize/views/main.haml +40 -0
- data/lib/pulse-meter/visualize/widget.rb +68 -0
- data/lib/pulse-meter/visualizer.rb +30 -0
- data/lib/test_helpers/matchers.rb +36 -0
- data/pulse-meter.gemspec +39 -0
- data/spec/pulse_meter/mixins/dumper_spec.rb +158 -0
- data/spec/pulse_meter/mixins/utils_spec.rb +134 -0
- data/spec/pulse_meter/sensor/base_spec.rb +97 -0
- data/spec/pulse_meter/sensor/counter_spec.rb +54 -0
- data/spec/pulse_meter/sensor/hashed_counter_spec.rb +39 -0
- data/spec/pulse_meter/sensor/indicator_spec.rb +43 -0
- data/spec/pulse_meter/sensor/timeline_spec.rb +58 -0
- data/spec/pulse_meter/sensor/timelined/average_spec.rb +6 -0
- data/spec/pulse_meter/sensor/timelined/counter_spec.rb +6 -0
- data/spec/pulse_meter/sensor/timelined/hashed_counter_spec.rb +8 -0
- data/spec/pulse_meter/sensor/timelined/max_spec.rb +7 -0
- data/spec/pulse_meter/sensor/timelined/median_spec.rb +7 -0
- data/spec/pulse_meter/sensor/timelined/min_spec.rb +7 -0
- data/spec/pulse_meter/sensor/timelined/percentile_spec.rb +17 -0
- data/spec/pulse_meter/visualize/app_spec.rb +27 -0
- data/spec/pulse_meter/visualize/dsl/layout_spec.rb +64 -0
- data/spec/pulse_meter/visualize/dsl/page_spec.rb +75 -0
- data/spec/pulse_meter/visualize/dsl/sensor_spec.rb +30 -0
- data/spec/pulse_meter/visualize/dsl/widget_spec.rb +127 -0
- data/spec/pulse_meter/visualize/layout_spec.rb +55 -0
- data/spec/pulse_meter/visualize/page_spec.rb +150 -0
- data/spec/pulse_meter/visualize/sensor_spec.rb +120 -0
- data/spec/pulse_meter/visualize/widget_spec.rb +113 -0
- data/spec/pulse_meter/visualizer_spec.rb +42 -0
- data/spec/pulse_meter_spec.rb +16 -0
- data/spec/shared_examples/timeline_sensor.rb +279 -0
- data/spec/shared_examples/timelined_subclass.rb +23 -0
- data/spec/spec_helper.rb +29 -0
- metadata +435 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
// Underscore.js 1.3.3
|
2
|
+
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
3
|
+
// Underscore is freely distributable under the MIT license.
|
4
|
+
// Portions of Underscore are inspired or borrowed from Prototype,
|
5
|
+
// Oliver Steele's Functional, and John Resig's Micro-Templating.
|
6
|
+
// For all details and documentation:
|
7
|
+
// http://documentcloud.github.com/underscore
|
8
|
+
(function(){function r(a,c,d){if(a===c)return 0!==a||1/a==1/c;if(null==a||null==c)return a===c;a._chain&&(a=a._wrapped);c._chain&&(c=c._wrapped);if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return!1;switch(e){case "[object String]":return a==""+c;case "[object Number]":return a!=+a?c!=+c:0==a?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source==
|
9
|
+
c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if("object"!=typeof a||"object"!=typeof c)return!1;for(var f=d.length;f--;)if(d[f]==a)return!0;d.push(a);var f=0,g=!0;if("[object Array]"==e){if(f=a.length,g=f==c.length)for(;f--&&(g=f in a==f in c&&r(a[f],c[f],d)););}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return!1;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&r(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c,h)&&!f--)break;
|
10
|
+
g=!f}}d.pop();return g}var s=this,I=s._,o={},k=Array.prototype,p=Object.prototype,i=k.slice,J=k.unshift,l=p.toString,K=p.hasOwnProperty,y=k.forEach,z=k.map,A=k.reduce,B=k.reduceRight,C=k.filter,D=k.every,E=k.some,q=k.indexOf,F=k.lastIndexOf,p=Array.isArray,L=Object.keys,t=Function.prototype.bind,b=function(a){return new m(a)};"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(exports=module.exports=b),exports._=b):s._=b;b.VERSION="1.3.3";var j=b.each=b.forEach=function(a,
|
11
|
+
c,d){if(a!=null)if(y&&a.forEach===y)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e<f;e++){if(e in a&&c.call(d,a[e],e,a)===o)break}else for(e in a)if(b.has(a,e)&&c.call(d,a[e],e,a)===o)break};b.map=b.collect=function(a,c,b){var e=[];if(a==null)return e;if(z&&a.map===z)return a.map(c,b);j(a,function(a,g,h){e[e.length]=c.call(b,a,g,h)});if(a.length===+a.length)e.length=a.length;return e};b.reduce=b.foldl=b.inject=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(A&&
|
12
|
+
a.reduce===A){e&&(c=b.bind(c,e));return f?a.reduce(c,d):a.reduce(c)}j(a,function(a,b,i){if(f)d=c.call(e,d,a,b,i);else{d=a;f=true}});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(B&&a.reduceRight===B){e&&(c=b.bind(c,e));return f?a.reduceRight(c,d):a.reduceRight(c)}var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect=function(a,
|
13
|
+
c,b){var e;G(a,function(a,g,h){if(c.call(b,a,g,h)){e=a;return true}});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(C&&a.filter===C)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(D&&a.every===D)return a.every(c,b);j(a,function(a,g,h){if(!(e=e&&c.call(b,
|
14
|
+
a,g,h)))return o});return!!e};var G=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(E&&a.some===E)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return o});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;if(q&&a.indexOf===q)return a.indexOf(c)!=-1;return b=G(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck=
|
15
|
+
function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b<e.computed&&
|
16
|
+
(e={value:a,computed:b})});return e.value};b.shuffle=function(a){var b=[],d;j(a,function(a,f){d=Math.floor(Math.random()*(f+1));b[f]=b[d];b[d]=a});return b};b.sortBy=function(a,c,d){var e=b.isFunction(c)?c:function(a){return a[c]};return b.pluck(b.map(a,function(a,b,c){return{value:a,criteria:e.call(d,a,b,c)}}).sort(function(a,b){var c=a.criteria,d=b.criteria;return c===void 0?1:d===void 0?-1:c<d?-1:c>d?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};
|
17
|
+
j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(c)?e=g+1:f=g}return e};b.toArray=function(a){return!a?[]:b.isArray(a)||b.isArguments(a)?i.call(a):a.toArray&&b.isFunction(a.toArray)?a.toArray():b.values(a)};b.size=function(a){return b.isArray(a)?a.length:b.keys(a).length};b.first=b.head=b.take=function(a,b,d){return b!=null&&!d?i.call(a,0,b):a[0]};b.initial=function(a,b,d){return i.call(a,
|
18
|
+
0,a.length-(b==null||d?1:b))};b.last=function(a,b,d){return b!=null&&!d?i.call(a,Math.max(a.length-b,0)):a[a.length-1]};b.rest=b.tail=function(a,b,d){return i.call(a,b==null||d?1:b)};b.compact=function(a){return b.filter(a,function(a){return!!a})};b.flatten=function(a,c){return b.reduce(a,function(a,e){if(b.isArray(e))return a.concat(c?e:b.flatten(e));a[a.length]=e;return a},[])};b.without=function(a){return b.difference(a,i.call(arguments,1))};b.uniq=b.unique=function(a,c,d){var d=d?b.map(a,d):a,
|
19
|
+
e=[];a.length<3&&(c=true);b.reduce(d,function(d,g,h){if(c?b.last(d)!==g||!d.length:!b.include(d,g)){d.push(g);e.push(a[h])}return d},[]);return e};b.union=function(){return b.uniq(b.flatten(arguments,true))};b.intersection=b.intersect=function(a){var c=i.call(arguments,1);return b.filter(b.uniq(a),function(a){return b.every(c,function(c){return b.indexOf(c,a)>=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1),true);return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=
|
20
|
+
i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e<c;e++)d[e]=b.pluck(a,""+e);return d};b.indexOf=function(a,c,d){if(a==null)return-1;var e;if(d){d=b.sortedIndex(a,c);return a[d]===c?d:-1}if(q&&a.indexOf===q)return a.indexOf(c);d=0;for(e=a.length;d<e;d++)if(d in a&&a[d]===c)return d;return-1};b.lastIndexOf=function(a,b){if(a==null)return-1;if(F&&a.lastIndexOf===F)return a.lastIndexOf(b);for(var d=a.length;d--;)if(d in a&&a[d]===b)return d;return-1};b.range=function(a,b,d){if(arguments.length<=
|
21
|
+
1){b=a||0;a=0}for(var d=arguments[2]||1,e=Math.max(Math.ceil((b-a)/d),0),f=0,g=Array(e);f<e;){g[f++]=a;a=a+d}return g};var H=function(){};b.bind=function(a,c){var d,e;if(a.bind===t&&t)return t.apply(a,i.call(arguments,1));if(!b.isFunction(a))throw new TypeError;e=i.call(arguments,2);return d=function(){if(!(this instanceof d))return a.apply(c,e.concat(i.call(arguments)));H.prototype=a.prototype;var b=new H,g=a.apply(b,e.concat(i.call(arguments)));return Object(g)===g?g:b}};b.bindAll=function(a){var c=
|
22
|
+
i.call(arguments,1);c.length==0&&(c=b.functions(a));j(c,function(c){a[c]=b.bind(a[c],a)});return a};b.memoize=function(a,c){var d={};c||(c=b.identity);return function(){var e=c.apply(this,arguments);return b.has(d,e)?d[e]:d[e]=a.apply(this,arguments)}};b.delay=function(a,b){var d=i.call(arguments,2);return setTimeout(function(){return a.apply(null,d)},b)};b.defer=function(a){return b.delay.apply(b,[a,1].concat(i.call(arguments,1)))};b.throttle=function(a,c){var d,e,f,g,h,i,j=b.debounce(function(){h=
|
23
|
+
g=false},c);return function(){d=this;e=arguments;f||(f=setTimeout(function(){f=null;h&&a.apply(d,e);j()},c));g?h=true:i=a.apply(d,e);j();g=true;return i}};b.debounce=function(a,b,d){var e;return function(){var f=this,g=arguments;d&&!e&&a.apply(f,g);clearTimeout(e);e=setTimeout(function(){e=null;d||a.apply(f,g)},b)}};b.once=function(a){var b=false,d;return function(){if(b)return d;b=true;return d=a.apply(this,arguments)}};b.wrap=function(a,b){return function(){var d=[a].concat(i.call(arguments,0));
|
24
|
+
return b.apply(this,d)}};b.compose=function(){var a=arguments;return function(){for(var b=arguments,d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=L||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&
|
25
|
+
c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.pick=function(a){var c={};j(b.flatten(i.call(arguments,1)),function(b){b in a&&(c[b]=a[b])});return c};b.defaults=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return r(a,b,[])};b.isEmpty=
|
26
|
+
function(a){if(a==null)return true;if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=p||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};b.isArguments=function(a){return l.call(a)=="[object Arguments]"};b.isArguments(arguments)||(b.isArguments=function(a){return!(!a||!b.has(a,"callee"))});b.isFunction=function(a){return l.call(a)=="[object Function]"};
|
27
|
+
b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isFinite=function(a){return b.isNumber(a)&&isFinite(a)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,
|
28
|
+
b){return K.call(a,b)};b.noConflict=function(){s._=I;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e<a;e++)b.call(d,e)};b.escape=function(a){return(""+a).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.result=function(a,c){if(a==null)return null;var d=a[c];return b.isFunction(d)?d.call(a):d};b.mixin=function(a){j(b.functions(a),function(c){M(c,b[c]=a[c])})};var N=0;b.uniqueId=
|
29
|
+
function(a){var b=N++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var u=/.^/,n={"\\":"\\","'":"'",r:"\r",n:"\n",t:"\t",u2028:"\u2028",u2029:"\u2029"},v;for(v in n)n[n[v]]=v;var O=/\\|'|\r|\n|\t|\u2028|\u2029/g,P=/\\(\\|'|r|n|t|u2028|u2029)/g,w=function(a){return a.replace(P,function(a,b){return n[b]})};b.template=function(a,c,d){d=b.defaults(d||{},b.templateSettings);a="__p+='"+a.replace(O,function(a){return"\\"+n[a]}).replace(d.escape||
|
30
|
+
u,function(a,b){return"'+\n_.escape("+w(b)+")+\n'"}).replace(d.interpolate||u,function(a,b){return"'+\n("+w(b)+")+\n'"}).replace(d.evaluate||u,function(a,b){return"';\n"+w(b)+"\n;__p+='"})+"';\n";d.variable||(a="with(obj||{}){\n"+a+"}\n");var a="var __p='';var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n"+a+"return __p;\n",e=new Function(d.variable||"obj","_",a);if(c)return e(c,b);c=function(a){return e.call(this,a,b)};c.source="function("+(d.variable||"obj")+"){\n"+a+"}";return c};
|
31
|
+
b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var x=function(a,c){return c?b(a).chain():a},M=function(a,c){m.prototype[a]=function(){var a=i.call(arguments);J.call(a,this._wrapped);return x(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return x(d,
|
32
|
+
this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return x(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=true;return this};m.prototype.value=function(){return this._wrapped}}).call(this);
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module PulseMeter
|
2
|
+
module Visualize
|
3
|
+
class Sensor
|
4
|
+
attr_reader :name
|
5
|
+
attr_reader :color
|
6
|
+
|
7
|
+
def initialize(args)
|
8
|
+
raise ArgumentError unless args.respond_to?('[]')
|
9
|
+
@name = args[:sensor] or raise ArgumentError, ":sensor_name not specified"
|
10
|
+
@color = args[:color]
|
11
|
+
end
|
12
|
+
|
13
|
+
def last_value(need_incomplete=false)
|
14
|
+
sensor = real_sensor
|
15
|
+
|
16
|
+
sensor_data = if need_incomplete
|
17
|
+
sensor.timeline(sensor.interval).first
|
18
|
+
else
|
19
|
+
sensor.timeline(sensor.interval * 2).first
|
20
|
+
end
|
21
|
+
|
22
|
+
if sensor_data.is_a?(PulseMeter::SensorData)
|
23
|
+
sensor_data.value
|
24
|
+
else
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def last_point_data(need_incomplete=false)
|
30
|
+
res = {
|
31
|
+
name: real_sensor.annotation,
|
32
|
+
y: last_value(need_incomplete)
|
33
|
+
}
|
34
|
+
res[:color] = color if color
|
35
|
+
res
|
36
|
+
end
|
37
|
+
|
38
|
+
def timeline_data(time_span, need_incomplete = false)
|
39
|
+
sensor = real_sensor
|
40
|
+
data = sensor.timeline(time_span).map{|sd| {x: sd.start_time.to_i*1000, y: sd.value}}
|
41
|
+
data.pop unless need_incomplete
|
42
|
+
res = {
|
43
|
+
name: sensor.annotation,
|
44
|
+
data: data
|
45
|
+
}
|
46
|
+
res[:color] = color if color
|
47
|
+
res
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
|
52
|
+
def real_sensor
|
53
|
+
# TODO add !temporarily! caching if this will be called too frequently
|
54
|
+
PulseMeter::Sensor::Base.restore(@name)
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
!!!
|
2
|
+
%head
|
3
|
+
%title= @title
|
4
|
+
:javascript
|
5
|
+
var ROOT = "#{url('/')}";
|
6
|
+
= include_gon
|
7
|
+
- %w{jquery-1.7.2.min.js json2.js underscore-min.js backbone-min.js highcharts.js application.js bootstrap.js}.each do |jsfile|
|
8
|
+
%script{:type => 'text/javascript', :src => url("/js/#{jsfile}")}
|
9
|
+
- %w{bootstrap.min.css application.css}.each do |cssfile|
|
10
|
+
%link{:rel => 'stylesheet', :href => url("/css/#{cssfile}"), :type => 'text/css', :media => 'screen'}
|
11
|
+
|
12
|
+
%body
|
13
|
+
%script#widget-template{type: 'text/template'}
|
14
|
+
#plotarea
|
15
|
+
#chart-controls
|
16
|
+
.form-inline
|
17
|
+
%span#refresh.btn.btn-mini
|
18
|
+
%i.icon-refresh
|
19
|
+
Refresh
|
20
|
+
%span.space
|
21
|
+
Cutoff min:
|
22
|
+
%input.btn-mini#cutoff-min
|
23
|
+
%span.space
|
24
|
+
Cutoff max:
|
25
|
+
%input.btn-mini#cutoff-max
|
26
|
+
%span.space
|
27
|
+
Refresh:
|
28
|
+
%input.btn-mini#need-refresh{type: :checkbox, checked: :true}
|
29
|
+
|
30
|
+
%hr
|
31
|
+
|
32
|
+
.container#main
|
33
|
+
.row
|
34
|
+
.span10.offset1
|
35
|
+
.navbar
|
36
|
+
.navbar-inner
|
37
|
+
.container
|
38
|
+
%span.brand= @title
|
39
|
+
%ul.nav#page-titles
|
40
|
+
#widgets.row
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module PulseMeter
|
2
|
+
module Visualize
|
3
|
+
class Widget
|
4
|
+
attr_reader :sensors
|
5
|
+
attr_reader :title
|
6
|
+
attr_reader :type
|
7
|
+
attr_reader :width
|
8
|
+
attr_reader :values_label
|
9
|
+
attr_reader :show_last_point
|
10
|
+
attr_reader :redraw_interval
|
11
|
+
attr_reader :timespan
|
12
|
+
|
13
|
+
def initialize(args)
|
14
|
+
raise ArgumentError unless args.respond_to?('[]')
|
15
|
+
@title = args[:title] or raise ArgumentError, ":title not specified"
|
16
|
+
@sensors = args[:sensors] or raise ArgumentError, ":sensors not specified"
|
17
|
+
@type = args[:type] or raise ArgumentError, ":type not specified"
|
18
|
+
@width = args[:width]
|
19
|
+
@values_label = args[:values_label]
|
20
|
+
@show_last_point = args[:show_last_point] || false
|
21
|
+
@redraw_interval = args[:redraw_interval]
|
22
|
+
@timespan = args[:timespan]
|
23
|
+
end
|
24
|
+
|
25
|
+
def data
|
26
|
+
{
|
27
|
+
title: title,
|
28
|
+
type: type,
|
29
|
+
values_title: values_label,
|
30
|
+
width: width,
|
31
|
+
interval: redraw_interval,
|
32
|
+
series: series_data
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def series_data
|
39
|
+
case type
|
40
|
+
when :spline
|
41
|
+
line_series_data
|
42
|
+
when :line
|
43
|
+
line_series_data
|
44
|
+
when :area
|
45
|
+
line_series_data
|
46
|
+
when :pie
|
47
|
+
pie_series_data
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def line_series_data
|
52
|
+
sensors.map do |s|
|
53
|
+
s.timeline_data(timespan, show_last_point)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def pie_series_data
|
58
|
+
[{
|
59
|
+
type: type,
|
60
|
+
name: values_label,
|
61
|
+
data: sensors.map{|s| s.last_point_data(show_last_point)}
|
62
|
+
}]
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'pulse-meter'
|
2
|
+
|
3
|
+
# DSL
|
4
|
+
|
5
|
+
require 'pulse-meter/visualize/dsl/errors'
|
6
|
+
require 'pulse-meter/visualize/dsl/sensor'
|
7
|
+
require 'pulse-meter/visualize/dsl/widget'
|
8
|
+
require 'pulse-meter/visualize/dsl/page'
|
9
|
+
require 'pulse-meter/visualize/dsl/layout'
|
10
|
+
|
11
|
+
# Visualize
|
12
|
+
|
13
|
+
require 'pulse-meter/visualize/sensor'
|
14
|
+
require 'pulse-meter/visualize/widget'
|
15
|
+
require 'pulse-meter/visualize/layout'
|
16
|
+
require 'pulse-meter/visualize/page'
|
17
|
+
|
18
|
+
# App
|
19
|
+
|
20
|
+
require 'pulse-meter/visualize/app'
|
21
|
+
|
22
|
+
module PulseMeter
|
23
|
+
class Visualizer
|
24
|
+
def self.draw(&block)
|
25
|
+
layout_cofigurator = PulseMeter::Visualize::DSL::Layout.new
|
26
|
+
yield(layout_cofigurator)
|
27
|
+
layout_cofigurator.to_layout
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module TestHelpers
|
2
|
+
module Matchers
|
3
|
+
|
4
|
+
class GenerallyEqual
|
5
|
+
|
6
|
+
EPSILON = 0.0001
|
7
|
+
|
8
|
+
def initialize(expected)
|
9
|
+
@expected = expected
|
10
|
+
end
|
11
|
+
|
12
|
+
def matches?(actual)
|
13
|
+
@actual = actual
|
14
|
+
|
15
|
+
if @actual.kind_of?(Float) || @expected.kind_of?(Float)
|
16
|
+
(@expected - EPSILON .. @expected + EPSILON).include? @actual
|
17
|
+
else
|
18
|
+
@expected == @actual
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def failure_message_for_should
|
23
|
+
"expected #{@actual.inspect} to be generally equal to #{@expected.inspect}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def failure_message_for_should_not
|
27
|
+
"expected #{@actual.inspect} not to be generally equal to #{@expected.inspect}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def be_generally_equal(expected)
|
32
|
+
GenerallyEqual.new(expected)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
data/pulse-meter.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/pulse-meter/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Ilya Averyanov", "Sergey Averyanov"]
|
6
|
+
gem.email = ["av@fun-box.ru", "averyanov@gmail.com"]
|
7
|
+
gem.description = %q{Lightweight metrics processor}
|
8
|
+
gem.summary = %q{
|
9
|
+
Lightweight Redis-based metrics aggregator and processor
|
10
|
+
with CLI and simple and customizable WEB interfaces
|
11
|
+
}
|
12
|
+
gem.homepage = ""
|
13
|
+
|
14
|
+
gem.files = `git ls-files`.split($\)
|
15
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
16
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
17
|
+
gem.name = "pulse-meter"
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
gem.version = PulseMeter::VERSION
|
20
|
+
|
21
|
+
gem.add_runtime_dependency('gon-sinatra')
|
22
|
+
gem.add_runtime_dependency('haml')
|
23
|
+
gem.add_runtime_dependency('json')
|
24
|
+
gem.add_runtime_dependency('redis')
|
25
|
+
gem.add_runtime_dependency('sinatra')
|
26
|
+
gem.add_runtime_dependency('terminal-table')
|
27
|
+
gem.add_runtime_dependency('thor')
|
28
|
+
|
29
|
+
gem.add_development_dependency('foreman')
|
30
|
+
gem.add_development_dependency('mock_redis')
|
31
|
+
gem.add_development_dependency('rack-test')
|
32
|
+
gem.add_development_dependency('rake')
|
33
|
+
gem.add_development_dependency('redcarpet')
|
34
|
+
gem.add_development_dependency('rspec')
|
35
|
+
gem.add_development_dependency('simplecov')
|
36
|
+
gem.add_development_dependency('timecop')
|
37
|
+
gem.add_development_dependency('yard')
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe PulseMeter::Mixins::Dumper do
|
4
|
+
class Base
|
5
|
+
include PulseMeter::Mixins::Dumper
|
6
|
+
end
|
7
|
+
|
8
|
+
class Bad < Base; end
|
9
|
+
|
10
|
+
class Undumpable < Base
|
11
|
+
def name; :name; end
|
12
|
+
|
13
|
+
def redis; PulseMeter.redis; end
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@socket = Socket.new(:INET, :STREAM)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Good < Base
|
21
|
+
attr_accessor :foo
|
22
|
+
def name; foo.to_s; end
|
23
|
+
|
24
|
+
def redis; PulseMeter.redis; end
|
25
|
+
|
26
|
+
def initialize(foo)
|
27
|
+
@foo = foo
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
let(:bad_obj){ Bad.new }
|
32
|
+
let(:undumpable_obj){ Undumpable.new }
|
33
|
+
let(:good_obj){ Good.new(:foo) }
|
34
|
+
let(:another_good_obj){ Good.new(:bar) }
|
35
|
+
let(:redis){ PulseMeter.redis }
|
36
|
+
|
37
|
+
describe '#dump' do
|
38
|
+
context "when class violates dump contract" do
|
39
|
+
context "when it has no name attribute" do
|
40
|
+
it "should raise exception" do
|
41
|
+
def bad_obj.redis; PulseMeter.redis; end
|
42
|
+
expect{ bad_obj.dump! }.to raise_exception(PulseMeter::DumpError)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context "when it has no redis attribute" do
|
47
|
+
it "should raise exception" do
|
48
|
+
def bad_obj.name; :foo; end
|
49
|
+
expect{ bad_obj.dump! }.to raise_exception(PulseMeter::DumpError)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "when redis is not avalable" do
|
54
|
+
it "should raise exception" do
|
55
|
+
def bad_obj.name; :foo; end
|
56
|
+
def bad_obj.redis; nil; end
|
57
|
+
expect{ bad_obj.dump! }.to raise_exception(PulseMeter::DumpError)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context "when object cannot be dumped" do
|
62
|
+
it "should raise exception" do
|
63
|
+
expect {undumpable_obj.dump!}.to raise_exception(PulseMeter::DumpError)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "when class follows dump contract" do
|
69
|
+
it "should not raise dump exception" do
|
70
|
+
expect {good_obj.dump!}.not_to raise_exception(PulseMeter::DumpError)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should save dump to redis" do
|
74
|
+
expect {good_obj.dump!}.to change {redis.hlen(Good::DUMP_REDIS_KEY)}.by(1)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe ".restore" do
|
80
|
+
context "when object has never been dumped" do
|
81
|
+
it "should raise exception" do
|
82
|
+
expect{ Base.restore(:nonexistant) }.to raise_exception(PulseMeter::RestoreError)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context "when object was dumped" do
|
87
|
+
before do
|
88
|
+
good_obj.dump!
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should keep object class" do
|
92
|
+
Base.restore(good_obj.name).should be_instance_of(good_obj.class)
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should restore object data" do
|
96
|
+
restored = Base.restore(good_obj.name)
|
97
|
+
restored.foo.should == good_obj.foo
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should restore last dumped object" do
|
101
|
+
good_obj.foo = :bar
|
102
|
+
good_obj.dump!
|
103
|
+
restored = Base.restore(good_obj.name)
|
104
|
+
restored.foo.should == :bar
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe ".list_names" do
|
110
|
+
context "when redis is not available" do
|
111
|
+
before do
|
112
|
+
PulseMeter.stub(:redis).and_return(nil)
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should raise exception" do
|
116
|
+
expect {Base.list_names}.to raise_exception(PulseMeter::RestoreError)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context "when redis if fine" do
|
121
|
+
it "should return empty list if nothing is registered" do
|
122
|
+
Base.list_names.should == []
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should return list of registered objects" do
|
126
|
+
good_obj.dump!
|
127
|
+
another_good_obj.dump!
|
128
|
+
Base.list_names.should =~ [good_obj.name, another_good_obj.name]
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe ".list_objects" do
|
134
|
+
before do
|
135
|
+
good_obj.dump!
|
136
|
+
another_good_obj.dump!
|
137
|
+
end
|
138
|
+
|
139
|
+
it "should return restored objects" do
|
140
|
+
objects = Base.list_objects
|
141
|
+
objects.map(&:name).should =~ [good_obj.name, another_good_obj.name]
|
142
|
+
end
|
143
|
+
|
144
|
+
it "should skip unrestorable objects" do
|
145
|
+
Base.stub(:list_names).and_return([good_obj.name, "scoundrel", another_good_obj.name])
|
146
|
+
objects = Base.list_objects
|
147
|
+
objects.map(&:name).should =~ [good_obj.name, another_good_obj.name]
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
describe "#cleanup_dump" do
|
152
|
+
it "should remove data from redis" do
|
153
|
+
good_obj.dump!
|
154
|
+
another_good_obj.dump!
|
155
|
+
expect {good_obj.cleanup_dump}.to change{good_obj.class.list_names.count}.by(-1)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|