pulse-meter 0.4.1 → 0.4.2
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/README.md +2 -0
- data/lib/cmd.rb +10 -8
- data/lib/pulse-meter/mixins/cmd.rb +1 -1
- data/lib/pulse-meter/mixins/utils.rb +14 -0
- data/lib/pulse-meter/observer.rb +51 -52
- data/lib/pulse-meter/sensor.rb +2 -0
- data/lib/pulse-meter/sensor/timeline.rb +31 -71
- data/lib/pulse-meter/sensor/timeline_reduce.rb +68 -0
- data/lib/pulse-meter/sensor/timelined/max.rb +5 -17
- data/lib/pulse-meter/sensor/timelined/min.rb +5 -17
- data/lib/pulse-meter/sensor/timelined/multi_percentile.rb +8 -17
- data/lib/pulse-meter/sensor/timelined/percentile.rb +5 -20
- data/lib/pulse-meter/sensor/timelined/zset_based.rb +38 -0
- data/lib/pulse-meter/version.rb +1 -1
- data/lib/pulse-meter/visualize/public/css/application.css +11 -0
- data/lib/pulse-meter/visualize/widget.rb +0 -16
- data/lib/pulse-meter/visualize/widgets/timeline.rb +2 -2
- data/spec/pulse_meter/mixins/cmd_spec.rb +11 -6
- data/spec/pulse_meter/mixins/utils_spec.rb +20 -0
- data/spec/pulse_meter/observer_spec.rb +16 -16
- data/spec/spec_helper.rb +2 -0
- metadata +7 -2
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
[](http://travis-ci.org/savonarola/pulse-meter)
|
2
|
+
[](https://codeclimate.com/github/savonarola/pulse-meter)
|
2
3
|
|
3
4
|
# PulseMeter
|
4
5
|
|
@@ -68,6 +69,7 @@ The following timeline sensors are available:
|
|
68
69
|
* Multifactor sensor
|
69
70
|
* Median value
|
70
71
|
* Percentile
|
72
|
+
* MultiPercentile
|
71
73
|
* Unique counter
|
72
74
|
|
73
75
|
There are several caveats with timeline sensors:
|
data/lib/cmd.rb
CHANGED
@@ -9,6 +9,10 @@ module Cmd
|
|
9
9
|
include PulseMeter::Mixins::Utils
|
10
10
|
include PulseMeter::Mixins::Cmd
|
11
11
|
no_tasks do
|
12
|
+
def create_redis
|
13
|
+
Redis.new :host => options[:host], :port => options[:port], :db => options[:db]
|
14
|
+
end
|
15
|
+
|
12
16
|
def self.common_options
|
13
17
|
method_option :host, :default => '127.0.0.1', :desc => "Redis host"
|
14
18
|
method_option :port, :default => 6379, :desc => "Redis port"
|
@@ -53,7 +57,10 @@ module Cmd
|
|
53
57
|
if "json" == options[:format]
|
54
58
|
value = JSON.parse(value)
|
55
59
|
end
|
56
|
-
with_safe_restore_of(name) {|sensor|
|
60
|
+
with_safe_restore_of(name) {|sensor|
|
61
|
+
sensor.event(value)
|
62
|
+
}
|
63
|
+
PulseMeter.command_aggregator.wait_for_pending_events
|
57
64
|
end
|
58
65
|
|
59
66
|
desc "timeline NAME SECONDS", "Get sensor's NAME timeline for last SECONDS"
|
@@ -142,13 +149,8 @@ module Cmd
|
|
142
149
|
desc "drop NAME DATE_FROM(YYYYmmddHHMMSS) DATE_TO(YYYYmmddHHMMSS)", "Drop timeline data of a particular sensor"
|
143
150
|
common_options
|
144
151
|
def drop(name, from, to)
|
145
|
-
time_from
|
146
|
-
|
147
|
-
Time.gm(*m.captures.map(&:to_i))
|
148
|
-
end
|
149
|
-
end
|
150
|
-
fail! "DATE_FROM is not a valid timestamp" unless time_from.is_a?(Time)
|
151
|
-
fail! "DATE_TO is not a valid timestamp" unless time_to.is_a?(Time)
|
152
|
+
time_from = parse_time(from)
|
153
|
+
time_to = parse_time(to)
|
152
154
|
with_safe_restore_of(name) do |sensor|
|
153
155
|
fail! "Sensor #{name} has no drop_within method" unless sensor.respond_to?(:drop_within)
|
154
156
|
sensor.drop_within(time_from, time_to)
|
@@ -88,6 +88,20 @@ module PulseMeter
|
|
88
88
|
(first_letter_upper ? first.capitalize : first.downcase) + terms.map(&:capitalize).join
|
89
89
|
end
|
90
90
|
|
91
|
+
# Converts string of the form YYYYmmddHHMMSS (considered as UTC) to Time
|
92
|
+
# @param str [String] string to be converted
|
93
|
+
# @return [Time]
|
94
|
+
# @raise [ArgumentError] unless passed value responds to to_s
|
95
|
+
def parse_time(str)
|
96
|
+
raise ArgumentError unless str.respond_to?(:to_s)
|
97
|
+
m = str.to_s.match(/\A(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\z/)
|
98
|
+
if m
|
99
|
+
Time.gm(*m.captures.map(&:to_i))
|
100
|
+
else
|
101
|
+
raise ArgumentError, "`#{str}' is not a YYYYmmddHHMMSS time"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
91
105
|
# Symbolizes hash keys
|
92
106
|
def symbolize_keys(h)
|
93
107
|
h.each_with_object({}) do |(k, v), acc|
|
data/lib/pulse-meter/observer.rb
CHANGED
@@ -8,13 +8,9 @@ module PulseMeter
|
|
8
8
|
# @param method [Symbol] instance method name
|
9
9
|
def unobserve_method(klass, method)
|
10
10
|
with_observer = method_with_observer(method)
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
klass.class_eval do
|
15
|
-
alias_method method, without_observer
|
16
|
-
remove_method with_observer
|
17
|
-
remove_method without_observer
|
11
|
+
if klass.method_defined?(with_observer)
|
12
|
+
block = unchain_block(method)
|
13
|
+
klass.class_eval &block
|
18
14
|
end
|
19
15
|
end
|
20
16
|
|
@@ -23,13 +19,10 @@ module PulseMeter
|
|
23
19
|
# @param method [Symbol] class method name
|
24
20
|
def unobserve_class_method(klass, method)
|
25
21
|
with_observer = method_with_observer(method)
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
alias_method method, without_observer
|
31
|
-
remove_method with_observer
|
32
|
-
remove_method without_observer
|
22
|
+
if klass.respond_to?(with_observer)
|
23
|
+
method_owner = metaclass(klass)
|
24
|
+
block = unchain_block(method)
|
25
|
+
method_owner.instance_eval &block
|
33
26
|
end
|
34
27
|
end
|
35
28
|
|
@@ -40,27 +33,9 @@ module PulseMeter
|
|
40
33
|
# @param proc [Proc] proc to be called in context of receiver each time observed method called
|
41
34
|
def observe_method(klass, method, sensor, &proc)
|
42
35
|
with_observer = method_with_observer(method)
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
klass.class_eval do
|
48
|
-
alias_method without_observer, method
|
49
|
-
define_method with_observer do |*args, &block|
|
50
|
-
result = nil
|
51
|
-
start_time = Time.now
|
52
|
-
begin
|
53
|
-
result = self.send without_observer, *args, &block
|
54
|
-
ensure
|
55
|
-
begin
|
56
|
-
delta = ((Time.now - start_time) * 1000).to_i
|
57
|
-
sensor.instance_exec delta, *args, &proc
|
58
|
-
rescue Exception
|
59
|
-
end
|
60
|
-
end
|
61
|
-
result
|
62
|
-
end
|
63
|
-
alias_method method, with_observer
|
36
|
+
unless klass.method_defined?(with_observer)
|
37
|
+
block = chain_block(method, sensor, &proc)
|
38
|
+
klass.class_eval &block
|
64
39
|
end
|
65
40
|
end
|
66
41
|
|
@@ -71,31 +46,55 @@ module PulseMeter
|
|
71
46
|
# @param proc [Proc] proc to be called in context of receiver each time observed method called
|
72
47
|
def observe_class_method(klass, method, sensor, &proc)
|
73
48
|
with_observer = method_with_observer(method)
|
74
|
-
|
49
|
+
unless klass.respond_to?(with_observer)
|
50
|
+
method_owner = metaclass(klass)
|
51
|
+
block = chain_block(method, sensor, &proc)
|
52
|
+
method_owner.instance_eval &block
|
53
|
+
end
|
54
|
+
end
|
75
55
|
|
76
|
-
|
56
|
+
private
|
57
|
+
|
58
|
+
def unchain_block(method)
|
59
|
+
with_observer = method_with_observer(method)
|
60
|
+
without_observer = method_without_observer(method)
|
77
61
|
|
78
|
-
|
79
|
-
alias_method without_observer
|
80
|
-
|
81
|
-
|
82
|
-
|
62
|
+
Proc.new do
|
63
|
+
alias_method(method, without_observer)
|
64
|
+
remove_method(with_observer)
|
65
|
+
remove_method(without_observer)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def define_instrumented_method(method_owner, method, receiver, &handler)
|
70
|
+
with_observer = method_with_observer(method)
|
71
|
+
without_observer = method_without_observer(method)
|
72
|
+
method_owner.send(:define_method, with_observer) do |*args, &block|
|
73
|
+
start_time = Time.now
|
74
|
+
begin
|
75
|
+
self.send(without_observer, *args, &block)
|
76
|
+
ensure
|
83
77
|
begin
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
delta = ((Time.now - start_time) * 1000).to_i
|
88
|
-
sensor.instance_exec delta, *args, &proc
|
89
|
-
rescue Exception
|
90
|
-
end
|
78
|
+
delta = ((Time.now - start_time) * 1000).to_i
|
79
|
+
receiver.instance_exec(delta, *args, &handler)
|
80
|
+
rescue StandardError
|
91
81
|
end
|
92
|
-
result
|
93
82
|
end
|
94
|
-
alias_method method, with_observer
|
95
83
|
end
|
96
84
|
end
|
97
85
|
|
98
|
-
|
86
|
+
def chain_block(method, receiver, &handler)
|
87
|
+
with_observer = method_with_observer(method)
|
88
|
+
without_observer = method_without_observer(method)
|
89
|
+
me = self
|
90
|
+
|
91
|
+
Proc.new do
|
92
|
+
alias_method(without_observer, method)
|
93
|
+
method_owner = self
|
94
|
+
me.send(:define_instrumented_method, method_owner, method, receiver, &handler)
|
95
|
+
alias_method(method, with_observer)
|
96
|
+
end
|
97
|
+
end
|
99
98
|
|
100
99
|
def metaclass(klass)
|
101
100
|
klass.class_eval do
|
data/lib/pulse-meter/sensor.rb
CHANGED
@@ -6,12 +6,14 @@ require 'pulse-meter/sensor/hashed_counter'
|
|
6
6
|
require 'pulse-meter/sensor/hashed_indicator'
|
7
7
|
require 'pulse-meter/sensor/multi'
|
8
8
|
require 'pulse-meter/sensor/uniq_counter'
|
9
|
+
require 'pulse-meter/sensor/timeline_reduce'
|
9
10
|
require 'pulse-meter/sensor/timeline'
|
10
11
|
require 'pulse-meter/sensor/timelined/average'
|
11
12
|
require 'pulse-meter/sensor/timelined/counter'
|
12
13
|
require 'pulse-meter/sensor/timelined/indicator'
|
13
14
|
require 'pulse-meter/sensor/timelined/hashed_counter'
|
14
15
|
require 'pulse-meter/sensor/timelined/hashed_indicator'
|
16
|
+
require 'pulse-meter/sensor/timelined/zset_based'
|
15
17
|
require 'pulse-meter/sensor/timelined/min'
|
16
18
|
require 'pulse-meter/sensor/timelined/max'
|
17
19
|
require 'pulse-meter/sensor/timelined/percentile'
|
@@ -6,9 +6,9 @@ module PulseMeter
|
|
6
6
|
# one value for each consequent time interval.
|
7
7
|
class Timeline < Base
|
8
8
|
include PulseMeter::Mixins::Utils
|
9
|
+
include PulseMeter::Sensor::TimelineReduce
|
9
10
|
|
10
11
|
MAX_TIMESPAN_POINTS = 1000
|
11
|
-
MAX_INTERVALS = 100
|
12
12
|
|
13
13
|
# @!attribute [r] interval
|
14
14
|
# @return [Fixnum] Rotation interval
|
@@ -64,51 +64,6 @@ module PulseMeter
|
|
64
64
|
false
|
65
65
|
end
|
66
66
|
|
67
|
-
# Reduces data in given interval.
|
68
|
-
# @note Interval id is
|
69
|
-
# just unixtime of its lower bound. Ruduction is a process
|
70
|
-
# of 'compressing' all interval's raw data to a single value.
|
71
|
-
# When reduction is done summarized data is saved to Redis
|
72
|
-
# separately with expiration time taken from sensor configuration.
|
73
|
-
# @param interval_id [Fixnum]
|
74
|
-
def reduce(interval_id)
|
75
|
-
interval_raw_data_key = raw_data_key(interval_id)
|
76
|
-
return unless redis.exists(interval_raw_data_key)
|
77
|
-
value = summarize(interval_raw_data_key)
|
78
|
-
interval_data_key = data_key(interval_id)
|
79
|
-
multi do
|
80
|
-
redis.del(interval_raw_data_key)
|
81
|
-
if redis.setnx(interval_data_key, value)
|
82
|
-
redis.expire(interval_data_key, ttl)
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
# Reduces data in all raw interval
|
88
|
-
def reduce_all_raw
|
89
|
-
time = Time.now
|
90
|
-
min_time = time - reduce_delay - interval
|
91
|
-
max_depth = time - reduce_delay - interval * MAX_INTERVALS
|
92
|
-
ids = []
|
93
|
-
while (time > max_depth)
|
94
|
-
time -= interval
|
95
|
-
interval_id = get_interval_id(time)
|
96
|
-
next if Time.at(interval_id) > min_time
|
97
|
-
|
98
|
-
reduced_key = data_key(interval_id)
|
99
|
-
raw_key = raw_data_key(interval_id)
|
100
|
-
break if redis.exists(reduced_key)
|
101
|
-
ids << interval_id
|
102
|
-
end
|
103
|
-
ids.reverse.each {|id| reduce(id)}
|
104
|
-
end
|
105
|
-
|
106
|
-
def self.reduce_all_raw
|
107
|
-
list_objects.each do |sensor|
|
108
|
-
sensor.reduce_all_raw if sensor.respond_to? :reduce_all_raw
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
67
|
# Returts sensor data within some last seconds
|
113
68
|
# @param time_ago [Fixnum] interval length in seconds
|
114
69
|
# @return [Array<SensorData>]
|
@@ -128,31 +83,12 @@ module PulseMeter
|
|
128
83
|
def timeline_within(from, till, skip_optimization = false)
|
129
84
|
raise ArgumentError unless from.kind_of?(Time) && till.kind_of?(Time)
|
130
85
|
start_time, end_time = from.to_i, till.to_i
|
131
|
-
actual_interval =
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
end
|
136
|
-
current_interval_id = get_interval_id(start_time) + actual_interval
|
137
|
-
keys = []
|
138
|
-
ids = []
|
139
|
-
while current_interval_id < end_time
|
140
|
-
ids << current_interval_id
|
141
|
-
keys << data_key(current_interval_id)
|
142
|
-
current_interval_id += actual_interval
|
143
|
-
end
|
144
|
-
values = keys.empty? ? [] : redis.mget(*keys)
|
145
|
-
res = []
|
146
|
-
ids.zip(values) do |(id, val)|
|
147
|
-
res << if val.nil?
|
148
|
-
get_raw_value(id)
|
149
|
-
else
|
150
|
-
sensor_data(id, val)
|
151
|
-
end
|
152
|
-
end
|
153
|
-
res
|
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)
|
154
90
|
end
|
155
|
-
|
91
|
+
|
156
92
|
# Returns sensor data for given interval making in-memory summarization
|
157
93
|
# and returns calculated value
|
158
94
|
# @param interval_id [Fixnum]
|
@@ -261,8 +197,9 @@ module PulseMeter
|
|
261
197
|
# @param start_time [Fixnum] unix timestamp of timespan start
|
262
198
|
# @param end_time [Fixnum] unix timestamp of timespan start
|
263
199
|
# @return [Fixnum] optimized interval in seconds.
|
264
|
-
def optimized_interval(start_time, end_time)
|
200
|
+
def optimized_interval(start_time, end_time, skip_optimization = false)
|
265
201
|
res_interval = interval
|
202
|
+
return res_interval if skip_optimization
|
266
203
|
timespan = end_time - start_time
|
267
204
|
while timespan / res_interval > MAX_TIMESPAN_POINTS - 1
|
268
205
|
res_interval *= 2
|
@@ -270,7 +207,30 @@ module PulseMeter
|
|
270
207
|
res_interval
|
271
208
|
end
|
272
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
|
273
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
|
274
234
|
end
|
275
235
|
end
|
276
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
|
+
|
@@ -2,27 +2,15 @@ module PulseMeter
|
|
2
2
|
module Sensor
|
3
3
|
module Timelined
|
4
4
|
# Calculates max value in interval
|
5
|
-
class Max <
|
5
|
+
class Max < ZSetBased
|
6
6
|
|
7
|
-
def
|
8
|
-
command_aggregator.zadd(key, value, "#{value}::#{uniqid}")
|
7
|
+
def update(key)
|
9
8
|
command_aggregator.zremrangebyrank(key, 0, -2)
|
10
9
|
end
|
11
10
|
|
12
|
-
def
|
13
|
-
|
14
|
-
|
15
|
-
max_el = redis.zrange(key, -1, -1)[0]
|
16
|
-
redis.zscore(key, max_el)
|
17
|
-
else
|
18
|
-
nil
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
private
|
23
|
-
|
24
|
-
def deflate(value)
|
25
|
-
value.to_f
|
11
|
+
def calculate(key, _)
|
12
|
+
max_el = redis.zrange(key, -1, -1)[0]
|
13
|
+
redis.zscore(key, max_el)
|
26
14
|
end
|
27
15
|
|
28
16
|
end
|
@@ -2,27 +2,15 @@ module PulseMeter
|
|
2
2
|
module Sensor
|
3
3
|
module Timelined
|
4
4
|
# Calculates min value in interval
|
5
|
-
class Min <
|
5
|
+
class Min < ZSetBased
|
6
6
|
|
7
|
-
def
|
8
|
-
command_aggregator.zadd(key, value, "#{value}::#{uniqid}")
|
7
|
+
def update(key)
|
9
8
|
command_aggregator.zremrangebyrank(key, 1, -1)
|
10
9
|
end
|
11
10
|
|
12
|
-
def
|
13
|
-
|
14
|
-
|
15
|
-
min_el = redis.zrange(key, 0, 0)[0]
|
16
|
-
redis.zscore(key, min_el)
|
17
|
-
else
|
18
|
-
nil
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
private
|
23
|
-
|
24
|
-
def deflate(value)
|
25
|
-
value.to_f
|
11
|
+
def calculate(key, _)
|
12
|
+
min_el = redis.zrange(key, 0, 0)[0]
|
13
|
+
redis.zscore(key, min_el)
|
26
14
|
end
|
27
15
|
|
28
16
|
end
|
@@ -2,7 +2,7 @@ module PulseMeter
|
|
2
2
|
module Sensor
|
3
3
|
module Timelined
|
4
4
|
# Calculates n'th percentile in interval
|
5
|
-
class MultiPercentile <
|
5
|
+
class MultiPercentile < ZSetBased
|
6
6
|
attr_reader :p_value
|
7
7
|
|
8
8
|
def initialize(name, options)
|
@@ -11,22 +11,13 @@ module PulseMeter
|
|
11
11
|
super(name, options)
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
percentile = if count > 0
|
22
|
-
position = p > 0 ? (p * count).round - 1 : 0
|
23
|
-
el = redis.zrange(key, position, position)[0]
|
24
|
-
redis.zscore(key, el)
|
25
|
-
else
|
26
|
-
nil
|
27
|
-
end
|
28
|
-
acc[p] = percentile
|
29
|
-
end.to_json
|
14
|
+
def calculate(key, count)
|
15
|
+
count =
|
16
|
+
@p_value.each_with_object({}) { |p, acc|
|
17
|
+
position = p > 0 ? (p * count).round - 1 : 0
|
18
|
+
el = redis.zrange(key, position, position)[0]
|
19
|
+
acc[p] = redis.zscore(key, el)
|
20
|
+
}.to_json
|
30
21
|
end
|
31
22
|
|
32
23
|
private
|
@@ -2,7 +2,7 @@ module PulseMeter
|
|
2
2
|
module Sensor
|
3
3
|
module Timelined
|
4
4
|
# Calculates n'th percentile in interval
|
5
|
-
class Percentile <
|
5
|
+
class Percentile < ZSetBased
|
6
6
|
attr_reader :p_value
|
7
7
|
|
8
8
|
def initialize(name, options)
|
@@ -10,25 +10,10 @@ module PulseMeter
|
|
10
10
|
super(name, options)
|
11
11
|
end
|
12
12
|
|
13
|
-
def
|
14
|
-
|
15
|
-
|
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)
|
23
|
-
else
|
24
|
-
nil
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
def deflate(value)
|
31
|
-
value.to_f
|
13
|
+
def calculate(key, count)
|
14
|
+
position = @p_value > 0 ? (@p_value * count).round - 1 : 0
|
15
|
+
el = redis.zrange(key, position, position)[0]
|
16
|
+
redis.zscore(key, el)
|
32
17
|
end
|
33
18
|
|
34
19
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module PulseMeter
|
2
|
+
module Sensor
|
3
|
+
module Timelined
|
4
|
+
# Calculates min value in interval
|
5
|
+
class ZSetBased < Timeline
|
6
|
+
|
7
|
+
def update(_)
|
8
|
+
end
|
9
|
+
|
10
|
+
def calculate(key)
|
11
|
+
0
|
12
|
+
end
|
13
|
+
|
14
|
+
def aggregate_event(key, value)
|
15
|
+
command_aggregator.zadd(key, value, "#{value}::#{uniqid}")
|
16
|
+
update(key)
|
17
|
+
end
|
18
|
+
|
19
|
+
def summarize(key)
|
20
|
+
count = redis.zcard(key)
|
21
|
+
if count > 0
|
22
|
+
calculate(key, count)
|
23
|
+
else
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def deflate(value)
|
31
|
+
value.to_f
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
data/lib/pulse-meter/version.rb
CHANGED
@@ -38,3 +38,14 @@
|
|
38
38
|
#dynamic-plotarea {
|
39
39
|
}
|
40
40
|
|
41
|
+
.ui-timepicker-div .ui-widget-header { margin-bottom: 8px; }
|
42
|
+
.ui-timepicker-div dl { text-align: left; }
|
43
|
+
.ui-timepicker-div dl dt { height: 25px; margin-bottom: -25px; }
|
44
|
+
.ui-timepicker-div dl dd { margin: 0 10px 10px 65px; }
|
45
|
+
.ui-timepicker-div td { font-size: 90%; }
|
46
|
+
.ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; }
|
47
|
+
|
48
|
+
.modal-body hr {
|
49
|
+
margin-top: 0px;
|
50
|
+
margin-bottom: 4px;
|
51
|
+
}
|
@@ -25,22 +25,6 @@ module PulseMeter
|
|
25
25
|
|
26
26
|
protected
|
27
27
|
|
28
|
-
def ensure_sensor_match!
|
29
|
-
intervals = []
|
30
|
-
sensors.each do |s|
|
31
|
-
unless s.type < PulseMeter::Sensor::Timeline
|
32
|
-
raise NotATimelinedSensorInWidget, "sensor `#{s.name}' is not timelined"
|
33
|
-
end
|
34
|
-
intervals << s.interval
|
35
|
-
end
|
36
|
-
|
37
|
-
unless intervals.all?{|i| i == intervals.first}
|
38
|
-
interval_notice = sensors.map{|s| "#{s.name}: #{s.interval}"}.join(', ')
|
39
|
-
raise DifferentSensorIntervalsInWidget, "Sensors with different intervals in a single widget: #{interval_notice}"
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
|
44
28
|
def gauge_series_data
|
45
29
|
ensure_gauge_indicators!
|
46
30
|
sensors.map do |s|
|
@@ -43,7 +43,7 @@ module PulseMeter
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def series_data(from, till)
|
46
|
-
|
46
|
+
ensure_equal_intervals!
|
47
47
|
sensor_datas = sensors.map{ |s|
|
48
48
|
s.timeline_data(from, till, show_last_point)
|
49
49
|
}
|
@@ -73,7 +73,7 @@ module PulseMeter
|
|
73
73
|
}
|
74
74
|
end
|
75
75
|
|
76
|
-
def
|
76
|
+
def ensure_equal_intervals!
|
77
77
|
intervals = []
|
78
78
|
sensors.each do |s|
|
79
79
|
unless s.type < PulseMeter::Sensor::Timeline
|
@@ -1,16 +1,21 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe PulseMeter::Mixins::Cmd do
|
4
|
-
class
|
5
|
-
|
4
|
+
class CmdDummy
|
5
|
+
include PulseMeter::Mixins::Cmd
|
6
6
|
|
7
|
-
def
|
8
|
-
|
7
|
+
def initialize(redis)
|
8
|
+
@redis = redis
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_redis
|
12
|
+
@redis
|
9
13
|
end
|
10
14
|
end
|
11
15
|
|
12
|
-
let(:
|
13
|
-
|
16
|
+
let(:redis){ MockRedis.new }
|
17
|
+
let(:dummy){ CmdDummy.new(redis) }
|
18
|
+
before{ PulseMeter.redis = redis }
|
14
19
|
|
15
20
|
describe "#fail!" do
|
16
21
|
it "prints given message and exits" do
|
@@ -181,4 +181,24 @@ describe PulseMeter::Mixins::Utils do
|
|
181
181
|
subsets.sort.should == [[], [1], [2], [1, 2]].sort
|
182
182
|
end
|
183
183
|
end
|
184
|
+
|
185
|
+
describe '#parse_time' do
|
186
|
+
context "when argument is a valid YYYYmmddHHMMSS string" do
|
187
|
+
it "should correct Time object" do
|
188
|
+
t = dummy.parse_time("19700101000000")
|
189
|
+
t.should be_kind_of(Time)
|
190
|
+
t.to_i.should == 0
|
191
|
+
end
|
192
|
+
end
|
193
|
+
context "when argument is an invalid YYYYmmddHHMMSS string" do
|
194
|
+
it "should raise ArgumentError" do
|
195
|
+
expect{ dummy.parse_time("19709901000000") }.to raise_exception(ArgumentError)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
context "when argument is not a YYYYmmddHHMMSS string" do
|
199
|
+
it "should raise ArgumentError" do
|
200
|
+
expect{ dummy.parse_time("197099010000000") }.to raise_exception(ArgumentError)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
184
204
|
end
|
@@ -4,7 +4,7 @@ describe PulseMeter::Observer do
|
|
4
4
|
|
5
5
|
context "instance methods observation" do
|
6
6
|
|
7
|
-
class
|
7
|
+
class ObservedDummy
|
8
8
|
attr_reader :count
|
9
9
|
|
10
10
|
def initialize
|
@@ -23,20 +23,20 @@ describe PulseMeter::Observer do
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
-
let!(:dummy) {
|
26
|
+
let!(:dummy) {ObservedDummy.new}
|
27
27
|
let!(:sensor) {PulseMeter::Sensor::Counter.new(:foo)}
|
28
28
|
before do
|
29
|
-
[:incr, :error].each {|m| described_class.unobserve_method(
|
29
|
+
[:incr, :error].each {|m| described_class.unobserve_method(ObservedDummy, m)}
|
30
30
|
end
|
31
31
|
|
32
32
|
def create_observer(method = :incr, increment = 1)
|
33
|
-
described_class.observe_method(
|
33
|
+
described_class.observe_method(ObservedDummy, method, sensor) do |*args|
|
34
34
|
event(increment)
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
38
|
def remove_observer(method = :incr)
|
39
|
-
described_class.unobserve_method(
|
39
|
+
described_class.unobserve_method(ObservedDummy, method)
|
40
40
|
end
|
41
41
|
|
42
42
|
describe ".observe_method" do
|
@@ -53,7 +53,7 @@ describe PulseMeter::Observer do
|
|
53
53
|
end
|
54
54
|
|
55
55
|
it "passes methods' params to block" do
|
56
|
-
described_class.observe_method(
|
56
|
+
described_class.observe_method(ObservedDummy, :incr, sensor) do |time, cnt|
|
57
57
|
event(cnt)
|
58
58
|
end
|
59
59
|
|
@@ -63,7 +63,7 @@ describe PulseMeter::Observer do
|
|
63
63
|
|
64
64
|
it "passes execution time in milliseconds to block" do
|
65
65
|
Timecop.freeze do
|
66
|
-
described_class.observe_method(
|
66
|
+
described_class.observe_method(ObservedDummy, :incr, sensor) do |time, cnt|
|
67
67
|
event(time)
|
68
68
|
end
|
69
69
|
|
@@ -73,7 +73,7 @@ describe PulseMeter::Observer do
|
|
73
73
|
end
|
74
74
|
|
75
75
|
it "does not break observed method even is observer raises error" do
|
76
|
-
described_class.observe_method(
|
76
|
+
described_class.observe_method(ObservedDummy, :incr, sensor) do |*args|
|
77
77
|
raise RuntimeError
|
78
78
|
end
|
79
79
|
|
@@ -125,7 +125,7 @@ describe PulseMeter::Observer do
|
|
125
125
|
|
126
126
|
context "class methods observation" do
|
127
127
|
|
128
|
-
class
|
128
|
+
class ObservedDummy
|
129
129
|
@@count = 0
|
130
130
|
class << self
|
131
131
|
def count
|
@@ -149,21 +149,21 @@ describe PulseMeter::Observer do
|
|
149
149
|
end
|
150
150
|
end
|
151
151
|
|
152
|
-
let!(:dummy) {
|
152
|
+
let!(:dummy) {ObservedDummy}
|
153
153
|
let!(:sensor) {PulseMeter::Sensor::Counter.new(:foo)}
|
154
154
|
before do
|
155
155
|
dummy.reset
|
156
|
-
[:incr, :error].each {|m| described_class.unobserve_class_method(
|
156
|
+
[:incr, :error].each {|m| described_class.unobserve_class_method(ObservedDummy, m)}
|
157
157
|
end
|
158
158
|
|
159
159
|
def create_observer(method = :incr, increment = 1)
|
160
|
-
described_class.observe_class_method(
|
160
|
+
described_class.observe_class_method(ObservedDummy, method, sensor) do |*args|
|
161
161
|
event(increment)
|
162
162
|
end
|
163
163
|
end
|
164
164
|
|
165
165
|
def remove_observer(method = :incr)
|
166
|
-
described_class.unobserve_class_method(
|
166
|
+
described_class.unobserve_class_method(ObservedDummy, method)
|
167
167
|
end
|
168
168
|
|
169
169
|
describe ".observe_class_method" do
|
@@ -180,7 +180,7 @@ describe PulseMeter::Observer do
|
|
180
180
|
end
|
181
181
|
|
182
182
|
it "passes methods' params to block" do
|
183
|
-
described_class.observe_class_method(
|
183
|
+
described_class.observe_class_method(ObservedDummy, :incr, sensor) do |time, cnt|
|
184
184
|
event(cnt)
|
185
185
|
end
|
186
186
|
|
@@ -190,7 +190,7 @@ describe PulseMeter::Observer do
|
|
190
190
|
|
191
191
|
it "passes execution time in milliseconds to block" do
|
192
192
|
Timecop.freeze do
|
193
|
-
described_class.observe_class_method(
|
193
|
+
described_class.observe_class_method(ObservedDummy, :incr, sensor) do |time, cnt|
|
194
194
|
event(time)
|
195
195
|
end
|
196
196
|
|
@@ -200,7 +200,7 @@ describe PulseMeter::Observer do
|
|
200
200
|
end
|
201
201
|
|
202
202
|
it "does not break observed method even is observer raises error" do
|
203
|
-
described_class.observe_class_method(
|
203
|
+
described_class.observe_class_method(ObservedDummy, :incr, sensor) do |*args|
|
204
204
|
raise RuntimeError
|
205
205
|
end
|
206
206
|
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pulse-meter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2013-02-02 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: gon-sinatra
|
@@ -429,6 +429,7 @@ files:
|
|
429
429
|
- lib/pulse-meter/sensor/indicator.rb
|
430
430
|
- lib/pulse-meter/sensor/multi.rb
|
431
431
|
- lib/pulse-meter/sensor/timeline.rb
|
432
|
+
- lib/pulse-meter/sensor/timeline_reduce.rb
|
432
433
|
- lib/pulse-meter/sensor/timelined/average.rb
|
433
434
|
- lib/pulse-meter/sensor/timelined/counter.rb
|
434
435
|
- lib/pulse-meter/sensor/timelined/hashed_counter.rb
|
@@ -440,6 +441,7 @@ files:
|
|
440
441
|
- lib/pulse-meter/sensor/timelined/multi_percentile.rb
|
441
442
|
- lib/pulse-meter/sensor/timelined/percentile.rb
|
442
443
|
- lib/pulse-meter/sensor/timelined/uniq_counter.rb
|
444
|
+
- lib/pulse-meter/sensor/timelined/zset_based.rb
|
443
445
|
- lib/pulse-meter/sensor/uniq_counter.rb
|
444
446
|
- lib/pulse-meter/server.rb
|
445
447
|
- lib/pulse-meter/server/command_line_options.rb
|
@@ -605,6 +607,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
605
607
|
- - ! '>='
|
606
608
|
- !ruby/object:Gem::Version
|
607
609
|
version: '0'
|
610
|
+
segments:
|
611
|
+
- 0
|
612
|
+
hash: -1795310197501122422
|
608
613
|
requirements: []
|
609
614
|
rubyforge_project:
|
610
615
|
rubygems_version: 1.8.23
|