pulse-meter-client-backport 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) 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 +3 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE +22 -0
  8. data/Procfile +3 -0
  9. data/README.md +98 -0
  10. data/Rakefile +53 -0
  11. data/bin/pulse +6 -0
  12. data/examples/readme_client_example.rb +52 -0
  13. data/lib/pulse-meter.rb +17 -0
  14. data/lib/pulse-meter/mixins/dumper.rb +76 -0
  15. data/lib/pulse-meter/mixins/utils.rb +93 -0
  16. data/lib/pulse-meter/sensor.rb +44 -0
  17. data/lib/pulse-meter/sensor/base.rb +75 -0
  18. data/lib/pulse-meter/sensor/counter.rb +36 -0
  19. data/lib/pulse-meter/sensor/hashed_counter.rb +31 -0
  20. data/lib/pulse-meter/sensor/indicator.rb +33 -0
  21. data/lib/pulse-meter/sensor/timeline.rb +180 -0
  22. data/lib/pulse-meter/sensor/timelined/average.rb +26 -0
  23. data/lib/pulse-meter/sensor/timelined/counter.rb +16 -0
  24. data/lib/pulse-meter/sensor/timelined/hashed_counter.rb +22 -0
  25. data/lib/pulse-meter/sensor/timelined/max.rb +25 -0
  26. data/lib/pulse-meter/sensor/timelined/median.rb +14 -0
  27. data/lib/pulse-meter/sensor/timelined/min.rb +25 -0
  28. data/lib/pulse-meter/sensor/timelined/percentile.rb +31 -0
  29. data/lib/pulse-meter/version.rb +3 -0
  30. data/pulse-meter-client-backport.gemspec +33 -0
  31. data/spec/pulse_meter/mixins/dumper_spec.rb +141 -0
  32. data/spec/pulse_meter/mixins/utils_spec.rb +125 -0
  33. data/spec/pulse_meter/sensor/base_spec.rb +97 -0
  34. data/spec/pulse_meter/sensor/counter_spec.rb +54 -0
  35. data/spec/pulse_meter/sensor/hashed_counter_spec.rb +39 -0
  36. data/spec/pulse_meter/sensor/indicator_spec.rb +43 -0
  37. data/spec/pulse_meter/sensor/timeline_spec.rb +45 -0
  38. data/spec/pulse_meter/sensor/timelined/average_spec.rb +6 -0
  39. data/spec/pulse_meter/sensor/timelined/counter_spec.rb +6 -0
  40. data/spec/pulse_meter/sensor/timelined/hashed_counter_spec.rb +8 -0
  41. data/spec/pulse_meter/sensor/timelined/max_spec.rb +7 -0
  42. data/spec/pulse_meter/sensor/timelined/median_spec.rb +7 -0
  43. data/spec/pulse_meter/sensor/timelined/min_spec.rb +7 -0
  44. data/spec/pulse_meter/sensor/timelined/percentile_spec.rb +17 -0
  45. data/spec/pulse_meter_spec.rb +16 -0
  46. data/spec/shared_examples/timeline_sensor.rb +274 -0
  47. data/spec/shared_examples/timelined_subclass.rb +23 -0
  48. data/spec/spec_helper.rb +21 -0
  49. data/spec/support/matchers.rb +45 -0
  50. metadata +276 -0
@@ -0,0 +1,44 @@
1
+ require 'pulse-meter/sensor/base'
2
+ require 'pulse-meter/sensor/counter'
3
+ require 'pulse-meter/sensor/hashed_counter'
4
+ require 'pulse-meter/sensor/indicator'
5
+ require 'pulse-meter/sensor/timeline'
6
+ require 'pulse-meter/sensor/timelined/average'
7
+ require 'pulse-meter/sensor/timelined/counter'
8
+ require 'pulse-meter/sensor/timelined/hashed_counter'
9
+ require 'pulse-meter/sensor/timelined/min'
10
+ require 'pulse-meter/sensor/timelined/max'
11
+ require 'pulse-meter/sensor/timelined/percentile'
12
+ require 'pulse-meter/sensor/timelined/median'
13
+
14
+ # Top level sensor module
15
+ module PulseMeter
16
+
17
+ # Atomic sensor data
18
+ SensorData = Struct.new(:start_time, :value)
19
+
20
+ # General sensor exception
21
+ class SensorError < StandardError; end
22
+
23
+ # Exception to be raised when sensor name is malformed
24
+ class BadSensorName < SensorError
25
+ def initialize(name, options = {})
26
+ super("Bad sensor name: `#{name}', only a-z letters and _ are allowed")
27
+ end
28
+ end
29
+
30
+ # Exception to be raised when Redis is not initialized
31
+ class RedisNotInitialized < SensorError
32
+ def initialize
33
+ super("PulseMeter.redis is not set")
34
+ end
35
+ end
36
+
37
+ # Exception to be raised when sensor cannot be dumped
38
+ class DumpError < SensorError; end
39
+
40
+ # Exception to be raised when sensor cannot be restored
41
+ class RestoreError < SensorError; end
42
+
43
+ end
44
+
@@ -0,0 +1,75 @@
1
+ module PulseMeter
2
+ module Sensor
3
+ # @abstract Subclass and override {#event} to implement sensor
4
+ class Base
5
+ include PulseMeter::Mixins::Dumper
6
+
7
+ # @!attribute [rw] redis
8
+ # @return [Redis]
9
+ attr_accessor :redis
10
+ # @!attribute [r] name
11
+ # @return [String] sensor name
12
+ attr_reader :name
13
+
14
+ # Initializes sensor and dumps it to redis
15
+ # @param name [String] sensor name
16
+ # @option options [String] :annotation Sensor annotation
17
+ # @raise [BadSensorName] if sensor name is malformed
18
+ # @raise [RedisNotInitialized] unless Redis is initialized
19
+ def initialize(name, options={})
20
+ @name = name.to_s
21
+ if options[:annotation]
22
+ annotate(options[:annotation])
23
+ end
24
+ raise BadSensorName, @name unless @name =~ /\A\w+\z/
25
+ raise RedisNotInitialized unless PulseMeter.redis
26
+ dump!
27
+ end
28
+
29
+ # Returns Redis instance
30
+ def redis
31
+ PulseMeter.redis
32
+ end
33
+
34
+ # Saves annotation to Redis
35
+ # @param description [String] Sensor annotation
36
+ def annotate(description)
37
+ redis.set(desc_key, description)
38
+ end
39
+
40
+ # Retrieves annotation from Redis
41
+ # @return [String] Sensor annotation
42
+ def annotation
43
+ redis.get(desc_key)
44
+ end
45
+
46
+ # Cleans up all sensor metadata in Redis
47
+ def cleanup
48
+ redis.del(desc_key)
49
+ cleanup_dump
50
+ end
51
+
52
+ # @abstract Processes event
53
+ # @param value [Object] value produced by some kind of event
54
+ def event(value)
55
+ # do nothing here
56
+ end
57
+
58
+ protected
59
+
60
+ # Forms Redis key to store annotation
61
+ def desc_key
62
+ "pulse_meter:desc:#{name}"
63
+ end
64
+
65
+ # For a block
66
+ # @yield Executes it within Redis multi
67
+ def multi
68
+ redis.multi
69
+ yield
70
+ redis.exec
71
+ end
72
+
73
+ end
74
+ end
75
+ end
@@ -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