pulse-meter 0.1.10 → 0.1.11

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 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