pulse-meter 0.2.11 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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