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.
Files changed (96) hide show
  1. data/.gitignore +19 -0
  2. data/.rbenv-version +1 -0
  3. data/.rspec +1 -0
  4. data/.rvmrc +1 -0
  5. data/.travis.yml +4 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE +22 -0
  8. data/Procfile +3 -0
  9. data/README.md +440 -0
  10. data/Rakefile +53 -0
  11. data/bin/pulse +6 -0
  12. data/examples/basic.ru +109 -0
  13. data/examples/basic_sensor_data.rb +38 -0
  14. data/examples/full/Procfile +2 -0
  15. data/examples/full/client.rb +82 -0
  16. data/examples/full/server.ru +114 -0
  17. data/examples/minimal/Procfile +2 -0
  18. data/examples/minimal/client.rb +16 -0
  19. data/examples/minimal/server.ru +20 -0
  20. data/examples/readme_client_example.rb +52 -0
  21. data/lib/cmd.rb +150 -0
  22. data/lib/pulse-meter.rb +17 -0
  23. data/lib/pulse-meter/mixins/dumper.rb +72 -0
  24. data/lib/pulse-meter/mixins/utils.rb +91 -0
  25. data/lib/pulse-meter/sensor.rb +44 -0
  26. data/lib/pulse-meter/sensor/base.rb +75 -0
  27. data/lib/pulse-meter/sensor/counter.rb +36 -0
  28. data/lib/pulse-meter/sensor/hashed_counter.rb +31 -0
  29. data/lib/pulse-meter/sensor/indicator.rb +33 -0
  30. data/lib/pulse-meter/sensor/timeline.rb +180 -0
  31. data/lib/pulse-meter/sensor/timelined/average.rb +26 -0
  32. data/lib/pulse-meter/sensor/timelined/counter.rb +16 -0
  33. data/lib/pulse-meter/sensor/timelined/hashed_counter.rb +22 -0
  34. data/lib/pulse-meter/sensor/timelined/max.rb +25 -0
  35. data/lib/pulse-meter/sensor/timelined/median.rb +14 -0
  36. data/lib/pulse-meter/sensor/timelined/min.rb +25 -0
  37. data/lib/pulse-meter/sensor/timelined/percentile.rb +31 -0
  38. data/lib/pulse-meter/version.rb +3 -0
  39. data/lib/pulse-meter/visualize/app.rb +43 -0
  40. data/lib/pulse-meter/visualize/dsl.rb +0 -0
  41. data/lib/pulse-meter/visualize/dsl/errors.rb +46 -0
  42. data/lib/pulse-meter/visualize/dsl/layout.rb +55 -0
  43. data/lib/pulse-meter/visualize/dsl/page.rb +50 -0
  44. data/lib/pulse-meter/visualize/dsl/sensor.rb +21 -0
  45. data/lib/pulse-meter/visualize/dsl/widget.rb +84 -0
  46. data/lib/pulse-meter/visualize/layout.rb +54 -0
  47. data/lib/pulse-meter/visualize/page.rb +30 -0
  48. data/lib/pulse-meter/visualize/public/css/application.css +19 -0
  49. data/lib/pulse-meter/visualize/public/css/bootstrap.css +4883 -0
  50. data/lib/pulse-meter/visualize/public/css/bootstrap.min.css +729 -0
  51. data/lib/pulse-meter/visualize/public/favicon.ico +0 -0
  52. data/lib/pulse-meter/visualize/public/img/glyphicons-halflings-white.png +0 -0
  53. data/lib/pulse-meter/visualize/public/img/glyphicons-halflings.png +0 -0
  54. data/lib/pulse-meter/visualize/public/js/application.coffee +262 -0
  55. data/lib/pulse-meter/visualize/public/js/application.js +279 -0
  56. data/lib/pulse-meter/visualize/public/js/backbone-min.js +38 -0
  57. data/lib/pulse-meter/visualize/public/js/bootstrap.js +1835 -0
  58. data/lib/pulse-meter/visualize/public/js/highcharts.js +203 -0
  59. data/lib/pulse-meter/visualize/public/js/jquery-1.7.2.min.js +4 -0
  60. data/lib/pulse-meter/visualize/public/js/json2.js +487 -0
  61. data/lib/pulse-meter/visualize/public/js/underscore-min.js +32 -0
  62. data/lib/pulse-meter/visualize/sensor.rb +60 -0
  63. data/lib/pulse-meter/visualize/views/main.haml +40 -0
  64. data/lib/pulse-meter/visualize/widget.rb +68 -0
  65. data/lib/pulse-meter/visualizer.rb +30 -0
  66. data/lib/test_helpers/matchers.rb +36 -0
  67. data/pulse-meter.gemspec +39 -0
  68. data/spec/pulse_meter/mixins/dumper_spec.rb +158 -0
  69. data/spec/pulse_meter/mixins/utils_spec.rb +134 -0
  70. data/spec/pulse_meter/sensor/base_spec.rb +97 -0
  71. data/spec/pulse_meter/sensor/counter_spec.rb +54 -0
  72. data/spec/pulse_meter/sensor/hashed_counter_spec.rb +39 -0
  73. data/spec/pulse_meter/sensor/indicator_spec.rb +43 -0
  74. data/spec/pulse_meter/sensor/timeline_spec.rb +58 -0
  75. data/spec/pulse_meter/sensor/timelined/average_spec.rb +6 -0
  76. data/spec/pulse_meter/sensor/timelined/counter_spec.rb +6 -0
  77. data/spec/pulse_meter/sensor/timelined/hashed_counter_spec.rb +8 -0
  78. data/spec/pulse_meter/sensor/timelined/max_spec.rb +7 -0
  79. data/spec/pulse_meter/sensor/timelined/median_spec.rb +7 -0
  80. data/spec/pulse_meter/sensor/timelined/min_spec.rb +7 -0
  81. data/spec/pulse_meter/sensor/timelined/percentile_spec.rb +17 -0
  82. data/spec/pulse_meter/visualize/app_spec.rb +27 -0
  83. data/spec/pulse_meter/visualize/dsl/layout_spec.rb +64 -0
  84. data/spec/pulse_meter/visualize/dsl/page_spec.rb +75 -0
  85. data/spec/pulse_meter/visualize/dsl/sensor_spec.rb +30 -0
  86. data/spec/pulse_meter/visualize/dsl/widget_spec.rb +127 -0
  87. data/spec/pulse_meter/visualize/layout_spec.rb +55 -0
  88. data/spec/pulse_meter/visualize/page_spec.rb +150 -0
  89. data/spec/pulse_meter/visualize/sensor_spec.rb +120 -0
  90. data/spec/pulse_meter/visualize/widget_spec.rb +113 -0
  91. data/spec/pulse_meter/visualizer_spec.rb +42 -0
  92. data/spec/pulse_meter_spec.rb +16 -0
  93. data/spec/shared_examples/timeline_sensor.rb +279 -0
  94. data/spec/shared_examples/timelined_subclass.rb +23 -0
  95. data/spec/spec_helper.rb +29 -0
  96. 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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;").replace(/\//g,"&#x2F;")};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
@@ -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