pulse_meter_core 0.4.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) 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 +8 -0
  6. data/Gemfile +2 -0
  7. data/LICENSE +22 -0
  8. data/README.md +40 -0
  9. data/Rakefile +20 -0
  10. data/lib/pulse_meter/command_aggregator/async.rb +83 -0
  11. data/lib/pulse_meter/command_aggregator/sync.rb +18 -0
  12. data/lib/pulse_meter/command_aggregator/udp.rb +48 -0
  13. data/lib/pulse_meter/mixins/dumper.rb +87 -0
  14. data/lib/pulse_meter/mixins/utils.rb +155 -0
  15. data/lib/pulse_meter/observer.rb +118 -0
  16. data/lib/pulse_meter/observer/extended.rb +32 -0
  17. data/lib/pulse_meter/sensor.rb +61 -0
  18. data/lib/pulse_meter/sensor/base.rb +88 -0
  19. data/lib/pulse_meter/sensor/configuration.rb +106 -0
  20. data/lib/pulse_meter/sensor/counter.rb +39 -0
  21. data/lib/pulse_meter/sensor/hashed_counter.rb +36 -0
  22. data/lib/pulse_meter/sensor/hashed_indicator.rb +24 -0
  23. data/lib/pulse_meter/sensor/indicator.rb +35 -0
  24. data/lib/pulse_meter/sensor/multi.rb +97 -0
  25. data/lib/pulse_meter/sensor/timeline.rb +236 -0
  26. data/lib/pulse_meter/sensor/timeline_reduce.rb +68 -0
  27. data/lib/pulse_meter/sensor/timelined/average.rb +32 -0
  28. data/lib/pulse_meter/sensor/timelined/counter.rb +23 -0
  29. data/lib/pulse_meter/sensor/timelined/hashed_counter.rb +31 -0
  30. data/lib/pulse_meter/sensor/timelined/hashed_indicator.rb +30 -0
  31. data/lib/pulse_meter/sensor/timelined/indicator.rb +23 -0
  32. data/lib/pulse_meter/sensor/timelined/max.rb +19 -0
  33. data/lib/pulse_meter/sensor/timelined/median.rb +14 -0
  34. data/lib/pulse_meter/sensor/timelined/min.rb +19 -0
  35. data/lib/pulse_meter/sensor/timelined/multi_percentile.rb +34 -0
  36. data/lib/pulse_meter/sensor/timelined/percentile.rb +22 -0
  37. data/lib/pulse_meter/sensor/timelined/uniq_counter.rb +22 -0
  38. data/lib/pulse_meter/sensor/timelined/zset_based.rb +37 -0
  39. data/lib/pulse_meter/sensor/uniq_counter.rb +24 -0
  40. data/lib/pulse_meter/server.rb +0 -0
  41. data/lib/pulse_meter/server/command_line_options.rb +0 -0
  42. data/lib/pulse_meter/server/config_options.rb +0 -0
  43. data/lib/pulse_meter/server/sensors.rb +0 -0
  44. data/lib/pulse_meter/udp_server.rb +45 -0
  45. data/lib/pulse_meter_core.rb +66 -0
  46. data/pulse_meter_core.gemspec +33 -0
  47. data/spec/pulse_meter/command_aggregator/async_spec.rb +53 -0
  48. data/spec/pulse_meter/command_aggregator/sync_spec.rb +25 -0
  49. data/spec/pulse_meter/command_aggregator/udp_spec.rb +45 -0
  50. data/spec/pulse_meter/mixins/dumper_spec.rb +162 -0
  51. data/spec/pulse_meter/mixins/utils_spec.rb +212 -0
  52. data/spec/pulse_meter/observer/extended_spec.rb +92 -0
  53. data/spec/pulse_meter/observer_spec.rb +207 -0
  54. data/spec/pulse_meter/sensor/base_spec.rb +106 -0
  55. data/spec/pulse_meter/sensor/configuration_spec.rb +103 -0
  56. data/spec/pulse_meter/sensor/counter_spec.rb +54 -0
  57. data/spec/pulse_meter/sensor/hashed_counter_spec.rb +43 -0
  58. data/spec/pulse_meter/sensor/hashed_indicator_spec.rb +39 -0
  59. data/spec/pulse_meter/sensor/indicator_spec.rb +43 -0
  60. data/spec/pulse_meter/sensor/multi_spec.rb +137 -0
  61. data/spec/pulse_meter/sensor/timeline_spec.rb +88 -0
  62. data/spec/pulse_meter/sensor/timelined/average_spec.rb +6 -0
  63. data/spec/pulse_meter/sensor/timelined/counter_spec.rb +6 -0
  64. data/spec/pulse_meter/sensor/timelined/hashed_counter_spec.rb +8 -0
  65. data/spec/pulse_meter/sensor/timelined/hashed_indicator_spec.rb +8 -0
  66. data/spec/pulse_meter/sensor/timelined/indicator_spec.rb +6 -0
  67. data/spec/pulse_meter/sensor/timelined/max_spec.rb +7 -0
  68. data/spec/pulse_meter/sensor/timelined/median_spec.rb +7 -0
  69. data/spec/pulse_meter/sensor/timelined/min_spec.rb +7 -0
  70. data/spec/pulse_meter/sensor/timelined/multi_percentile_spec.rb +21 -0
  71. data/spec/pulse_meter/sensor/timelined/percentile_spec.rb +17 -0
  72. data/spec/pulse_meter/sensor/timelined/uniq_counter_spec.rb +9 -0
  73. data/spec/pulse_meter/sensor/uniq_counter_spec.rb +28 -0
  74. data/spec/pulse_meter/udp_server_spec.rb +36 -0
  75. data/spec/pulse_meter_spec.rb +73 -0
  76. data/spec/shared_examples/timeline_sensor.rb +439 -0
  77. data/spec/shared_examples/timelined_subclass.rb +23 -0
  78. data/spec/spec_helper.rb +37 -0
  79. data/spec/support/matchers.rb +34 -0
  80. data/spec/support/observered.rb +40 -0
  81. metadata +342 -0
@@ -0,0 +1,36 @@
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
+ # Returs data stored in counter
15
+ # @return [Hash]
16
+ def value
17
+ redis.
18
+ hgetall(value_key).
19
+ inject(Hash.new(0)) {|h, (k, v)| h[k] = v.to_i; h}
20
+ end
21
+
22
+ private
23
+
24
+ # Processes events for multiple keys
25
+ # @param data [Hash] hash where keys represent counter keys
26
+ # and values are increments for their keys
27
+ def process_event(data)
28
+ data.each_pair do |k, v|
29
+ command_aggregator.hincrby(value_key, k, v.to_i)
30
+ command_aggregator.hincrby(value_key, :total, v.to_i)
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,24 @@
1
+ module PulseMeter
2
+ module Sensor
3
+ # Static hashed indicator. In fact is is just a named hash with float value
4
+ class HashedIndicator < Indicator
5
+
6
+ # Get indicator values
7
+ # @return [Fixnum] indicator value or zero unless it was initialized
8
+ def value
9
+ redis.
10
+ hgetall(value_key).
11
+ inject(Hash.new(0)) {|h, (k, v)| h[k] = v.to_f; h}
12
+ end
13
+
14
+ private
15
+
16
+ # Sets indicator values
17
+ # @param value [Hash] new indicator values
18
+ def process_event(events)
19
+ events.each_pair {|name, value| command_aggregator.hset(value_key, name, value.to_f)}
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,35 @@
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
+ # Get indicator value
13
+ # @return [Fixnum] indicator value or zero unless it was initialized
14
+ def value
15
+ val = redis.get(value_key)
16
+ val.nil? ? 0 : val.to_f
17
+ end
18
+
19
+ # Gets redis key by which counter value is stored
20
+ # @return [String]
21
+ def value_key
22
+ @value_key ||= "pulse_meter:value:#{name}"
23
+ end
24
+
25
+ private
26
+
27
+ # Sets indicator value
28
+ # @param value [Float] new indicator value
29
+ def process_event(value)
30
+ command_aggregator.set(value_key, value.to_f)
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,97 @@
1
+ module PulseMeter
2
+ module Sensor
3
+ class Multi < Base
4
+ include PulseMeter::Mixins::Utils
5
+ include Enumerable
6
+
7
+ attr_reader :name
8
+ attr_reader :factors
9
+ attr_reader :sensors
10
+ attr_reader :configuration_options
11
+
12
+ # TODO restore in initializer
13
+
14
+ def initialize(name, options)
15
+ @name = name
16
+ @factors = assert_array!(options, :factors)
17
+ @sensors = PulseMeter::Sensor::Configuration.new
18
+ @configuration_options = options[:configuration]
19
+ raise ArgumentError, "configuration option missing" unless @configuration_options
20
+ end
21
+
22
+ def sensor(name)
23
+ raise ArgumentError, 'need a block' unless block_given?
24
+ sensors.sensor(name){|s| yield(s)}
25
+ end
26
+
27
+ def event(factors_hash, value)
28
+ ensure_valid_factors!(factors_hash)
29
+
30
+ each_factors_combination do |combination|
31
+ factor_values = factor_values_for_combination(combination, factors_hash)
32
+ get_or_create_sensor(combination, factor_values) do |s|
33
+ s.event(value)
34
+ end
35
+ end
36
+ end
37
+
38
+ def each
39
+ sensors.each {|s| yield(s)}
40
+ end
41
+
42
+ def sensor_for_factors(factor_names, factor_values)
43
+ raise ArgumentError, 'need a block' unless block_given?
44
+ sensor(get_sensor_name(factor_names, factor_values)){|s| yield(s)}
45
+ end
46
+
47
+ protected
48
+
49
+ def is_subsensor?(sensor)
50
+ sensor.name.start_with?(get_sensor_name([], []).to_s)
51
+ end
52
+
53
+ def get_or_create_sensor(factor_names, factor_values)
54
+ raise ArgumentError, 'need a block' unless block_given?
55
+ name = get_sensor_name(factor_names, factor_values)
56
+ unless sensors.has_sensor?(name)
57
+ sensors.add_sensor(name, configuration_options)
58
+ dump!(false)
59
+ end
60
+ sensor(name) do |s|
61
+ yield(s)
62
+ end
63
+ end
64
+
65
+ def ensure_valid_factors!(factors_hash)
66
+ factors.each do |factor_name|
67
+ unless factors_hash.has_key?(factor_name)
68
+ raise ArgumentError, "Value of factor #{factor_name} missing"
69
+ end
70
+ end
71
+ end
72
+
73
+ def each_factors_combination
74
+ each_subset(factors) do |combination|
75
+ yield(combination)
76
+ end
77
+ end
78
+
79
+ def factor_values_for_combination(combination, factors_hash)
80
+ combination.each_with_object([]) do |k, acc|
81
+ acc << factors_hash[k]
82
+ end
83
+ end
84
+
85
+ def get_sensor_name(factor_names, factor_values)
86
+ sensor_name = name.to_s
87
+ unless factor_names.empty?
88
+ factor_names.zip(factor_values).each do |n, v|
89
+ sensor_name << "_#{n}_#{v}"
90
+ end
91
+ end
92
+ sensor_name.to_sym
93
+ end
94
+
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,236 @@
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
+ include PulseMeter::Sensor::TimelineReduce
10
+
11
+ MAX_TIMESPAN_POINTS = 1000
12
+
13
+ # @!attribute [r] interval
14
+ # @return [Fixnum] Rotation interval
15
+ # @!attribute [r] ttl
16
+ # @return [Fixnum] How long summarized data will be stored before expiration
17
+ # @!attribute [r] raw_data_ttl
18
+ # @return [Fixnum] How long unsummarized raw data will be stored before expiration
19
+ # @!attribute [r] reduce_delay
20
+ # @return [Fixnum] Delay between end of interval and summarization
21
+ attr_reader :interval, :ttl, :raw_data_ttl, :reduce_delay
22
+
23
+ # Default values for some sensor parameters
24
+ DEFAULTS = {
25
+ :raw_data_ttl => 3600,
26
+ :reduce_delay => 60,
27
+ }
28
+
29
+ # Initializes sensor with given name and parameters
30
+ # @param name [String] sensor name
31
+ # @option options [Fixnum] :interval Rotation interval
32
+ # @option options [Fixnum] :ttl How long summarized data will be stored before expiration
33
+ # @option options [Fixnum] :raw_data_ttl How long unsummarized raw data will be stored before expiration
34
+ # @option options [Fixnum] :reduce_delay Delay between end of interval and summarization
35
+ def initialize(name, options)
36
+ @interval = assert_positive_integer!(options, :interval)
37
+ @ttl = assert_positive_integer!(options, :ttl)
38
+ @raw_data_ttl = assert_positive_integer!(options, :raw_data_ttl, DEFAULTS[:raw_data_ttl])
39
+ @reduce_delay = assert_positive_integer!(options, :reduce_delay, DEFAULTS[:reduce_delay])
40
+ super
41
+ end
42
+
43
+ # Clean up all sensor metadata and data
44
+ def cleanup
45
+ keys = redis.keys(raw_data_key('*')) + redis.keys(data_key('*'))
46
+ multi do
47
+ keys.each{|key| redis.del(key)}
48
+ end
49
+ super
50
+ end
51
+
52
+ # Processes event from the past
53
+ # @param time [Time] event time
54
+ # @param value event value
55
+ def event_at(time, value = nil)
56
+ multi do
57
+ interval_id = get_interval_id(time)
58
+ key = raw_data_key(interval_id)
59
+ aggregate_event(key, value)
60
+ command_aggregator.expire(key, raw_data_ttl)
61
+ end
62
+ true
63
+ rescue StandardError => e
64
+ false
65
+ end
66
+
67
+ # Returts sensor data within some last seconds
68
+ # @param time_ago [Fixnum] interval length in seconds
69
+ # @return [Array<SensorData>]
70
+ # @raise ArgumentError if argumets are not valid time objects
71
+ def timeline(time_ago)
72
+ raise ArgumentError unless time_ago.respond_to?(:to_i) && time_ago.to_i > 0
73
+ now = Time.now
74
+ timeline_within(now - time_ago.to_i, now)
75
+ end
76
+
77
+ # Returts sensor data within given time
78
+ # @param from [Time] lower bound
79
+ # @param till [Time] upper bound
80
+ # @param skip_optimization [Boolean] must be set to true to skip interval optimization
81
+ # @return [Array<SensorData>]
82
+ # @raise ArgumentError if argumets are not valid time objects
83
+ def timeline_within(from, till, skip_optimization = false)
84
+ raise ArgumentError unless from.kind_of?(Time) && till.kind_of?(Time)
85
+ start_time, end_time = from.to_i, till.to_i
86
+ actual_interval = optimized_interval(start_time, end_time, skip_optimization)
87
+ start_interval_id = get_interval_id(start_time) + actual_interval
88
+ ids, values = fetch_reduced_interval_data(start_interval_id, actual_interval, end_time)
89
+ zip_with_raw_data(ids, values)
90
+ end
91
+
92
+ # Returns sensor data for given interval making in-memory summarization
93
+ # and returns calculated value
94
+ # @param interval_id [Fixnum]
95
+ # @return [SensorData]
96
+ def get_raw_value(interval_id)
97
+ interval_raw_data_key = raw_data_key(interval_id)
98
+ if redis.exists(interval_raw_data_key)
99
+ sensor_data(interval_id, summarize(interval_raw_data_key))
100
+ else
101
+ sensor_data(interval_id, nil)
102
+ end
103
+ end
104
+
105
+ # Drops sensor data within given time
106
+ # @param from [Time] lower bound
107
+ # @param till [Time] upper bound
108
+ # @raise ArgumentError if argumets are not valid time objects
109
+ def drop_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
+ keys = []
114
+ while current_interval_id < end_time
115
+ keys << data_key(current_interval_id)
116
+ keys << raw_data_key(current_interval_id)
117
+ current_interval_id += interval
118
+ end
119
+ keys.empty? ? 0 : redis.del(*keys)
120
+ end
121
+
122
+ # Returns Redis key by which raw data for current interval is stored
123
+ def current_raw_data_key
124
+ raw_data_key(current_interval_id)
125
+ end
126
+
127
+ # Returns Redis key by which raw data for given interval is stored
128
+ # @param id [Fixnum] interval id
129
+ def raw_data_key(id)
130
+ "pulse_meter:raw:#{name}:#{id}"
131
+ end
132
+
133
+ # Returns Redis key by which summarized data for given interval is stored
134
+ # @param id [Fixnum] interval id
135
+ def data_key(id)
136
+ "pulse_meter:data:#{name}:#{id}"
137
+ end
138
+
139
+ # Returns interval id where given time is
140
+ # @param time [Time]
141
+ def get_interval_id(time)
142
+ (time.to_i / interval) * interval
143
+ end
144
+
145
+ # Returns current interval id
146
+ # @return [Fixnum]
147
+ def current_interval_id
148
+ get_interval_id(Time.now)
149
+ end
150
+
151
+ # @abstract Registeres event for current interval identified by key
152
+ # @param key [Fixnum] interval id
153
+ # @param value [Object] value to be aggregated
154
+ def aggregate_event(key, value)
155
+ # simple
156
+ redis.set(key, value)
157
+ end
158
+
159
+ # @abstract Summarizes all event within interval to a single value
160
+ # @param key [Fixnum] interval_id
161
+ def summarize(key)
162
+ # simple
163
+ redis.get(key)
164
+ end
165
+
166
+ # @abstract Deflates data taken from redis as string preserving nil values
167
+ # @param value [String] raw data
168
+ def deflate_safe(value)
169
+ value.nil? ? nil : deflate(value)
170
+ rescue
171
+ nil
172
+ end
173
+
174
+ private
175
+
176
+ def deflate(value)
177
+ # simple
178
+ value
179
+ end
180
+
181
+ def sensor_data(interval_id, value)
182
+ value = deflate(value) unless value.nil?
183
+ SensorData.new(Time.at(interval_id), value)
184
+ end
185
+
186
+ # Processes event
187
+ # @param value event value
188
+ def process_event(value = nil)
189
+ command_aggregator.multi do
190
+ current_key = current_raw_data_key
191
+ aggregate_event(current_key, value)
192
+ command_aggregator.expire(current_key, raw_data_ttl)
193
+ end
194
+ end
195
+
196
+ # Makes interval optimization so that the requested timespan contains less than MAX_TIMESPAN_POINTS values
197
+ # @param start_time [Fixnum] unix timestamp of timespan start
198
+ # @param end_time [Fixnum] unix timestamp of timespan start
199
+ # @return [Fixnum] optimized interval in seconds.
200
+ def optimized_interval(start_time, end_time, skip_optimization = false)
201
+ res_interval = interval
202
+ return res_interval if skip_optimization
203
+ timespan = end_time - start_time
204
+ while timespan / res_interval > MAX_TIMESPAN_POINTS - 1
205
+ res_interval *= 2
206
+ end
207
+ res_interval
208
+ end
209
+
210
+ def fetch_reduced_interval_data(start_interval_id, actual_interval, end_time)
211
+ keys = []
212
+ ids = []
213
+ current_interval_id = start_interval_id
214
+ while current_interval_id < end_time
215
+ ids << current_interval_id
216
+ keys << data_key(current_interval_id)
217
+ current_interval_id += actual_interval
218
+ end
219
+ values = keys.empty? ? [] : redis.mget(*keys)
220
+ [ids, values]
221
+ end
222
+
223
+ def zip_with_raw_data(ids, values)
224
+ res = []
225
+ ids.zip(values) do |(id, val)|
226
+ res << if val.nil?
227
+ get_raw_value(id)
228
+ else
229
+ sensor_data(id, val)
230
+ end
231
+ end
232
+ res
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,68 @@
1
+ module PulseMeter
2
+ module Sensor
3
+ # Methods for reducing raw data to single values
4
+ module TimelineReduce
5
+
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ MAX_INTERVALS = 100
11
+
12
+ # @note Interval id is
13
+ # just unixtime of its lower bound. Ruduction is a process
14
+ # of 'compressing' all interval's raw data to a single value.
15
+ # When reduction is done summarized data is saved to Redis
16
+ # separately with expiration time taken from sensor configuration.
17
+ # @param interval_id [Fixnum]
18
+ def reduce(interval_id)
19
+ interval_raw_data_key = raw_data_key(interval_id)
20
+ return unless redis.exists(interval_raw_data_key)
21
+ value = summarize(interval_raw_data_key)
22
+ interval_data_key = data_key(interval_id)
23
+ multi do
24
+ redis.del(interval_raw_data_key)
25
+ if redis.setnx(interval_data_key, value)
26
+ redis.expire(interval_data_key, ttl)
27
+ end
28
+ end
29
+ end
30
+
31
+ # Reduces data in all raw intervals
32
+ def reduce_all_raw
33
+ time = Time.now
34
+ min_time = time - reduce_delay - interval
35
+ max_depth = time - reduce_delay - interval * MAX_INTERVALS
36
+ ids = collect_ids_to_reduce(time, max_depth, min_time)
37
+ ids.reverse.each {|id| reduce(id)}
38
+ end
39
+
40
+ def collect_ids_to_reduce(time, time_from, time_to)
41
+ ids = []
42
+ while (time > time_from) # go backwards
43
+ time -= interval
44
+ interval_id = get_interval_id(time)
45
+ next if Time.at(interval_id) > time_to
46
+
47
+ reduced_key = data_key(interval_id)
48
+ raw_key = raw_data_key(interval_id)
49
+ break if redis.exists(reduced_key)
50
+ ids << interval_id
51
+ end
52
+ ids
53
+ end
54
+
55
+ module ClassMethods
56
+
57
+ def reduce_all_raw
58
+ list_objects.each do |sensor|
59
+ sensor.reduce_all_raw if sensor.respond_to? :reduce_all_raw
60
+ end
61
+ end
62
+
63
+ end
64
+
65
+ end
66
+ end
67
+ end
68
+