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 +134 -56
- data/examples/full/Procfile +1 -1
- data/examples/full/client.rb +74 -55
- data/examples/{readme_client_example.rb → readme_client.rb} +0 -0
- data/examples/readme_client_conf.rb +59 -0
- data/examples/server_config.yml +0 -0
- data/lib/cmd.rb +61 -18
- data/lib/pulse-meter.rb +1 -0
- data/lib/pulse-meter/sensor.rb +7 -0
- data/lib/pulse-meter/sensor/base.rb +3 -3
- data/lib/pulse-meter/sensor/remote.rb +60 -0
- data/lib/pulse-meter/sensor/timeline.rb +21 -1
- data/lib/pulse-meter/sensor/timelined/uniq_counter.rb +16 -0
- data/lib/pulse-meter/sensor/uniq_counter.rb +22 -0
- data/lib/pulse-meter/server.rb +0 -0
- data/lib/pulse-meter/server/command_line_options.rb +0 -0
- data/lib/pulse-meter/server/config_options.rb +0 -0
- data/lib/pulse-meter/server/sensors.rb +0 -0
- data/lib/pulse-meter/version.rb +1 -1
- data/lib/pulse-meter/visualize/series_extractor.rb +11 -2
- data/spec/pulse_meter/sensor/remote_spec.rb +86 -0
- data/spec/pulse_meter/sensor/timelined/uniq_counter_spec.rb +9 -0
- data/spec/pulse_meter/sensor/uniq_counter_spec.rb +28 -0
- data/spec/pulse_meter/visualize/series_extractor_spec.rb +5 -5
- data/spec/shared_examples/timeline_sensor.rb +67 -1
- data/spec/spec_helper.rb +3 -1
- metadata +21 -3
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
|
-
|
229
|
-
:
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
:
|
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
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
:
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
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
|
-
|
276
|
-
|
277
|
-
:
|
278
|
-
:
|
279
|
-
|
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
|
286
|
-
requests_per_hour
|
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
|
290
|
-
errors_per_hour
|
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
|
296
|
-
shortest_minute_request
|
297
|
-
perc90_minute_request
|
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 =
|
377
|
+
agent_counter = sensors.sensor(agent_names.shuffle.first)
|
300
378
|
agent_counter.event(1)
|
301
379
|
|
302
380
|
sleep(Random.rand / 10)
|
data/examples/full/Procfile
CHANGED
@@ -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
|
data/examples/full/client.rb
CHANGED
@@ -4,78 +4,97 @@ require "pulse-meter"
|
|
4
4
|
|
5
5
|
PulseMeter.redis = Redis.new
|
6
6
|
|
7
|
-
|
8
|
-
:
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
:
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
errors_per_hour
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
:
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
55
|
-
|
56
|
-
:
|
57
|
-
:
|
58
|
-
|
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
|
65
|
-
requests_per_hour
|
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
|
69
|
-
errors_per_hour
|
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
|
75
|
-
shortest_minute_request
|
76
|
-
perc90_minute_request
|
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 =
|
97
|
+
agent_counter = sensors.sensor(agent_names.shuffle.first)
|
79
98
|
agent_counter.event(1)
|
80
99
|
|
81
100
|
sleep(Random.rand / 10)
|
File without changes
|
@@ -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(
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
58
|
+
data << [s.name, s.class, s.ttl, s.raw_data_ttl, s.interval, s.reduce_delay]
|
35
59
|
else
|
36
|
-
|
60
|
+
data << [s.name, s.class] + [''] * 4
|
37
61
|
end
|
38
62
|
end
|
39
|
-
|
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(
|
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
|
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
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
data/lib/pulse-meter/sensor.rb
CHANGED
@@ -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
|
|
@@ -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
|
data/lib/pulse-meter/version.rb
CHANGED
@@ -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 "
|
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)
|
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.
|
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-
|
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/
|
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
|