pulse-meter 0.4.1 → 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://secure.travis-ci.org/savonarola/pulse-meter.png)](http://travis-ci.org/savonarola/pulse-meter)
|
2
|
+
[![Code Climate](https://codeclimate.com/badge.png)](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
|