pulse-meter 0.2.11 → 0.3.0

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.
Files changed (79) hide show
  1. data/.rbenv-version +1 -1
  2. data/README.md +32 -16
  3. data/Rakefile +29 -10
  4. data/examples/basic.ru +1 -1
  5. data/examples/full/server.ru +1 -1
  6. data/examples/minimal/server.ru +1 -1
  7. data/lib/pulse-meter.rb +1 -0
  8. data/lib/pulse-meter/observer.rb +117 -0
  9. data/lib/pulse-meter/sensor.rb +1 -0
  10. data/lib/pulse-meter/sensor/timeline.rb +3 -2
  11. data/lib/pulse-meter/sensor/timelined/multi_percentile.rb +43 -0
  12. data/lib/pulse-meter/version.rb +1 -1
  13. data/lib/pulse-meter/visualize/app.rb +28 -12
  14. data/lib/pulse-meter/visualize/coffee/application.coffee +40 -0
  15. data/lib/pulse-meter/visualize/coffee/collections/page_info_list.coffee +17 -0
  16. data/lib/pulse-meter/visualize/coffee/collections/sensor_info_list.coffee +4 -0
  17. data/lib/pulse-meter/visualize/coffee/collections/widget_list.coffee +14 -0
  18. data/lib/pulse-meter/visualize/coffee/extensions.coffee +26 -0
  19. data/lib/pulse-meter/visualize/coffee/models/dinamic_widget.coffee +34 -0
  20. data/lib/pulse-meter/visualize/coffee/models/page_info.coffee +2 -0
  21. data/lib/pulse-meter/visualize/coffee/models/sensor_info.coffee +2 -0
  22. data/lib/pulse-meter/visualize/coffee/models/widget.coffee +54 -0
  23. data/lib/pulse-meter/visualize/coffee/presenters/area.coffee +2 -0
  24. data/lib/pulse-meter/visualize/coffee/presenters/gauge.coffee +11 -0
  25. data/lib/pulse-meter/visualize/coffee/presenters/line.coffee +2 -0
  26. data/lib/pulse-meter/visualize/coffee/presenters/pie.coffee +20 -0
  27. data/lib/pulse-meter/visualize/coffee/presenters/series.coffee +44 -0
  28. data/lib/pulse-meter/visualize/coffee/presenters/table.coffee +10 -0
  29. data/lib/pulse-meter/visualize/coffee/presenters/timeline.coffee +13 -0
  30. data/lib/pulse-meter/visualize/coffee/presenters/widget.coffee +65 -0
  31. data/lib/pulse-meter/visualize/coffee/router.coffee +21 -0
  32. data/lib/pulse-meter/visualize/coffee/views/dynamic_chart.coffee +91 -0
  33. data/lib/pulse-meter/visualize/coffee/views/dynamic_widget.coffee +58 -0
  34. data/lib/pulse-meter/visualize/coffee/views/page_title.coffee +17 -0
  35. data/lib/pulse-meter/visualize/coffee/views/page_titles.coffee +15 -0
  36. data/lib/pulse-meter/visualize/coffee/views/sensor_info_list.coffee +19 -0
  37. data/lib/pulse-meter/visualize/coffee/views/widget.coffee +99 -0
  38. data/lib/pulse-meter/visualize/coffee/views/widget_chart.coffee +13 -0
  39. data/lib/pulse-meter/visualize/coffee/views/widget_list.coffee +15 -0
  40. data/lib/pulse-meter/visualize/layout.rb +4 -4
  41. data/lib/pulse-meter/visualize/public/css/application.css +13 -4
  42. data/lib/pulse-meter/visualize/public/css/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  43. data/lib/pulse-meter/visualize/public/css/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  44. data/lib/pulse-meter/visualize/public/css/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  45. data/lib/pulse-meter/visualize/public/css/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  46. data/lib/pulse-meter/visualize/public/css/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  47. data/lib/pulse-meter/visualize/public/css/images/ui-bg_glass_75_ffffff_1x400.png +0 -0
  48. data/lib/pulse-meter/visualize/public/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  49. data/lib/pulse-meter/visualize/public/css/images/ui-bg_inset-soft_95_fef1ec_1x100.png +0 -0
  50. data/lib/pulse-meter/visualize/public/css/images/ui-icons_222222_256x240.png +0 -0
  51. data/lib/pulse-meter/visualize/public/css/images/ui-icons_2e83ff_256x240.png +0 -0
  52. data/lib/pulse-meter/visualize/public/css/images/ui-icons_454545_256x240.png +0 -0
  53. data/lib/pulse-meter/visualize/public/css/images/ui-icons_888888_256x240.png +0 -0
  54. data/lib/pulse-meter/visualize/public/css/images/ui-icons_cd0a0a_256x240.png +0 -0
  55. data/lib/pulse-meter/visualize/public/css/images/ui-icons_f6cf3b_256x240.png +0 -0
  56. data/lib/pulse-meter/visualize/public/css/jquery-ui-1.8.16.bootstrap.css +1320 -0
  57. data/lib/pulse-meter/visualize/public/js/application.js +900 -691
  58. data/lib/pulse-meter/visualize/public/js/jquery-ui-1.8.16.bootstrap.min.js +791 -0
  59. data/lib/pulse-meter/visualize/public/js/jquery-ui-1.8.23.custom.min.js +21 -0
  60. data/lib/pulse-meter/visualize/public/js/jquery-ui-timepicker-addon.js +1687 -0
  61. data/lib/pulse-meter/visualize/sensor.rb +2 -2
  62. data/lib/pulse-meter/visualize/views/main.haml +2 -3
  63. data/lib/pulse-meter/visualize/views/sensors.haml +14 -1
  64. data/lib/pulse-meter/visualize/views/widgets/area.haml +46 -24
  65. data/lib/pulse-meter/visualize/views/widgets/line.haml +46 -23
  66. data/lib/pulse-meter/visualize/views/widgets/table.haml +37 -15
  67. data/lib/pulse-meter/visualize/widgets/timeline.rb +20 -5
  68. data/pulse-meter.gemspec +4 -0
  69. data/spec/pulse_meter/observer_spec.rb +252 -0
  70. data/spec/pulse_meter/sensor/timelined/multi_percentile_spec.rb +21 -0
  71. data/spec/pulse_meter/visualize/sensor_spec.rb +5 -5
  72. data/spec/pulse_meter/visualize/widgets/area_spec.rb +1 -74
  73. data/spec/pulse_meter/visualize/widgets/line_spec.rb +1 -73
  74. data/spec/pulse_meter/visualize/widgets/table_spec.rb +1 -73
  75. data/spec/shared_examples/timeline_sensor.rb +10 -0
  76. data/spec/shared_examples/widget.rb +97 -0
  77. data/spec/spec_helper.rb +1 -0
  78. metadata +120 -5
  79. data/lib/pulse-meter/visualize/public/js/application.coffee +0 -616
data/.rbenv-version CHANGED
@@ -1 +1 @@
1
- 1.9.2-p290
1
+ 1.9.3-p125
data/README.md CHANGED
@@ -76,10 +76,26 @@ There are several caveats with timeline sensors:
76
76
  When building a visualisation you may choose to display the last value or not.
77
77
  * For some sensors (currently Median and Percentile) considerable amount of data should be stored for a
78
78
  particular interval to obtain value for this interval. So it is a good idea to schedule
79
- <tt>pulse reduce</tt>
79
+ `pulse reduce`
80
80
  command on a regular basis. This command reduces the stored data for passed intervals to single values,
81
81
  so that they do not consume storage space.
82
82
 
83
+
84
+ ### Observers
85
+
86
+ Observer allows to notify a sensor each time some class or instance method is called
87
+ Suppose you have a user model and want to count users distribytion by name. To do this you have to observe class method `create` of User class:
88
+
89
+ counter = PulseMeter::Sensor::HashedCounter.new :users_by_name
90
+ PulseMeter::Observer.observe_class_method(User, :create, counter) do |execution_time, attrs|
91
+ event({attrs[:name] => 1})
92
+ end
93
+
94
+ Block recieves all execution time and observed method's argements and is executed in context of sensor passed to observer (this means that event method refers to `counter`).
95
+ To observe instance methods use `observe_method`.
96
+
97
+ `unobserve_class_method` and `unobserve_method` remove observations from class or instace method.
98
+
83
99
  ## Client usage
84
100
 
85
101
  Just create sensor objects and write data. Some examples below.
@@ -146,7 +162,7 @@ Just create sensor objects and write data. Some examples below.
146
162
  # 2012-05-24 11:07:00 +0400: 3.0
147
163
  # 2012-05-24 11:08:00 +0400: 7.0
148
164
 
149
- 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.
165
+ There is also an alternative and a bit more DRY way for sensor creation, management and usage using `PulseMeter::Sensor::Configuration` class. It is also convenient for creating a bunch of sensors from some configuration data.
150
166
 
151
167
  require 'pulse-meter'
152
168
  PulseMeter.redis = Redis.new
@@ -205,12 +221,12 @@ Just create sensor objects and write data. Some examples below.
205
221
 
206
222
  ## Command line interface
207
223
 
208
- Gem includes a tool <tt>pulse</tt>, which allows to send events to sensors, list them, etc.
209
- You should pay attention to the command <tt>pulse reduce</tt>, which is generally should be
224
+ Gem includes a tool `pulse`, which allows to send events to sensors, list them, etc.
225
+ You should pay attention to the command `pulse reduce`, which is generally should be
210
226
  scheduled on a regular basis to keep data in Redis small.
211
227
 
212
- To see available commands of this tool one can run the example above(see <tt>examples/readme\_client\_example.rb</tt>)
213
- and run <tt>pulse help</tt>.
228
+ To see available commands of this tool one can run the example above(see `examples/readme_client_example.rb`)
229
+ and run `pulse help`.
214
230
 
215
231
  ## Visualisation
216
232
 
@@ -225,12 +241,12 @@ There is a minimal and a full example below.
225
241
 
226
242
  ### Minimal example
227
243
 
228
- It can be found in <tt>examples/minimal</tt> folder. To run it, execute
229
- <tt>bundle && cd examples/minimal && bundle exec foreman start</tt> (or just <tt>rake example:minimal</tt>)
244
+ It can be found in `examples/minimal` folder. To run it, execute
245
+ `bundle && cd examples/minimal && bundle exec foreman start` (or just `rake example:minimal`)
230
246
  at project root and visit
231
- <tt>http://localhost:9292</tt> at your browser.
247
+ `http://localhost:9292` at your browser.
232
248
 
233
- <tt>client.rb</tt> just creates a timelined counter an sends data to it in an infinite loop.
249
+ `client.rb` just creates a timelined counter an sends data to it in an infinite loop.
234
250
 
235
251
  require "pulse-meter"
236
252
 
@@ -247,7 +263,7 @@ at project root and visit
247
263
  sleep(Random.rand)
248
264
  end
249
265
 
250
- <tt>server.ru</tt> is a Rackup file creating a simple layout with one page and one widget on it, which displays
266
+ `server.ru` is a Rackup file creating a simple layout with one page and one widget on it, which displays
251
267
  the sensor's data. The layout is converted to a rack application and launched.
252
268
 
253
269
  require "pulse-meter/visualizer"
@@ -269,19 +285,19 @@ the sensor's data. The layout is converted to a rack application and launched.
269
285
 
270
286
  run layout.to_app
271
287
 
272
- <tt>Procfile</tt> allows to launch both "client" script and the web server with <tt>foreman</tt>.
288
+ `Procfile` allows to launch both "client" script and the web server with `foreman`.
273
289
 
274
290
  web: bundle exec rackup server.ru
275
291
  sensor_data_generator: bundle exec ruby client.rb
276
292
 
277
293
  ### Full example with DSL explanation
278
294
 
279
- It can be found in <tt>examples/full</tt> folder. To run it, execute
280
- <tt>bundle && cd examples/full && bundle exec foreman start</tt> (or just <tt>rake example:full</tt>)
295
+ It can be found in `examples/full` folder. To run it, execute
296
+ `bundle && cd examples/full && bundle exec foreman start` (or just `rake example:full`)
281
297
  at project root and visit
282
- <tt>http://localhost:9292</tt> at your browser.
298
+ `http://localhost:9292` at your browser.
283
299
 
284
- <tt>client.rb</tt> imitating users visiting some imaginary site.
300
+ `client.rb` imitating users visiting some imaginary site.
285
301
 
286
302
  require "pulse-meter"
287
303
 
data/Rakefile CHANGED
@@ -1,7 +1,11 @@
1
1
  #!/usr/bin/env rake
2
2
  require 'bundler/gem_tasks'
3
+ require 'coffee-script'
4
+ require 'listen'
3
5
  require 'rspec/core/rake_task'
4
- require "yard"
6
+ require 'sprockets'
7
+ require 'tilt'
8
+ require 'yard'
5
9
  require 'yard/rake/yardoc_task'
6
10
 
7
11
  RSpec::Core::RakeTask.new(:spec)
@@ -13,17 +17,32 @@ ROOT = File.dirname(__FILE__)
13
17
  task :default => :spec
14
18
 
15
19
  namespace :coffee do
16
- desc "Complile coffee to js"
17
- task :compile do
18
- system 'coffee', '-c', "#{ROOT}/lib/pulse-meter/visualize/public/"
19
- puts "Done"
20
- end
20
+ COFFEE_PATH = "#{ROOT}/lib/pulse-meter/visualize/coffee"
21
21
 
22
- desc "Watch coffee files and recomplile them immediately"
23
- task :watch do
24
- system 'coffee', '--watch', '-c', "#{ROOT}/lib/pulse-meter/visualize/public/"
25
- end
22
+ def compile_js
23
+ Tilt::CoffeeScriptTemplate.default_bare = true
24
+ env = Sprockets::Environment.new
25
+ env.append_path COFFEE_PATH
26
+ data = env['application.coffee']
27
+ open("#{ROOT}/lib/pulse-meter/visualize/public/js/application.js", "w").write(data)
28
+ puts "application.js compiled"
29
+ end
30
+
31
+ desc "Compile coffee to js"
32
+ task :compile do
33
+ compile_js
34
+ end
26
35
 
36
+ desc "Watch coffee files and recomplile them immediately"
37
+ task :watch do
38
+ Listen.to(COFFEE_PATH) do |modified, added, removed|
39
+ puts "Modified: #{modified}" unless modified.empty?
40
+ puts "Added: #{added}" unless added.empty?
41
+ puts "Removed: #{removed}" unless removed.empty?
42
+ puts "Recompiling..."
43
+ compile_js
44
+ end
45
+ end
27
46
  end
28
47
 
29
48
  namespace :yard do
data/examples/basic.ru CHANGED
@@ -1,4 +1,4 @@
1
- $: << File.join(File.absolute_path(__FILE__), '..', 'lib')
1
+ $: << File.join(File.dirname(__FILE__), '..', 'lib')
2
2
 
3
3
  require 'pulse-meter/visualizer'
4
4
 
@@ -1,4 +1,4 @@
1
- $: << File.join(File.absolute_path(__FILE__), '..', 'lib')
1
+ $: << File.join(File.dirname(__FILE__), '..', 'lib')
2
2
 
3
3
  require "pulse-meter/visualizer"
4
4
 
@@ -1,4 +1,4 @@
1
- $: << File.join(File.absolute_path(__FILE__), '..', 'lib')
1
+ $: << File.join(File.dirname(__FILE__), '..', 'lib')
2
2
 
3
3
  require "pulse-meter/visualizer"
4
4
 
data/lib/pulse-meter.rb CHANGED
@@ -4,6 +4,7 @@ require "pulse-meter/version"
4
4
  require "pulse-meter/mixins/dumper"
5
5
  require "pulse-meter/mixins/utils"
6
6
  require "pulse-meter/mixins/cmd"
7
+ require "pulse-meter/observer"
7
8
  require "pulse-meter/sensor"
8
9
  require "pulse-meter/sensor/configuration"
9
10
 
@@ -0,0 +1,117 @@
1
+ require 'pulse-meter'
2
+
3
+ module PulseMeter
4
+ class Observer
5
+ class << self
6
+ # Removes observation from instance method
7
+ # @param klass [Class] class
8
+ # @param method [Symbol] instance method name
9
+ def unobserve_method(klass, method)
10
+ with_observer = method_with_observer(method)
11
+ without_observer = method_without_observer(method)
12
+
13
+ return unless klass.method_defined? with_observer
14
+ klass.class_eval do
15
+ alias_method method, without_observer
16
+ remove_method with_observer
17
+ remove_method without_observer
18
+ end
19
+ end
20
+
21
+ # Removes observation from class method
22
+ # @param klass [Class] class
23
+ # @param method [Symbol] class method name
24
+ def unobserve_class_method(klass, method)
25
+ with_observer = method_with_observer(method)
26
+ without_observer = method_without_observer(method)
27
+
28
+ return unless klass.respond_to? with_observer
29
+ metaclass(klass).instance_eval do
30
+ alias_method method, without_observer
31
+ remove_method with_observer
32
+ remove_method without_observer
33
+ end
34
+ end
35
+
36
+ # Registeres an observer for instance method
37
+ # @param klass [Class] class
38
+ # @param method [Symbol] instance method
39
+ # @param sensor [Object] notifications receiver
40
+ # @param proc [Proc] proc to be called in context of receiver each time observed method called
41
+ def observe_method(klass, method, sensor, &proc)
42
+ with_observer = method_with_observer(method)
43
+ without_observer = method_without_observer(method)
44
+
45
+ return if klass.method_defined? with_observer
46
+
47
+ klass.class_eval do
48
+ alias_method without_observer, method
49
+ define_method with_observer do |*args, &block|
50
+ result = nil
51
+ start_time = Time.now
52
+ begin
53
+ result = self.send without_observer, *args, &block
54
+ ensure
55
+ begin
56
+ delta = ((Time.now - start_time) * 1000).to_i
57
+ sensor.instance_exec delta, *args, &proc
58
+ rescue Exception
59
+ end
60
+ end
61
+ result
62
+ end
63
+ alias_method method, with_observer
64
+ end
65
+ end
66
+
67
+ # Registeres an observer for class method
68
+ # @param klass [Class] class
69
+ # @param method [Symbol] class method
70
+ # @param sensor [Object] notifications receiver
71
+ # @param proc [Proc] proc to be called in context of receiver each time observed method called
72
+ def observe_class_method(klass, method, sensor, &proc)
73
+ with_observer = method_with_observer(method)
74
+ without_observer = method_without_observer(method)
75
+
76
+ return if klass.respond_to? with_observer
77
+
78
+ metaclass(klass).instance_eval do
79
+ alias_method without_observer, method
80
+ define_method with_observer do |*args, &block|
81
+ result = nil
82
+ start_time = Time.now
83
+ begin
84
+ result = self.send without_observer, *args, &block
85
+ ensure
86
+ begin
87
+ delta = ((Time.now - start_time) * 1000).to_i
88
+ sensor.instance_exec delta, *args, &proc
89
+ rescue Exception
90
+ end
91
+ end
92
+ result
93
+ end
94
+ alias_method method, with_observer
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ def metaclass(klass)
101
+ klass.class_eval do
102
+ class << self
103
+ self
104
+ end
105
+ end
106
+ end
107
+
108
+ def method_with_observer(method)
109
+ "#{method}_with_observer"
110
+ end
111
+
112
+ def method_without_observer(method)
113
+ "#{method}_without_observer"
114
+ end
115
+ end
116
+ end
117
+ end
@@ -15,6 +15,7 @@ require 'pulse-meter/sensor/timelined/hashed_indicator'
15
15
  require 'pulse-meter/sensor/timelined/min'
16
16
  require 'pulse-meter/sensor/timelined/max'
17
17
  require 'pulse-meter/sensor/timelined/percentile'
18
+ require 'pulse-meter/sensor/timelined/multi_percentile'
18
19
  require 'pulse-meter/sensor/timelined/median'
19
20
  require 'pulse-meter/sensor/timelined/uniq_counter'
20
21
 
@@ -74,8 +74,9 @@ module PulseMeter
74
74
  interval_data_key = data_key(interval_id)
75
75
  multi do
76
76
  redis.del(interval_raw_data_key)
77
- redis.set(interval_data_key, value)
78
- redis.expire(interval_data_key, ttl)
77
+ if redis.setnx(interval_data_key, value)
78
+ redis.expire(interval_data_key, ttl)
79
+ end
79
80
  end
80
81
  end
81
82
 
@@ -0,0 +1,43 @@
1
+ module PulseMeter
2
+ module Sensor
3
+ module Timelined
4
+ # Calculates n'th percentile in interval
5
+ class MultiPercentile < Timeline
6
+ attr_reader :p_value
7
+
8
+ def initialize(name, options)
9
+ @p_value = assert_array!(options, :p, [])
10
+ @p_value.each {|p| assert_ranged_float!({:percentile => p}, :percentile, 0, 1)}
11
+ super(name, options)
12
+ end
13
+
14
+ def aggregate_event(key, value)
15
+ redis.zadd(key, value, "#{value}::#{uniqid}")
16
+ end
17
+
18
+ def summarize(key)
19
+ @p_value.each_with_object({}) do |p, acc|
20
+ count = redis.zcard(key)
21
+ percentile = if count > 0
22
+ position = p > 0 ? (p * count).round - 1 : 0
23
+ el = redis.zrange(key, position, position)[0]
24
+ redis.zscore(key, el)
25
+ else
26
+ nil
27
+ end
28
+ acc[p] = percentile
29
+ end.to_json
30
+ end
31
+
32
+ private
33
+
34
+ def deflate(value)
35
+ hash = JSON.parse(value)
36
+ hash.each {|p, v| hash[p] = v.to_f}
37
+ hash
38
+ end
39
+
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,3 +1,3 @@
1
1
  module PulseMeter
2
- VERSION = "0.2.11"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -24,25 +24,35 @@ module PulseMeter
24
24
  get '/' do
25
25
  @title = @layout.title
26
26
  gon.pageInfos = camelize_keys(@layout.page_infos)
27
- gon.options = camelize_keys(@layout.options)
27
+ gon.options = camelize_keys(@layout.options)
28
28
  haml :main
29
29
  end
30
30
 
31
- get '/pages/:id/widgets' do
32
- id = params[:id].to_i
31
+ get '/pages/:id/widgets' do
32
+ id = params[:id].to_i
33
33
 
34
- content_type :json
34
+ content_type :json
35
35
  camelize_keys(@layout.widgets(id - 1)).to_json
36
- end
36
+ end
37
37
 
38
- get '/pages/:page_id/widgets/:id' do
39
- page_id = params[:page_id].to_i
40
- id = params[:id].to_i
38
+ get '/pages/:page_id/widgets/:id' do
39
+ page_id = params[:page_id].to_i
40
+ id = params[:id].to_i
41
41
  timespan = params[:timespan].to_i
42
+ start_time = params[:startTime].to_i
43
+ end_time = params[:endTime].to_i
42
44
 
43
- content_type :json
44
- camelize_keys(@layout.widget(page_id - 1, id - 1, timespan: timespan)).to_json
45
- end
45
+ content_type :json
46
+ camelize_keys(
47
+ @layout.widget(
48
+ page_id - 1,
49
+ id - 1,
50
+ timespan: timespan,
51
+ start_time: start_time,
52
+ end_time: end_time
53
+ )
54
+ ).to_json
55
+ end
46
56
 
47
57
  get '/sensors' do
48
58
  content_type :json
@@ -50,9 +60,15 @@ module PulseMeter
50
60
  end
51
61
 
52
62
  get '/dynamic_widget' do
63
+ start_time = params[:startTime].to_i
64
+ end_time = params[:endTime].to_i
65
+ timespan = params[:timespan].to_i
66
+
53
67
  content_type :json
54
68
  camelize_keys(@layout.dynamic_widget(
55
- timespan: params[:timespan],
69
+ timespan: timespan,
70
+ start_time: start_time,
71
+ end_time: end_time,
56
72
  sensors: params[:sensor],
57
73
  type: params[:type])
58
74
  ).to_json