pulse-meter 0.1.10 → 0.1.11

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -44,6 +44,7 @@ following static sensors are available:
44
44
 
45
45
  * Counter
46
46
  * Hashed Counter
47
+ * Unique Counter
47
48
  * Indicator
48
49
 
49
50
  They have no web visualisation interface and they are assumed to be used by external visualisation tools.
@@ -64,6 +65,7 @@ The following timeline sensors are available:
64
65
  * Min value
65
66
  * Median value
66
67
  * Percentile
68
+ * Unique counter
67
69
 
68
70
  There are several caveats with timeline sensors:
69
71
 
@@ -140,6 +142,63 @@ Just create sensor objects and write data. Some examples below.
140
142
  # prints somewhat like
141
143
  # 2012-05-24 11:07:00 +0400: 3.0
142
144
  # 2012-05-24 11:08:00 +0400: 7.0
145
+
146
+ There is also an alternative and a bit more DRY way for sensor creation, management and usage using <tt>PulseMeter::Sensor::Configuration</tt> class. It is also convenient for creating a bunch of sensors from some configuration data.
147
+
148
+ require 'pulse-meter'
149
+ PulseMeter.redis = Redis.new
150
+
151
+ sensors = PulseMeter::Sensor::Configuration.new(
152
+ my_counter: {sensor_type: 'counter'},
153
+ my_value: {sensor_type: 'indicator'},
154
+ my_h_counter: {sensor_type: 'hashed_counter'},
155
+ my_t_counter: {
156
+ sensor_type: 'timelined/counter',
157
+ args: {
158
+ interval: 60, # count for each minute
159
+ ttl: 24 * 60 * 60 # keep data one day
160
+ }
161
+ },
162
+ my_t_max: {
163
+ sensor_type: 'timelined/max',
164
+ args: {
165
+ interval: 60, # count for each minute
166
+ ttl: 24 * 60 * 60 # keep data one day
167
+ }
168
+ }
169
+ )
170
+
171
+ sensors.my_counter(1)
172
+ sensors.my_counter(2)
173
+ puts sensors.sensor(:my_counter).value
174
+
175
+ sensors.my_value(3.14)
176
+ sensors.my_value(2.71)
177
+ puts sensors.sensor(:my_value).value
178
+
179
+ sensors.my_h_counter(:x => 1)
180
+ sensors.my_h_counter(:y => 5)
181
+ sensors.my_h_counter(:y => 1)
182
+ p sensors.sensor(:my_h_counter).value
183
+
184
+ sensors.my_t_counter(1)
185
+ sensors.my_t_counter(1)
186
+ sleep(60)
187
+ sensors.my_t_counter(1)
188
+ sensors.sensor(:my_t_counter).timeline(2 * 60).each do |v|
189
+ puts "#{v.start_time}: #{v.value}"
190
+ end
191
+
192
+ sensors.my_t_max(3)
193
+ sensors.my_t_max(1)
194
+ sensors.my_t_max(2)
195
+ sleep(60)
196
+ sensors.my_t_max(5)
197
+ sensors.my_t_max(7)
198
+ sensors.my_t_max(6)
199
+ sensors.sensor(:my_t_max).timeline(2 * 60).each do |v|
200
+ puts "#{v.start_time}: #{v.value}"
201
+ end
143
202
 
144
203
  ## Command line interface
145
204
 
@@ -219,84 +278,103 @@ It can be found in <tt>examples/full</tt> folder. To run it, execute
219
278
  at project root and visit
220
279
  <tt>http://localhost:9292</tt> at your browser.
221
280
 
222
- <tt>client.rb</tt> imitating users visiting some imaginary site
281
+ <tt>client.rb</tt> imitating users visiting some imaginary site.
223
282
 
224
283
  require "pulse-meter"
225
284
 
226
285
  PulseMeter.redis = Redis.new
227
286
 
228
- requests_per_minute = PulseMeter::Sensor::Timelined::Counter.new(:requests_per_minute,
229
- :annotation => 'Requests per minute',
230
- :interval => 60,
231
- :ttl => 60 * 60 * 24 # keep data one day
232
- )
233
-
234
- requests_per_hour = PulseMeter::Sensor::Timelined::Counter.new(:requests_per_hour,
235
- :annotation => 'Requests per hour',
236
- :interval => 60 * 60,
237
- :ttl => 60 * 60 * 24 * 30 # keep data 30 days
287
+ sensors = PulseMeter::Sensor::Configuration.new(
288
+ requests_per_minute: {
289
+ sensor_type: 'timelined/counter',
290
+ args: {
291
+ annotation: 'Requests per minute',
292
+ interval: 60,
293
+ ttl: 60 * 60 * 24 # keep data one day
294
+ }
295
+ },
296
+ requests_per_hour: {
297
+ sensor_type: 'timelined/counter',
298
+ args: {
299
+ annotation: 'Requests per hour',
300
+ interval: 60 * 60,
301
+ ttl: 60 * 60 * 24 * 30 # keep data 30 days
302
+ }
303
+ },
238
304
  # when ActiveSupport extentions are loaded, a better way is to write just
239
305
  # :interval => 1.hour,
240
306
  # :ttl => 30.days
241
- )
242
-
243
- errors_per_minute = PulseMeter::Sensor::Timelined::Counter.new(:errors_per_minute,
244
- :annotation => 'Errors per minute',
245
- :interval => 60,
246
- :ttl => 60 * 60 * 24
247
- )
248
-
249
- errors_per_hour = PulseMeter::Sensor::Timelined::Counter.new(:errors_per_hour,
250
- :annotation => 'Errors per hour',
251
- :interval => 60 * 60,
252
- :ttl => 60 * 60 * 24 * 30
253
- )
254
-
255
- longest_minute_request = PulseMeter::Sensor::Timelined::Max.new(:longest_minute_request,
256
- :annotation => 'Longest minute requests',
257
- :interval => 60,
258
- :ttl => 60 * 60 * 24
259
- )
260
-
261
- shortest_minute_request = PulseMeter::Sensor::Timelined::Min.new(:shortest_minute_request,
262
- :annotation => 'Shortest minute requests',
263
- :interval => 60,
264
- :ttl => 60 * 60 * 24
265
- )
266
-
267
- perc90_minute_request = PulseMeter::Sensor::Timelined::Percentile.new(:perc90_minute_request,
268
- :annotation => 'Minute request 90-percent percentile',
269
- :interval => 60,
270
- :ttl => 60 * 60 * 24,
271
- :p => 0.9
307
+ errors_per_minute: {
308
+ sensor_type: 'timelined/counter',
309
+ args: {
310
+ annotation: 'Errors per minute',
311
+ interval: 60,
312
+ ttl: 60 * 60 * 24
313
+ }
314
+ },
315
+ errors_per_hour: {
316
+ sensor_type: 'timelined/counter',
317
+ args: {
318
+ annotation: 'Errors per hour',
319
+ interval: 60 * 60,
320
+ ttl: 60 * 60 * 24 * 30
321
+ }
322
+ },
323
+ longest_minute_request: {
324
+ sensor_type: 'timelined/max',
325
+ args: {
326
+ annotation: 'Longest minute requests',
327
+ interval: 60,
328
+ ttl: 60 * 60 * 24
329
+ }
330
+ },
331
+ shortest_minute_request: {
332
+ sensor_type: 'timelined/min',
333
+ args: {
334
+ annotation: 'Shortest minute requests',
335
+ interval: 60,
336
+ ttl: 60 * 60 * 24
337
+ }
338
+ },
339
+ perc90_minute_request: {
340
+ sensor_type: 'timelined/percentile',
341
+ args: {
342
+ annotation: 'Minute request 90-percent percentile',
343
+ interval: 60,
344
+ ttl: 60 * 60 * 24,
345
+ p: 0.9
346
+ }
347
+ }
272
348
  )
273
349
 
274
350
  agent_names = [:ie, :firefox, :chrome, :other]
275
- hour_agents = agent_names.each_with_object({}) do |agent, h|
276
- h[agent] = PulseMeter::Sensor::Timelined::Counter.new(agent,
277
- :annotation => "Requests from #{agent} browser",
278
- :interval => 60 * 60,
279
- :ttl => 60 * 60 * 24 * 30
351
+ agent_names.each do |agent|
352
+ sensors.add_sensor(agent,
353
+ sensor_type: 'timelined/counter',
354
+ args: {
355
+ annotation: "Requests from #{agent} browser",
356
+ interval: 60 * 60,
357
+ ttl: 60 * 60 * 24 * 30
358
+ }
280
359
  )
281
360
  end
282
361
 
283
-
284
362
  while true
285
- requests_per_minute.event(1)
286
- requests_per_hour.event(1)
363
+ sensors.requests_per_minute(1)
364
+ sensors.requests_per_hour(1)
287
365
 
288
366
  if Random.rand(10) < 1 # let "errors" sometimes occur
289
- errors_per_minute.event(1)
290
- errors_per_hour.event(1)
367
+ sensors.errors_per_minute(1)
368
+ sensors.errors_per_hour(1)
291
369
  end
292
370
 
293
371
  request_time = 0.1 + Random.rand
294
372
 
295
- longest_minute_request.event(request_time)
296
- shortest_minute_request.event(request_time)
297
- perc90_minute_request.event(request_time)
373
+ sensors.longest_minute_request(request_time)
374
+ sensors.shortest_minute_request(request_time)
375
+ sensors.perc90_minute_request(request_time)
298
376
 
299
- agent_counter = hour_agents[agent_names.shuffle.first]
377
+ agent_counter = sensors.sensor(agent_names.shuffle.first)
300
378
  agent_counter.event(1)
301
379
 
302
380
  sleep(Random.rand / 10)
@@ -1,2 +1,2 @@
1
1
  web: bundle exec rackup server.ru
2
- sensor_data_generator: bundle exec ruby client.rb
2
+ sensor_data_generator: bundle exec ruby client.rb
@@ -4,78 +4,97 @@ require "pulse-meter"
4
4
 
5
5
  PulseMeter.redis = Redis.new
6
6
 
7
- requests_per_minute = PulseMeter::Sensor::Timelined::Counter.new(:requests_per_minute,
8
- :annotation => 'Requests per minute',
9
- :interval => 60,
10
- :ttl => 60 * 60 * 24 # keep data one day
11
- )
12
-
13
- requests_per_hour = PulseMeter::Sensor::Timelined::Counter.new(:requests_per_hour,
14
- :annotation => 'Requests per hour',
15
- :interval => 60 * 60,
16
- :ttl => 60 * 60 * 24 * 30 # keep data 30 days
7
+ sensors = PulseMeter::Sensor::Configuration.new(
8
+ requests_per_minute: {
9
+ sensor_type: 'timelined/counter',
10
+ args: {
11
+ annotation: 'Requests per minute',
12
+ interval: 60,
13
+ ttl: 60 * 60 * 24 # keep data one day
14
+ }
15
+ },
16
+ requests_per_hour: {
17
+ sensor_type: 'timelined/counter',
18
+ args: {
19
+ annotation: 'Requests per hour',
20
+ interval: 60 * 60,
21
+ ttl: 60 * 60 * 24 * 30 # keep data 30 days
22
+ }
23
+ },
17
24
  # when ActiveSupport extentions are loaded, a better way is to write just
18
25
  # :interval => 1.hour,
19
26
  # :ttl => 30.days
20
- )
21
-
22
- errors_per_minute = PulseMeter::Sensor::Timelined::Counter.new(:errors_per_minute,
23
- :annotation => 'Errors per minute',
24
- :interval => 60,
25
- :ttl => 60 * 60 * 24
26
- )
27
-
28
- errors_per_hour = PulseMeter::Sensor::Timelined::Counter.new(:errors_per_hour,
29
- :annotation => 'Errors per hour',
30
- :interval => 60 * 60,
31
- :ttl => 60 * 60 * 24 * 30
32
- )
33
-
34
- longest_minute_request = PulseMeter::Sensor::Timelined::Max.new(:longest_minute_request,
35
- :annotation => 'Longest minute requests',
36
- :interval => 60,
37
- :ttl => 60 * 60 * 24
38
- )
39
-
40
- shortest_minute_request = PulseMeter::Sensor::Timelined::Min.new(:shortest_minute_request,
41
- :annotation => 'Shortest minute requests',
42
- :interval => 60,
43
- :ttl => 60 * 60 * 24
44
- )
45
-
46
- perc90_minute_request = PulseMeter::Sensor::Timelined::Percentile.new(:perc90_minute_request,
47
- :annotation => 'Minute request 90-percent percentile',
48
- :interval => 60,
49
- :ttl => 60 * 60 * 24,
50
- :p => 0.9
27
+ errors_per_minute: {
28
+ sensor_type: 'timelined/counter',
29
+ args: {
30
+ annotation: 'Errors per minute',
31
+ interval: 60,
32
+ ttl: 60 * 60 * 24
33
+ }
34
+ },
35
+ errors_per_hour: {
36
+ sensor_type: 'timelined/counter',
37
+ args: {
38
+ annotation: 'Errors per hour',
39
+ interval: 60 * 60,
40
+ ttl: 60 * 60 * 24 * 30
41
+ }
42
+ },
43
+ longest_minute_request: {
44
+ sensor_type: 'timelined/max',
45
+ args: {
46
+ annotation: 'Longest minute requests',
47
+ interval: 60,
48
+ ttl: 60 * 60 * 24
49
+ }
50
+ },
51
+ shortest_minute_request: {
52
+ sensor_type: 'timelined/min',
53
+ args: {
54
+ annotation: 'Shortest minute requests',
55
+ interval: 60,
56
+ ttl: 60 * 60 * 24
57
+ }
58
+ },
59
+ perc90_minute_request: {
60
+ sensor_type: 'timelined/percentile',
61
+ args: {
62
+ annotation: 'Minute request 90-percent percentile',
63
+ interval: 60,
64
+ ttl: 60 * 60 * 24,
65
+ p: 0.9
66
+ }
67
+ }
51
68
  )
52
69
 
53
70
  agent_names = [:ie, :firefox, :chrome, :other]
54
- hour_agents = agent_names.each_with_object({}) do |agent, h|
55
- h[agent] = PulseMeter::Sensor::Timelined::Counter.new(agent,
56
- :annotation => "Requests from #{agent} browser",
57
- :interval => 60 * 60,
58
- :ttl => 60 * 60 * 24 * 30
71
+ agent_names.each do |agent|
72
+ sensors.add_sensor(agent,
73
+ sensor_type: 'timelined/counter',
74
+ args: {
75
+ annotation: "Requests from #{agent} browser",
76
+ interval: 60 * 60,
77
+ ttl: 60 * 60 * 24 * 30
78
+ }
59
79
  )
60
80
  end
61
81
 
62
-
63
82
  while true
64
- requests_per_minute.event(1)
65
- requests_per_hour.event(1)
83
+ sensors.requests_per_minute(1)
84
+ sensors.requests_per_hour(1)
66
85
 
67
86
  if Random.rand(10) < 1 # let "errors" sometimes occur
68
- errors_per_minute.event(1)
69
- errors_per_hour.event(1)
87
+ sensors.errors_per_minute(1)
88
+ sensors.errors_per_hour(1)
70
89
  end
71
90
 
72
91
  request_time = 0.1 + Random.rand
73
92
 
74
- longest_minute_request.event(request_time)
75
- shortest_minute_request.event(request_time)
76
- perc90_minute_request.event(request_time)
93
+ sensors.longest_minute_request(request_time)
94
+ sensors.shortest_minute_request(request_time)
95
+ sensors.perc90_minute_request(request_time)
77
96
 
78
- agent_counter = hour_agents[agent_names.shuffle.first]
97
+ agent_counter = sensors.sensor(agent_names.shuffle.first)
79
98
  agent_counter.event(1)
80
99
 
81
100
  sleep(Random.rand / 10)
@@ -0,0 +1,59 @@
1
+ $: << File.join(File.absolute_path(__FILE__), '..', 'lib')
2
+
3
+ require 'pulse-meter'
4
+ PulseMeter.redis = Redis.new
5
+
6
+ # static sensor examples
7
+
8
+ sensors = PulseMeter::Sensor::Configuration.new(
9
+ my_counter: {sensor_type: 'counter'},
10
+ my_value: {sensor_type: 'indicator'},
11
+ my_h_counter: {sensor_type: 'hashed_counter'},
12
+ my_t_counter: {
13
+ sensor_type: 'timelined/counter',
14
+ args: {
15
+ interval: 60, # count for each minute
16
+ ttl: 24 * 60 * 60 # keep data one day
17
+ }
18
+ },
19
+ my_t_max: {
20
+ sensor_type: 'timelined/max',
21
+ args: {
22
+ interval: 60, # count for each minute
23
+ ttl: 24 * 60 * 60 # keep data one day
24
+ }
25
+ }
26
+ )
27
+
28
+ sensors.my_counter(1)
29
+ sensors.my_counter(2)
30
+ puts sensors.sensor(:my_counter).value
31
+
32
+ sensors.my_value(3.14)
33
+ sensors.my_value(2.71)
34
+ puts sensors.sensor(:my_value).value
35
+
36
+ sensors.my_h_counter(:x => 1)
37
+ sensors.my_h_counter(:y => 5)
38
+ sensors.my_h_counter(:y => 1)
39
+ p sensors.sensor(:my_h_counter).value
40
+
41
+ sensors.my_t_counter(1)
42
+ sensors.my_t_counter(1)
43
+ sleep(60)
44
+ sensors.my_t_counter(1)
45
+ sensors.sensor(:my_t_counter).timeline(2 * 60).each do |v|
46
+ puts "#{v.start_time}: #{v.value}"
47
+ end
48
+
49
+ sensors.my_t_max(3)
50
+ sensors.my_t_max(1)
51
+ sensors.my_t_max(2)
52
+ sleep(60)
53
+ sensors.my_t_max(5)
54
+ sensors.my_t_max(7)
55
+ sensors.my_t_max(6)
56
+ sensors.sensor(:my_t_max).timeline(2 * 60).each do |v|
57
+ puts "#{v.start_time}: #{v.value}"
58
+ end
59
+
File without changes
data/lib/cmd.rb CHANGED
@@ -2,6 +2,29 @@ require 'thor'
2
2
  require 'terminal-table'
3
3
  require 'time'
4
4
  require 'json'
5
+ require 'csv'
6
+
7
+ module Enumerable
8
+ def convert_time
9
+ map do |el|
10
+ if el.is_a?(Time)
11
+ el.to_i
12
+ else
13
+ el
14
+ end
15
+ end
16
+ end
17
+
18
+ def to_table(format = nil)
19
+ if "csv" == format
20
+ CSV.generate(:col_sep => ';') do |csv|
21
+ self.each {|row| csv << row.convert_time}
22
+ end
23
+ else
24
+ self.each_with_object(Terminal::Table.new) {|row, table| table << row}
25
+ end
26
+ end
27
+ end
5
28
 
6
29
  module Cmd
7
30
  class All < Thor
@@ -25,18 +48,19 @@ module Cmd
25
48
  PulseMeter::Sensor::Timeline.list_objects
26
49
  end
27
50
 
28
- def all_sensors_table(title = nil)
29
- table = Terminal::Table.new :title => title
30
- table << ["Name", "Class", "ttl", "raw data ttl", "interval", "reduce delay"]
31
- table << :separator
51
+ def all_sensors_table(format = nil)
52
+ data = [
53
+ ["Name", "Class", "ttl", "raw data ttl", "interval", "reduce delay"],
54
+ ]
55
+ data << :separator unless format == 'csv'
32
56
  all_sensors.each do |s|
33
57
  if s.kind_of? PulseMeter::Sensor::Timeline
34
- table << [s.name, s.class, s.ttl, s.raw_data_ttl, s.interval, s.reduce_delay]
58
+ data << [s.name, s.class, s.ttl, s.raw_data_ttl, s.interval, s.reduce_delay]
35
59
  else
36
- table << [s.name, s.class] + ['-'] * 4
60
+ data << [s.name, s.class] + [''] * 4
37
61
  end
38
62
  end
39
- table
63
+ data.to_table(format)
40
64
  end
41
65
 
42
66
  def fail!(description = nil)
@@ -53,15 +77,17 @@ module Cmd
53
77
 
54
78
  desc "sensors", "List all sensors available"
55
79
  common_options
80
+ method_option :format, :default => :table, :desc => "Output format: table or csv"
56
81
  def sensors
57
- with_redis {puts all_sensors_table('Registered sensors')}
82
+ with_redis {puts all_sensors_table(options[:format])}
58
83
  end
59
84
 
60
85
  desc "reduce", "Execute reduction for all sensors' raw data"
61
86
  common_options
62
87
  def reduce
63
88
  with_redis do
64
- puts all_sensors_table('Registered sensors to be reduced')
89
+ puts 'Registered sensors to be reduced'
90
+ puts all_sensors_table
65
91
  PulseMeter::Sensor::Timeline.reduce_all_raw
66
92
  puts "DONE"
67
93
  end
@@ -79,24 +105,25 @@ module Cmd
79
105
 
80
106
  desc "timeline NAME SECONDS", "Get sensor's NAME timeline for last SECONDS"
81
107
  common_options
108
+ method_option :format, :default => :table, :desc => "Output format: table or csv"
82
109
  def timeline(name, seconds)
83
110
  with_safe_restore_of(name) do |sensor|
84
- table = Terminal::Table.new
85
- sensor.timeline(seconds).each {|data| table << [data.start_time, data.value || '-']}
86
- puts table
111
+ puts sensor.
112
+ timeline(seconds).
113
+ map {|data| [data.start_time, data.value || '']}.
114
+ to_table(options[:format])
87
115
  end
88
116
  end
89
117
 
90
118
  desc "timeline_within NAME FROM TILL", "Get sensor's NAME timeline in interval. Time format: YYYY-MM-DD HH:MM:SS"
91
119
  common_options
120
+ method_option :format, :default => :table, :desc => "Output format: table or csv"
92
121
  def timeline_within(name, from, till)
93
122
  with_safe_restore_of(name) do |sensor|
94
- table = Terminal::Table.new
95
- sensor.timeline_within(
96
- Time.parse(from),
97
- Time.parse(till)
98
- ).each {|data| table << [data.start_time, data.value || '-']}
99
- puts table
123
+ puts sensor.
124
+ timeline_within(Time.parse(from), Time.parse(till)).
125
+ map {|data| [data.start_time, data.value || '']}.
126
+ to_table(options[:format])
100
127
  end
101
128
  end
102
129
 
@@ -146,5 +173,21 @@ module Cmd
146
173
  end
147
174
  end
148
175
 
176
+ desc "drop NAME DATE_FROM(YYYYmmddHHMMSS) DATE_TO(YYYYmmddHHMMSS)", "Drop timeline data of a particular sensor"
177
+ common_options
178
+ def drop(name, from, to)
179
+ time_from, time_to = [from, to].map do |str|
180
+ str.match(/\A(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\z/) do |m|
181
+ Time.gm(*m.captures.map(&:to_i))
182
+ end
183
+ end
184
+ fail! "DATE_FROM is not a valid timestamp" unless time_from.is_a?(Time)
185
+ fail! "DATE_TO is not a valid timestamp" unless time_to.is_a?(Time)
186
+ with_safe_restore_of(name) do |sensor|
187
+ fail! "Sensor #{name} has no drop_within method" unless sensor.respond_to?(:drop_within)
188
+ sensor.drop_within(time_from, time_to)
189
+ end
190
+ end
191
+
149
192
  end
150
193
  end
data/lib/pulse-meter.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require "redis"
2
+ require "logger"
2
3
  require "pulse-meter/version"
3
4
  require "pulse-meter/mixins/dumper"
4
5
  require "pulse-meter/mixins/utils"
@@ -2,6 +2,8 @@ require 'pulse-meter/sensor/base'
2
2
  require 'pulse-meter/sensor/counter'
3
3
  require 'pulse-meter/sensor/hashed_counter'
4
4
  require 'pulse-meter/sensor/indicator'
5
+ require 'pulse-meter/sensor/remote'
6
+ require 'pulse-meter/sensor/uniq_counter'
5
7
  require 'pulse-meter/sensor/timeline'
6
8
  require 'pulse-meter/sensor/timelined/average'
7
9
  require 'pulse-meter/sensor/timelined/counter'
@@ -10,6 +12,7 @@ require 'pulse-meter/sensor/timelined/min'
10
12
  require 'pulse-meter/sensor/timelined/max'
11
13
  require 'pulse-meter/sensor/timelined/percentile'
12
14
  require 'pulse-meter/sensor/timelined/median'
15
+ require 'pulse-meter/sensor/timelined/uniq_counter'
13
16
 
14
17
  # Top level sensor module
15
18
  module PulseMeter
@@ -43,5 +46,9 @@ module PulseMeter
43
46
  # Exception to be raised when sensor cannot be restored
44
47
  class RestoreError < SensorError; end
45
48
 
49
+ module Remote
50
+ class MessageTooLarge < PulseMeter::SensorError; end
51
+ class ConnectionError < PulseMeter::SensorError; end
52
+ end
46
53
  end
47
54
 
@@ -65,9 +65,9 @@ module PulseMeter
65
65
  # For a block
66
66
  # @yield Executes it within Redis multi
67
67
  def multi
68
- redis.multi
69
- yield
70
- redis.exec
68
+ redis.multi do
69
+ yield
70
+ end
71
71
  end
72
72
 
73
73
  end
@@ -0,0 +1,60 @@
1
+ require 'socket'
2
+ require 'json'
3
+
4
+ module PulseMeter
5
+ module Sensor
6
+
7
+ # Remote sensor, i.e. a simple UDP proxy for sending data without
8
+ # taking in account backend performance issues
9
+ class Remote < Base
10
+
11
+ DEFAULT_PORT = 27182
12
+ DEFAULT_HOST = 'localhost'
13
+
14
+ # @!attribute [r] name
15
+ # @return [String] sensor name
16
+ attr_reader :name
17
+
18
+ # Initializes sensor and creates UDP socket
19
+ # @param name [String] sensor name
20
+ # @option options [Symbol] :host host for remote pulse-meter daemon
21
+ # @option options [Symbol] :port port for remote pulse-meter daemon
22
+ # @raise [BadSensorName] if sensor name is malformed
23
+ # @raise [ConnectionError] if invalid host or port are provided
24
+ def initialize(name, options={})
25
+ @name = name.to_s
26
+ raise BadSensorName, @name unless @name =~ /\A\w+\z/
27
+ @host = options[:host].to_s || DEFAULT_HOST
28
+ @port = options[:port].to_i || DEFAULT_PORT
29
+ @socket = UDPSocket.new
30
+ end
31
+
32
+ # Send value to remote sensor
33
+ # @param value value for remote sensor
34
+ # @raise [ConnectionError] if remote daemon is not available
35
+ # @raise [MessageTooLarge] if event data is too large to be serialized into a UDP datagram
36
+ def event(value)
37
+ events(name => value)
38
+ end
39
+
40
+ # Send values to multiple remote sensors
41
+ # @param event_data hash with remote sensor names as keys end event value for each value as sensor
42
+ def events(event_data)
43
+ raise ArgumentError unless event_data.is_a?(Hash)
44
+ socket_action do
45
+ @socket.send(event_data.to_json, 0, @host, @port)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def socket_action
52
+ yield
53
+ rescue SocketError, Errno::EADDRNOTAVAIL, Errno::EINVAL => exc
54
+ raise PulseMeter::Remote::ConnectionError, exc.to_s
55
+ rescue Errno::EMSGSIZE => exc
56
+ raise PulseMeter::Remote::MessageTooLarge, exc.to_s
57
+ end
58
+ end
59
+ end
60
+ end
@@ -149,7 +149,6 @@ module PulseMeter
149
149
  res_interval
150
150
  end
151
151
 
152
-
153
152
  # Returns sensor data for given interval making in-memory summarization
154
153
  # and returns calculated value
155
154
  # @param interval_id [Fixnum]
@@ -160,6 +159,27 @@ module PulseMeter
160
159
  SensorData.new(Time.at(interval_id), nil)
161
160
  end
162
161
 
162
+ # Drops sensor data within given time
163
+ # @param from [Time] lower bound
164
+ # @param till [Time] upper bound
165
+ # @raise ArgumentError if argumets are not valid time objects
166
+ def drop_within(from, till)
167
+ raise ArgumentError unless from.kind_of?(Time) && till.kind_of?(Time)
168
+ start_time, end_time = from.to_i, till.to_i
169
+ current_interval_id = get_interval_id(start_time) + interval
170
+ keys = []
171
+ while current_interval_id < end_time
172
+ keys << data_key(current_interval_id)
173
+ keys << raw_data_key(current_interval_id)
174
+ current_interval_id += interval
175
+ end
176
+ if keys.empty?
177
+ 0
178
+ else
179
+ redis.del(*keys)
180
+ end
181
+ end
182
+
163
183
  # Returns Redis key by which raw data for current interval is stored
164
184
  def current_raw_data_key
165
185
  raw_data_key(current_interval_id)
@@ -0,0 +1,16 @@
1
+ module PulseMeter
2
+ module Sensor
3
+ module Timelined
4
+ # Counts unique events per interval
5
+ class UniqCounter < Timeline
6
+ def aggregate_event(key, value)
7
+ redis.sadd(key, value)
8
+ end
9
+
10
+ def summarize(key)
11
+ redis.scard(key)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ require 'json'
2
+
3
+ # Static counter to count unique values
4
+ module PulseMeter
5
+ module Sensor
6
+ class UniqCounter < Counter
7
+
8
+ # Processes event
9
+ # @param name [String] value to be counted
10
+ def event(name)
11
+ redis.sadd(value_key, name)
12
+ end
13
+
14
+ # Returs number of unique values ever sent to counter
15
+ # @return [Fixnum]
16
+ def value
17
+ redis.scard(value_key)
18
+ end
19
+
20
+ end
21
+ end
22
+ end
File without changes
File without changes
File without changes
File without changes
@@ -1,3 +1,3 @@
1
1
  module PulseMeter
2
- VERSION = "0.1.10"
2
+ VERSION = "0.1.11"
3
3
  end
@@ -52,16 +52,25 @@ module PulseMeter
52
52
  data.keys.map do |k|
53
53
  {
54
54
  y: to_float(data[k]),
55
- name: k
55
+ name: series_title(k)
56
56
  }
57
57
  end
58
58
  end
59
59
 
60
+ def series_title(key)
61
+ annotation = @sensor.annotation
62
+ if annotation && !annotation.empty?
63
+ "#{annotation}: #{key}"
64
+ else
65
+ key
66
+ end
67
+ end
68
+
60
69
  def series_data(timeline_data)
61
70
  series_data = {}
62
71
  parsed_data = timeline_data.map do |sd|
63
72
  data = parse_data(sd.value)
64
- data.keys.each{|k| series_data[k] ||= {name: k, data: []}}
73
+ data.keys.each{|k| series_data[k] ||= {name: series_title(k), data: []}}
65
74
  [sd.start_time.to_i*1000, data]
66
75
  end
67
76
 
@@ -0,0 +1,86 @@
1
+ require "spec_helper"
2
+
3
+ describe PulseMeter::Sensor::Remote do
4
+
5
+ def data_sent_to(host, port)
6
+ socket = UDPSocket.new
7
+ socket.bind(host, port)
8
+ yield
9
+ data, _ = socket.recvfrom(65000)
10
+ socket.close
11
+ data
12
+ end
13
+
14
+ let(:host){'localhost'}
15
+ let(:port){56789}
16
+ let(:sensor){described_class.new(:some_remote_sensor, host: host, port: port)}
17
+
18
+ describe "#new" do
19
+ it "should raise exception if sensor name is bad" do
20
+ expect{described_class.new("aa bb")}.to raise_exception(PulseMeter::BadSensorName)
21
+ end
22
+ end
23
+
24
+ describe "#event" do
25
+ it "should send event data to remote host and port" do
26
+ data_sent_to(host, port) {
27
+ sensor.event(123)
28
+ }.should_not be_empty
29
+ end
30
+
31
+ it "should use sensor name as a single key and sent data as its value" do
32
+ data = data_sent_to(host, port) do
33
+ sensor.event(123)
34
+ end
35
+ JSON.parse(data).should == {sensor.name => 123}
36
+ end
37
+
38
+ it "should raise MessageTooLarge if message is too long" do
39
+ expect{ sensor.event("123" * 100000) }.to raise_exception(PulseMeter::Remote::MessageTooLarge)
40
+ end
41
+
42
+
43
+ it "should raise PulseMeter::Remote::ConnectionError if remote host is invalid" do
44
+ expect{described_class.new("xxx", host: "bad host").event(123)}.to raise_exception(PulseMeter::Remote::ConnectionError)
45
+ end
46
+
47
+ it "should raise PulseMeter::Remote::ConnectionError if remote port is invalid" do
48
+ expect{described_class.new("xxx", port: -123).event(123)}.to raise_exception(PulseMeter::Remote::ConnectionError)
49
+ expect{described_class.new("xxx", port: 'bad port').event(123)}.to raise_exception(PulseMeter::Remote::ConnectionError)
50
+ end
51
+
52
+ end
53
+
54
+ describe "#events" do
55
+ it "should send event data to remote host and port" do
56
+ data_sent_to(host, port) {
57
+ sensor.events(a: 1, b: 2)
58
+ }.should_not be_empty
59
+ end
60
+
61
+ it "should use sensor name as a single key and sent data as its value" do
62
+ data = data_sent_to(host, port) do
63
+ sensor.events(a: 1, b: 2)
64
+ end
65
+ JSON.parse(data).should == {"a" => 1, "b" => 2}
66
+ end
67
+
68
+ it "should raise ArgumentError if argument is not a hash" do
69
+ expect{ sensor.events(1213) }.to raise_exception(ArgumentError)
70
+ end
71
+
72
+ it "should raise MessageTooLarge if message is too long" do
73
+ expect{ sensor.events(a: "x" * 100000) }.to raise_exception(PulseMeter::Remote::MessageTooLarge)
74
+ end
75
+
76
+ it "should raise PulseMeter::Remote::ConnectionError if remote host is invalid" do
77
+ expect{described_class.new("xxx", host: "bad host").events(a: 123)}.to raise_exception(PulseMeter::Remote::ConnectionError)
78
+ end
79
+
80
+ it "should raise PulseMeter::Remote::ConnectionError if remote port is invalid" do
81
+ expect{described_class.new("xxx", port: -123).events(a: 123)}.to raise_exception(PulseMeter::Remote::ConnectionError)
82
+ expect{described_class.new("xxx", port: 'bad port').events(a: 123)}.to raise_exception(PulseMeter::Remote::ConnectionError)
83
+ end
84
+ end
85
+
86
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe PulseMeter::Sensor::Timelined::UniqCounter do
4
+ it_should_behave_like "timeline sensor"
5
+ it_should_behave_like "timelined subclass", [:foo, :bar], 2
6
+ it_should_behave_like "timelined subclass", [:foo, :bar, :foo], 2
7
+ data = (1..100).map {rand(200)}
8
+ it_should_behave_like "timelined subclass", data, data.uniq.count
9
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe PulseMeter::Sensor::UniqCounter do
4
+ let(:name){ :some_counter }
5
+ let(:sensor){ described_class.new(name) }
6
+ let(:redis){ PulseMeter.redis }
7
+
8
+ describe "#event" do
9
+ it "should count unique values" do
10
+ expect{ sensor.event(:first) }.to change{sensor.value}.to(1)
11
+ expect{ sensor.event(:first) }.not_to change{sensor.value}
12
+ expect{ sensor.event(:second) }.to change{sensor.value}.from(1).to(2)
13
+ end
14
+ end
15
+
16
+ describe "#value" do
17
+ it "should have initial value 0" do
18
+ sensor.value.should == 0
19
+ end
20
+
21
+ it "should return count of unique values" do
22
+ data = (1..100).map {rand(200)}
23
+ data.each {|e| sensor.event(e)}
24
+ sensor.value.should == data.uniq.count
25
+ end
26
+ end
27
+
28
+ end
@@ -50,8 +50,8 @@ describe PulseMeter::Visualize::SeriesExtractor do
50
50
 
51
51
  it "should create point data correctly" do
52
52
  extractor.point_data('{"x": 123, "y": 321}').should == [
53
- {y: 123, name: 'x'},
54
- {y: 321, name: 'y'}
53
+ {y: 123, name: 'hashed sensor: x'},
54
+ {y: 321, name: 'hashed sensor: y'}
55
55
  ]
56
56
  end
57
57
 
@@ -62,15 +62,15 @@ describe PulseMeter::Visualize::SeriesExtractor do
62
62
  ]
63
63
  extractor.series_data(tl_data).should == [
64
64
  {
65
- name: 'a',
65
+ name: 'hashed sensor: a',
66
66
  data: [{x: 1000, y: 5}, {x: 2000, y: nil}]
67
67
  },
68
68
  {
69
- name: 'b',
69
+ name: 'hashed sensor: b',
70
70
  data: [{x: 1000, y: 6}, {x: 2000, y: 6}]
71
71
  },
72
72
  {
73
- name: 'c',
73
+ name: 'hashed sensor: c',
74
74
  data: [{x: 1000, y: nil}, {x: 2000, y: 7}]
75
75
  }
76
76
  ]
@@ -163,7 +163,7 @@ shared_examples_for "timeline sensor" do |extra_init_values, default_event|
163
163
  end
164
164
 
165
165
  describe "#timeline_within" do
166
- it "shoulde raise exception unless both arguments are Time objects" do
166
+ it "should raise exception unless both arguments are Time objects" do
167
167
  [:q, nil, -1].each do |bad_value|
168
168
  expect{ sensor.timeline_within(Time.now, bad_value) }.to raise_exception(ArgumentError)
169
169
  expect{ sensor.timeline_within(bad_value, Time.now) }.to raise_exception(ArgumentError)
@@ -244,6 +244,72 @@ shared_examples_for "timeline sensor" do |extra_init_values, default_event|
244
244
  end
245
245
  end
246
246
 
247
+ describe "#drop_within" do
248
+ it "should raise exception unless both arguments are Time objects" do
249
+ [:q, nil, -1].each do |bad_value|
250
+ expect{ sensor.drop_within(Time.now, bad_value) }.to raise_exception(ArgumentError)
251
+ expect{ sensor.drop_within(bad_value, Time.now) }.to raise_exception(ArgumentError)
252
+ end
253
+ end
254
+
255
+ it "should drop as many raw results as there are sensor interval beginnings in the passed interval" do
256
+ Timecop.freeze(@start_of_interval){ sensor.event(sample_event) }
257
+ Timecop.freeze(@start_of_interval + interval){ sensor.event(sample_event) }
258
+
259
+ future = @start_of_interval + interval * 3
260
+ Timecop.freeze(future) do
261
+ sensor.drop_within(
262
+ Time.at(@start_of_interval + interval - 1),
263
+ Time.at(@start_of_interval + interval + 1)
264
+ ).should == 1
265
+
266
+ data = sensor.timeline_within(
267
+ Time.at(@start_of_interval + interval - 1),
268
+ Time.at(@start_of_interval + interval + 1)
269
+ )
270
+ data.size.should == 1
271
+ data.first.value.should be_nil # since data is dropped
272
+
273
+ end
274
+
275
+ Timecop.freeze(@start_of_interval + interval + 2) do
276
+ sensor.drop_within(
277
+ Time.at(@start_of_interval + interval + 1),
278
+ Time.at(@start_of_interval + interval + 2)
279
+ ).should == 0
280
+ end
281
+ end
282
+
283
+ it "should drop as many reduced results as there are sensor interval beginnings in the passed interval" do
284
+ Timecop.freeze(@start_of_interval){ sensor.event(sample_event) }
285
+ Timecop.freeze(@start_of_interval + interval){ sensor.event(sample_event) }
286
+
287
+ future = @start_of_interval
288
+ Timecop.freeze(future) do
289
+ sensor.reduce_all_raw
290
+ sensor.drop_within(
291
+ Time.at(@start_of_interval + interval - 1),
292
+ Time.at(@start_of_interval + interval + 1)
293
+ ).should == 1
294
+
295
+ data = sensor.timeline_within(
296
+ Time.at(@start_of_interval + interval - 1),
297
+ Time.at(@start_of_interval + interval + 1)
298
+ )
299
+ data.size.should == 1
300
+ data.first.value.should be_nil # since data is dropped
301
+
302
+ end
303
+
304
+ Timecop.freeze(@start_of_interval + interval + 2) do
305
+ sensor.drop_within(
306
+ Time.at(@start_of_interval + interval + 1),
307
+ Time.at(@start_of_interval + interval + 2)
308
+ ).should == 0
309
+ end
310
+ end
311
+ end
312
+
247
313
  describe "SensorData value for an interval" do
248
314
  def check_sensor_data(sensor, value)
249
315
  data = sensor.timeline(2).first
data/spec/spec_helper.rb CHANGED
@@ -16,7 +16,9 @@ Dir['spec/support/**/*.rb'].each{|f| require File.join(ROOT, f) }
16
16
  Dir['spec/shared_examples/**/*.rb'].each{|f| require File.join(ROOT,f)}
17
17
 
18
18
  RSpec.configure do |config|
19
- config.before(:each) { PulseMeter.redis = MockRedis.new }
19
+ config.before(:each) do
20
+ PulseMeter.redis = MockRedis.new
21
+ end
20
22
  config.filter_run :focus => true
21
23
  config.run_all_when_everything_filtered = true
22
24
  config.include(Matchers)
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.1.10
4
+ version: 0.1.11
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: 2012-06-08 00:00:00.000000000 Z
13
+ date: 2012-06-15 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: gon-sinatra
@@ -312,7 +312,9 @@ files:
312
312
  - examples/minimal/Procfile
313
313
  - examples/minimal/client.rb
314
314
  - examples/minimal/server.ru
315
- - examples/readme_client_example.rb
315
+ - examples/readme_client.rb
316
+ - examples/readme_client_conf.rb
317
+ - examples/server_config.yml
316
318
  - lib/cmd.rb
317
319
  - lib/pulse-meter.rb
318
320
  - lib/pulse-meter/mixins/dumper.rb
@@ -323,6 +325,7 @@ files:
323
325
  - lib/pulse-meter/sensor/counter.rb
324
326
  - lib/pulse-meter/sensor/hashed_counter.rb
325
327
  - lib/pulse-meter/sensor/indicator.rb
328
+ - lib/pulse-meter/sensor/remote.rb
326
329
  - lib/pulse-meter/sensor/timeline.rb
327
330
  - lib/pulse-meter/sensor/timelined/average.rb
328
331
  - lib/pulse-meter/sensor/timelined/counter.rb
@@ -331,6 +334,12 @@ files:
331
334
  - lib/pulse-meter/sensor/timelined/median.rb
332
335
  - lib/pulse-meter/sensor/timelined/min.rb
333
336
  - lib/pulse-meter/sensor/timelined/percentile.rb
337
+ - lib/pulse-meter/sensor/timelined/uniq_counter.rb
338
+ - lib/pulse-meter/sensor/uniq_counter.rb
339
+ - lib/pulse-meter/server.rb
340
+ - lib/pulse-meter/server/command_line_options.rb
341
+ - lib/pulse-meter/server/config_options.rb
342
+ - lib/pulse-meter/server/sensors.rb
334
343
  - lib/pulse-meter/version.rb
335
344
  - lib/pulse-meter/visualize/app.rb
336
345
  - lib/pulse-meter/visualize/dsl.rb
@@ -368,6 +377,7 @@ files:
368
377
  - spec/pulse_meter/sensor/counter_spec.rb
369
378
  - spec/pulse_meter/sensor/hashed_counter_spec.rb
370
379
  - spec/pulse_meter/sensor/indicator_spec.rb
380
+ - spec/pulse_meter/sensor/remote_spec.rb
371
381
  - spec/pulse_meter/sensor/timeline_spec.rb
372
382
  - spec/pulse_meter/sensor/timelined/average_spec.rb
373
383
  - spec/pulse_meter/sensor/timelined/counter_spec.rb
@@ -376,6 +386,8 @@ files:
376
386
  - spec/pulse_meter/sensor/timelined/median_spec.rb
377
387
  - spec/pulse_meter/sensor/timelined/min_spec.rb
378
388
  - spec/pulse_meter/sensor/timelined/percentile_spec.rb
389
+ - spec/pulse_meter/sensor/timelined/uniq_counter_spec.rb
390
+ - spec/pulse_meter/sensor/uniq_counter_spec.rb
379
391
  - spec/pulse_meter/visualize/app_spec.rb
380
392
  - spec/pulse_meter/visualize/dsl/layout_spec.rb
381
393
  - spec/pulse_meter/visualize/dsl/page_spec.rb
@@ -410,6 +422,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
410
422
  - - ! '>='
411
423
  - !ruby/object:Gem::Version
412
424
  version: '0'
425
+ segments:
426
+ - 0
427
+ hash: -1712600594864838902
413
428
  requirements: []
414
429
  rubyforge_project:
415
430
  rubygems_version: 1.8.24
@@ -425,6 +440,7 @@ test_files:
425
440
  - spec/pulse_meter/sensor/counter_spec.rb
426
441
  - spec/pulse_meter/sensor/hashed_counter_spec.rb
427
442
  - spec/pulse_meter/sensor/indicator_spec.rb
443
+ - spec/pulse_meter/sensor/remote_spec.rb
428
444
  - spec/pulse_meter/sensor/timeline_spec.rb
429
445
  - spec/pulse_meter/sensor/timelined/average_spec.rb
430
446
  - spec/pulse_meter/sensor/timelined/counter_spec.rb
@@ -433,6 +449,8 @@ test_files:
433
449
  - spec/pulse_meter/sensor/timelined/median_spec.rb
434
450
  - spec/pulse_meter/sensor/timelined/min_spec.rb
435
451
  - spec/pulse_meter/sensor/timelined/percentile_spec.rb
452
+ - spec/pulse_meter/sensor/timelined/uniq_counter_spec.rb
453
+ - spec/pulse_meter/sensor/uniq_counter_spec.rb
436
454
  - spec/pulse_meter/visualize/app_spec.rb
437
455
  - spec/pulse_meter/visualize/dsl/layout_spec.rb
438
456
  - spec/pulse_meter/visualize/dsl/page_spec.rb