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,36 @@
1
+ # Static counter
2
+ module PulseMeter
3
+ module Sensor
4
+ class Counter < Base
5
+
6
+ # Cleans up all sensor metadata in Redis
7
+ def cleanup
8
+ redis.del(value_key)
9
+ super
10
+ end
11
+
12
+ # Increments counter value by 1
13
+ def incr
14
+ event(1)
15
+ end
16
+
17
+ # Processes event by incremnting counter by given value
18
+ # @param value [Fixnum] increment
19
+ def event(value)
20
+ redis.incrby(value_key, value.to_i)
21
+ end
22
+
23
+ # Gets counter value
24
+ # @return [Fixnum]
25
+ def value
26
+ redis.get(value_key).to_i
27
+ end
28
+
29
+ # Gets redis key by which counter value is stored
30
+ # @return [String]
31
+ def value_key
32
+ @value_key ||= "pulse_meter:value:#{name}"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,31 @@
1
+ require 'json'
2
+
3
+ # Static hashed counter to count values by multiple keys
4
+ module PulseMeter
5
+ module Sensor
6
+ class HashedCounter < Counter
7
+
8
+ # Increments counter value by 1 for given key
9
+ # @param key [String] key to be incremented
10
+ def incr(key)
11
+ event({key => 1})
12
+ end
13
+
14
+ # Processes events for multiple keys
15
+ # @param data [Hash] hash where keys represent counter keys
16
+ # and values are increments for their keys
17
+ def event(data)
18
+ data.each_pair {|k, v| redis.hincrby(value_key, k, v.to_i)}
19
+ end
20
+
21
+ # Returs data stored in counter
22
+ # @return [Hash]
23
+ def value
24
+ redis.
25
+ hgetall(value_key).
26
+ inject(Hash.new(0)) {|h, (k, v)| h[k] = v.to_i; h}
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,33 @@
1
+ module PulseMeter
2
+ module Sensor
3
+ # Static indicator. In fact is is just a named variable with float value
4
+ class Indicator < Base
5
+
6
+ # Cleans up all sensor metadata in Redis
7
+ def cleanup
8
+ redis.del(value_key)
9
+ super
10
+ end
11
+
12
+ # Sets indicator value
13
+ # @param value [Float] new indicator value
14
+ def event(value)
15
+ redis.set(value_key, value.to_f)
16
+ end
17
+
18
+ # Get indicator value
19
+ # @return [Fixnum] indicator value or zero unless it was initialized
20
+ def value
21
+ val = redis.get(value_key)
22
+ val.nil? ? 0 : val.to_f
23
+ end
24
+
25
+ # Gets redis key by which counter value is stored
26
+ # @return [String]
27
+ def value_key
28
+ @value_key ||= "pulse_meter:value:#{name}"
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,180 @@
1
+ require 'securerandom'
2
+
3
+ module PulseMeter
4
+ module Sensor
5
+ # @abstract Represents timelined sensor: series of values,
6
+ # one value for each consequent time interval.
7
+ class Timeline < Base
8
+ include PulseMeter::Mixins::Utils
9
+
10
+ # @!attribute [r] interval
11
+ # @return [Fixnum] Rotation interval
12
+ # @!attribute [r] ttl
13
+ # @return [Fixnum] How long summarized data will be stored before expiration
14
+ # @!attribute [r] raw_data_ttl
15
+ # @return [Fixnum] How long unsummarized raw data will be stored before expiration
16
+ # @!attribute [r] reduce_delay
17
+ # @return [Fixnum] Delay between end of interval and summarization
18
+ attr_reader :interval, :ttl, :raw_data_ttl, :reduce_delay
19
+
20
+ # Default values for some sensor parameters
21
+ DEFAULTS = {
22
+ :raw_data_ttl => 3600,
23
+ :reduce_delay => 60,
24
+ }
25
+
26
+ # Initializes sensor with given name and parameters
27
+ # @param name [String] sensor name
28
+ # @option options [Fixnum] :interval Rotation interval
29
+ # @option options [Fixnum] :ttl How long summarized data will be stored before expiration
30
+ # @option options [Fixnum] :raw_data_ttl How long unsummarized raw data will be stored before expiration
31
+ # @option options [Fixnum] :reduce_delay Delay between end of interval and summarization
32
+ def initialize(name, options)
33
+ @interval = assert_positive_integer!(options, :interval)
34
+ @ttl = assert_positive_integer!(options, :ttl)
35
+ @raw_data_ttl = assert_positive_integer!(options, :raw_data_ttl, DEFAULTS[:raw_data_ttl])
36
+ @reduce_delay = assert_positive_integer!(options, :reduce_delay, DEFAULTS[:reduce_delay])
37
+ super
38
+ end
39
+
40
+ # Clean up all sensor metadata and data
41
+ def cleanup
42
+ keys = redis.keys(raw_data_key('*')) + redis.keys(data_key('*'))
43
+ multi do
44
+ keys.each{|key| redis.del(key)}
45
+ end
46
+ super
47
+ end
48
+
49
+ # Processes event
50
+ def event(value = nil)
51
+ multi do
52
+ current_key = current_raw_data_key
53
+ aggregate_event(current_key, value)
54
+ redis.expire(current_key, raw_data_ttl)
55
+ end
56
+ end
57
+
58
+
59
+ # Reduces data in given interval.
60
+ # @note Interval id is
61
+ # just unixtime of its lower bound. Ruduction is a process
62
+ # of 'compressing' all interval's raw data to a single value.
63
+ # When reduction is done summarized data is saved to Redis
64
+ # separately with expiration time taken from sensor configuration.
65
+ # @param interval_id [Fixnum]
66
+ def reduce(interval_id)
67
+ interval_raw_data_key = raw_data_key(interval_id)
68
+ return unless redis.exists(interval_raw_data_key)
69
+ value = summarize(interval_raw_data_key)
70
+ interval_data_key = data_key(interval_id)
71
+ multi do
72
+ redis.del(interval_raw_data_key)
73
+ redis.set(interval_data_key, value)
74
+ redis.expire(interval_data_key, ttl)
75
+ end
76
+ end
77
+
78
+ # Reduces data in all raw interval
79
+ def reduce_all_raw
80
+ min_time = Time.now - reduce_delay - interval
81
+ redis.keys(raw_data_key('*')).each do |key|
82
+ interval_id = key.split(':').last
83
+ next if Time.at(interval_id.to_i) > min_time
84
+ reduce(interval_id)
85
+ end
86
+ end
87
+
88
+ def self.reduce_all_raw
89
+ list_objects.each do |sensor|
90
+ sensor.reduce_all_raw if sensor.respond_to? :reduce_all_raw
91
+ end
92
+ end
93
+
94
+ # Returts sensor data within some last seconds
95
+ # @param time_ago [Fixnum] interval length in seconds
96
+ # @return [Array<SensorData>]
97
+ # @raise ArgumentError if argumets are not valid time objects
98
+ def timeline(time_ago)
99
+ raise ArgumentError unless time_ago.respond_to?(:to_i) && time_ago.to_i > 0
100
+ now = Time.now
101
+ timeline_within(now - time_ago.to_i, now)
102
+ end
103
+
104
+ # Returts sensor data within given time
105
+ # @param from [Time] lower bound
106
+ # @param till [Time] upper bound
107
+ # @return [Array<SensorData>]
108
+ # @raise ArgumentError if argumets are not valid time objects
109
+ def timeline_within(from, till)
110
+ raise ArgumentError unless from.kind_of?(Time) && till.kind_of?(Time)
111
+ start_time, end_time = from.to_i, till.to_i
112
+ current_interval_id = get_interval_id(start_time) + interval
113
+ res = []
114
+ while current_interval_id < end_time
115
+ res << get_timeline_value(current_interval_id)
116
+ current_interval_id += interval
117
+ end
118
+ res
119
+ end
120
+
121
+ # Returns sensor data for given interval.
122
+ # If the interval is not over yet makes its data in-memory summarization
123
+ # and returns calculated value
124
+ # @param interval_id [Fixnum]
125
+ # @return [SensorData]
126
+ def get_timeline_value(interval_id)
127
+ interval_data_key = data_key(interval_id)
128
+ return SensorData.new(Time.at(interval_id), redis.get(interval_data_key)) if redis.exists(interval_data_key)
129
+ interval_raw_data_key = raw_data_key(interval_id)
130
+ return SensorData.new(Time.at(interval_id), summarize(interval_raw_data_key)) if redis.exists(interval_raw_data_key)
131
+ SensorData.new(Time.at(interval_id), nil)
132
+ end
133
+
134
+ # Returns Redis key by which raw data for current interval is stored
135
+ def current_raw_data_key
136
+ raw_data_key(current_interval_id)
137
+ end
138
+
139
+ # Returns Redis key by which raw data for given interval is stored
140
+ # @param id [Fixnum] interval id
141
+ def raw_data_key(id)
142
+ "pulse_meter:raw:#{name}:#{id}"
143
+ end
144
+
145
+ # Returns Redis key by which summarized data for given interval is stored
146
+ # @param id [Fixnum] interval id
147
+ def data_key(id)
148
+ "pulse_meter:data:#{name}:#{id}"
149
+ end
150
+
151
+ # Returns interval id where given time is
152
+ # @param time [Time]
153
+ def get_interval_id(time)
154
+ (time.to_i / interval) * interval
155
+ end
156
+
157
+ # Returns current interval id
158
+ # @return [Fixnum]
159
+ def current_interval_id
160
+ get_interval_id(Time.now)
161
+ end
162
+
163
+ # @abstract Registeres event for current interval identified by key
164
+ # @param key [Fixnum] interval id
165
+ # @param value [Object] value to be aggregated
166
+ def aggregate_event(key, value)
167
+ # simple
168
+ redis.set(key, value)
169
+ end
170
+
171
+ # @abstract Summarizes all event within interval to a single value
172
+ # @param key [Fixnum] interval_id
173
+ def summarize(key)
174
+ # simple
175
+ redis.get(key)
176
+ end
177
+
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,26 @@
1
+ module PulseMeter
2
+ module Sensor
3
+ module Timelined
4
+ # Average value over interval
5
+ class Average < Timeline
6
+
7
+ def aggregate_event(key, value)
8
+ redis.hincrby(key, :count, 1)
9
+ redis.hincrby(key, :sum, value)
10
+ end
11
+
12
+ def summarize(key)
13
+ count = redis.hget(key, :count)
14
+ sum = redis.hget(key, :sum)
15
+ if count && !count.empty?
16
+ sum.to_f / count.to_f
17
+ else
18
+ 0
19
+ end
20
+ end
21
+
22
+ end
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,16 @@
1
+ module PulseMeter
2
+ module Sensor
3
+ module Timelined
4
+ # Counts events per interval
5
+ class Counter < Timeline
6
+ def aggregate_event(key, value)
7
+ redis.incrby(key, value.to_i)
8
+ end
9
+
10
+ def summarize(key)
11
+ redis.get(key).to_i
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ require 'json'
2
+
3
+ module PulseMeter
4
+ module Sensor
5
+ module Timelined
6
+ # Counts multiple types of events per interval.
7
+ # Good replacement for multiple counters to be visualized together
8
+ class HashedCounter < Timeline
9
+ def aggregate_event(key, data)
10
+ data.each_pair {|k, v| redis.hincrby(key, k, v)}
11
+ end
12
+
13
+ def summarize(key)
14
+ redis.
15
+ hgetall(key).
16
+ inject({}) {|h, (k, v)| h[k] = v.to_i; h}.
17
+ to_json
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ module PulseMeter
2
+ module Sensor
3
+ module Timelined
4
+ # Calculates max value in interval
5
+ class Max < Timeline
6
+
7
+ def aggregate_event(key, value)
8
+ redis.zadd(key, value, "#{value}::#{uniqid}")
9
+ redis.zremrangebyrank(key, 0, -2)
10
+ end
11
+
12
+ def summarize(key)
13
+ count = redis.zcard(key)
14
+ if count > 0
15
+ max_el = redis.zrange(key, -1, -1)[0]
16
+ redis.zscore(key, max_el).to_f
17
+ else
18
+ nil
19
+ end
20
+ end
21
+
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ module PulseMeter
2
+ module Sensor
3
+ module Timelined
4
+ # Calculates median of iterval values
5
+ class Median < Percentile
6
+
7
+ def initialize(name, options)
8
+ super(name, options.merge({:p => 0.5}))
9
+ end
10
+
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,25 @@
1
+ module PulseMeter
2
+ module Sensor
3
+ module Timelined
4
+ # Calculates min value in interval
5
+ class Min < Timeline
6
+
7
+ def aggregate_event(key, value)
8
+ redis.zadd(key, value, "#{value}::#{uniqid}")
9
+ redis.zremrangebyrank(key, 1, -1)
10
+ end
11
+
12
+ def summarize(key)
13
+ count = redis.zcard(key)
14
+ if count > 0
15
+ min_el = redis.zrange(key, 0, 0)[0]
16
+ redis.zscore(key, min_el).to_f
17
+ else
18
+ nil
19
+ end
20
+ end
21
+
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ module PulseMeter
2
+ module Sensor
3
+ module Timelined
4
+ # Calculates n'th percentile in interval
5
+ class Percentile < Timeline
6
+ attr_reader :p_value
7
+
8
+ def initialize(name, options)
9
+ @p_value = assert_ranged_float!(options, :p, 0, 1)
10
+ super(name, options)
11
+ end
12
+
13
+ def aggregate_event(key, value)
14
+ redis.zadd(key, value, "#{value}::#{uniqid}")
15
+ end
16
+
17
+ def summarize(key)
18
+ count = redis.zcard(key)
19
+ if count > 0
20
+ position = @p_value > 0 ? (@p_value * count).round - 1 : 0
21
+ el = redis.zrange(key, position, position)[0]
22
+ redis.zscore(key, el).to_f
23
+ else
24
+ nil
25
+ end
26
+ end
27
+
28
+ end
29
+ end
30
+ end
31
+ end