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