rails_metrics 0.1

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 (66) hide show
  1. data/CHANGELOG.rdoc +3 -0
  2. data/Gemfile +9 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +58 -0
  5. data/Rakefile +58 -0
  6. data/TODO.rdoc +1 -0
  7. data/app/controllers/rails_metrics_controller.rb +78 -0
  8. data/app/helpers/rails_metrics_helper.rb +161 -0
  9. data/app/views/layouts/rails_metrics.html.erb +21 -0
  10. data/app/views/rails_metrics/_request.html.erb +21 -0
  11. data/app/views/rails_metrics/_row.html.erb +23 -0
  12. data/app/views/rails_metrics/all.html.erb +25 -0
  13. data/app/views/rails_metrics/chart.html.erb +49 -0
  14. data/app/views/rails_metrics/index.html.erb +21 -0
  15. data/app/views/rails_metrics/show.html.erb +41 -0
  16. data/config/routes.rb +7 -0
  17. data/lib/generators/rails_metrics_generator.rb +40 -0
  18. data/lib/rails_metrics.rb +112 -0
  19. data/lib/rails_metrics/async_consumer.rb +54 -0
  20. data/lib/rails_metrics/engine.rb +29 -0
  21. data/lib/rails_metrics/middleware.rb +27 -0
  22. data/lib/rails_metrics/orm/active_record.rb +66 -0
  23. data/lib/rails_metrics/payload_parser.rb +131 -0
  24. data/lib/rails_metrics/store.rb +132 -0
  25. data/lib/rails_metrics/version.rb +3 -0
  26. data/public/images/rails_metrics/arrow_down.png +0 -0
  27. data/public/images/rails_metrics/arrow_up.png +0 -0
  28. data/public/images/rails_metrics/cancel.png +0 -0
  29. data/public/images/rails_metrics/chart_pie.png +0 -0
  30. data/public/images/rails_metrics/page_white_delete.png +0 -0
  31. data/public/images/rails_metrics/page_white_go.png +0 -0
  32. data/public/images/rails_metrics/tick.png +0 -0
  33. data/public/javascripts/rails_metrics/g.pie-min.js +6 -0
  34. data/public/javascripts/rails_metrics/g.raphael-min.js +5 -0
  35. data/public/javascripts/rails_metrics/raphael-min.js +5 -0
  36. data/public/stylesheets/rails_metrics.css +135 -0
  37. data/test/dummy/app/controllers/application_controller.rb +4 -0
  38. data/test/dummy/app/controllers/users_controller.rb +43 -0
  39. data/test/dummy/app/helpers/application_helper.rb +2 -0
  40. data/test/dummy/app/models/metric.rb +3 -0
  41. data/test/dummy/app/models/notification.rb +7 -0
  42. data/test/dummy/app/models/user.rb +2 -0
  43. data/test/dummy/config/application.rb +52 -0
  44. data/test/dummy/config/boot.rb +9 -0
  45. data/test/dummy/config/environment.rb +5 -0
  46. data/test/dummy/config/environments/development.rb +19 -0
  47. data/test/dummy/config/environments/production.rb +33 -0
  48. data/test/dummy/config/environments/test.rb +29 -0
  49. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  50. data/test/dummy/config/initializers/cookie_verification_secret.rb +7 -0
  51. data/test/dummy/config/initializers/session_store.rb +15 -0
  52. data/test/dummy/config/routes.rb +60 -0
  53. data/test/dummy/db/migrate/20100106152343_create_metrics.rb +17 -0
  54. data/test/dummy/db/migrate/20100108120821_create_users.rb +13 -0
  55. data/test/integration/instrumentation_test.rb +100 -0
  56. data/test/integration/navigation_test.rb +103 -0
  57. data/test/orm/active_record_test.rb +51 -0
  58. data/test/payload_parser_test.rb +36 -0
  59. data/test/rails_metrics_test.rb +43 -0
  60. data/test/store_test.rb +81 -0
  61. data/test/support/helpers.rb +16 -0
  62. data/test/support/instrumentation.rb +18 -0
  63. data/test/support/mock_store.rb +34 -0
  64. data/test/support/webrat/integrations/rails.rb +31 -0
  65. data/test/test_helper.rb +32 -0
  66. metadata +118 -0
@@ -0,0 +1,25 @@
1
+ <% if @metrics.empty? %>
2
+ <h2>No metrics so far, navigate on your app and come back.</h2>
3
+ <% else %>
4
+ <% content_for(:rails_metrics_header) do %>
5
+ <%= pagination_and_scopes_info(:metrics) %>
6
+
7
+ <% form_tag url_for(params.merge(:action => "destroy_all")), :method => :delete do %>
8
+ <%= submit_tag "Delete all", :onclick => "return confirm('Are you sure you want to delete those #{@metrics_count} metrics?')" %>
9
+ <% end %>
10
+ <% end %>
11
+
12
+ <table id="rails_metrics_table" class="all">
13
+ <tr>
14
+ <th>When<br /><%= link_to_order_by_scopes(:earliest, :latest) %></th>
15
+ <th>Name<br /><%= link_to_clear_by_scope(:name) %></th>
16
+ <th>Duration<br /><%= link_to_order_by_scopes(:slowest, :fastest) %></th>
17
+ <th>Payload</th>
18
+ <th></th>
19
+ </tr>
20
+
21
+ <%= render :partial => "row", :collection => @metrics, :as => :metric %>
22
+ </table>
23
+
24
+ <% paginate! %>
25
+ <% end %>
@@ -0,0 +1,49 @@
1
+ <% content_for(:rails_metrics_header) do %>
2
+ Showing request #<%= @request.id %>
3
+ <div class="actions"><% add_action_links!(@request) %></div>
4
+ <% end %>
5
+
6
+ <div id="chart_container">
7
+ <div id="chart"></div>
8
+ </div>
9
+
10
+ <script type="text/javascript" charset="utf-8">
11
+ var r = Raphael("chart");
12
+ r.g.text(400, 30, "<%= @request.payload[:method] %> <%= @request.payload[:path] %> at <%= @request.started_at.strftime("%d %b %H:%M:%S") %>").attr({"font-size": 20});
13
+
14
+ var pie = r.g.piechart(250, 150, 100,
15
+ <%=raw @metrics.map { |m| m.exclusive_duration_in_ms }.inspect %>, {
16
+ legend: <%=raw @metrics.map { |m| "##.# ms - #{m.name}" }.inspect %>,
17
+ href: <%=raw @metrics.map { |m| "#rails_metric_#{m.id}" }.inspect %>,
18
+ cut: 0
19
+ }
20
+ );
21
+
22
+ pie.hover(function () {
23
+ this.sector.stop();
24
+ this.sector.scale(1.1, 1.1, this.cx, this.cy);
25
+ if (this.label) {
26
+ this.label[0].stop();
27
+ this.label[0].scale(1.5);
28
+ this.label[1].attr({"font-weight": 800});
29
+ }
30
+ }, function () {
31
+ this.sector.animate({scale: [1, 1, this.cx, this.cy]}, 500, "bounce");
32
+ if (this.label) {
33
+ this.label[0].animate({scale: 1}, 500, "bounce");
34
+ this.label[1].attr({"font-weight": 400});
35
+ }
36
+ });
37
+ </script>
38
+
39
+ <table id="rails_metrics_table" class="chart">
40
+ <tr>
41
+ <th>Name</th>
42
+ <th>Duration (exclusive)</th>
43
+ <th>Payload</th>
44
+ <th></th>
45
+ </tr>
46
+
47
+ <%= render :partial => "row", :collection => @metrics, :as => :metric,
48
+ :locals => { :skip_timestamps => true } %>
49
+ </table>
@@ -0,0 +1,21 @@
1
+ <% if @metrics.empty? %>
2
+ <h2>No requests so far, navigate on your app and come back.</h2>
3
+ <% else %>
4
+ <% content_for(:rails_metrics_header) do %>
5
+ <%= pagination_and_scopes_info(:requests) %>
6
+ <% end %>
7
+
8
+ <table id="rails_metrics_table" class="requests">
9
+ <tr>
10
+ <th>When<br /><%= link_to_order_by_scopes(:earliest, :latest) %></th>
11
+ <th>Method</th>
12
+ <th>Path</th>
13
+ <th>Duration<br /><%= link_to_order_by_scopes(:slowest, :fastest) %></th>
14
+ <th></th>
15
+ </tr>
16
+
17
+ <%= render :partial => "request", :collection => @metrics, :as => :metric %>
18
+ </table>
19
+
20
+ <% paginate! %>
21
+ <% end %>
@@ -0,0 +1,41 @@
1
+ <% content_for(:rails_metrics_header) do %>
2
+ Showing metric #<%= @metric.id %>
3
+ <div class="actions"><% add_action_links!(@metric) %></div>
4
+ <% end %>
5
+
6
+ <table id="rails_metrics_table" class="show">
7
+ <tr>
8
+ <th>Key</th>
9
+ <th>Value</th>
10
+ </tr>
11
+
12
+ <tr class="odd">
13
+ <td>Name</td>
14
+ <td><%= link_to_set_by_scope @metric, :name %></td>
15
+ </tr>
16
+
17
+ <tr class="even">
18
+ <td>Request</td>
19
+ <td><%= link_to @metric.request_id, chart_rails_metric_path(@metric.request_id) %></td>
20
+ </tr>
21
+
22
+ <tr class="odd">
23
+ <td>Duration</td>
24
+ <td><%= @metric.duration_in_ms %> ms</td>
25
+ </tr>
26
+
27
+ <tr class="even">
28
+ <td>Payload</td>
29
+ <td class="payload"><%= payload_inspect(@metric.payload) %></td>
30
+ </tr>
31
+
32
+ <tr class="odd">
33
+ <td>Started at</td>
34
+ <td><%= @metric.started_at %></td>
35
+ </tr>
36
+
37
+ <tr class="even">
38
+ <td>Created at</td>
39
+ <td><%= @metric.created_at %></td>
40
+ </tr>
41
+ </table>
@@ -0,0 +1,7 @@
1
+ Rails::Application.routes.draw do |map|
2
+ resources :rails_metrics, :only => [:index, :show, :destroy] do
3
+ get :all, :on => :collection
4
+ get :chart, :on => :member
5
+ delete :destroy_all, :on => :collection
6
+ end
7
+ end
@@ -0,0 +1,40 @@
1
+ class RailsMetricsGenerator < Rails::Generators::NamedBase
2
+ class_option :migration, :type => :boolean, :default => true
3
+
4
+ class_option :update, :type => :boolean, :default => false,
5
+ :desc => "Just update public files, do not create a model"
6
+
7
+ def self.source_root
8
+ @_metrics_source_root ||= File.dirname(__FILE__)
9
+ end
10
+
11
+ def copy_public_files
12
+ directory "../../public", "public", :recursive => true
13
+ exit(0) if options.update?
14
+ end
15
+
16
+ def invoke_model
17
+ invoke "model", [name].concat(migration_columns),
18
+ :timestamps => false, :test_framework => false, :migration => options.migration?
19
+ end
20
+
21
+ def add_model_config
22
+ inject_into_class "app/models/#{file_name}.rb", class_name, <<-CONTENT
23
+ include RailsMetrics::ORM::#{Rails::Generators.options[:rails][:orm].to_s.camelize}
24
+ CONTENT
25
+ end
26
+
27
+ def add_application_config
28
+ inject_into_class "config/application.rb", "Application", <<-CONTENT
29
+ # Set rails metrics store
30
+ config.rails_metrics.set_store = lambda { ::#{class_name} }
31
+
32
+ CONTENT
33
+ end
34
+
35
+ protected
36
+
37
+ def migration_columns
38
+ %w(name:string duration:integer request_id:integer parent_id:integer payload:text started_at:datetime created_at:datetime)
39
+ end
40
+ end
@@ -0,0 +1,112 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+ Thread.abort_on_exception = Rails.env.development? || Rails.env.test?
3
+
4
+ module RailsMetrics
5
+ autoload :AsyncConsumer, 'rails_metrics/async_consumer'
6
+ autoload :Middleware, 'rails_metrics/middleware'
7
+ autoload :PayloadParser, 'rails_metrics/payload_parser'
8
+ autoload :Store, 'rails_metrics/store'
9
+ autoload :VERSION, 'rails_metrics/version'
10
+ autoload :VoidInstrumenter, 'rails_metrics/async_consumer'
11
+
12
+ module ORM
13
+ autoload :ActiveRecord, 'rails_metrics/orm/active_record'
14
+ end
15
+
16
+ # Set which store to use in RailsMetrics.
17
+ #
18
+ # RailsMetrics.set_store { Metric }
19
+ #
20
+ def self.set_store(&block)
21
+ metaclass.send :define_method, :store, &block
22
+ end
23
+
24
+ # Place holder for the store.
25
+ def self.store; end
26
+
27
+ # Holds the events for a specific thread.
28
+ def self.events
29
+ Thread.current[:rails_metrics_events] ||= []
30
+ end
31
+
32
+ # Turn RailsMetrics on, i.e. make it listen to notifications during the block.
33
+ # At the end, it pushes notifications to the async consumer.
34
+ def self.listen_request
35
+ events = RailsMetrics.events
36
+ events.clear
37
+
38
+ Thread.current[:rails_metrics_listening] = true
39
+ result = yield
40
+
41
+ RailsMetrics.async_consumer.push(events.dup)
42
+ result
43
+ ensure
44
+ Thread.current[:rails_metrics_listening] = false
45
+ RailsMetrics.events.clear
46
+ end
47
+
48
+ # Returns if events are being registered or not.
49
+ def self.listening?
50
+ Thread.current[:rails_metrics_listening] || false
51
+ end
52
+
53
+ # Allow you to specify a condition to ignore a notification based
54
+ # on its name and/or payload. For example, if you want to ignore
55
+ # all notifications with empty payload, one can do:
56
+ #
57
+ # RailsMetrics.ignore :with_empty_payload do |name, payload|
58
+ # payload.empty?
59
+ # end
60
+ #
61
+ # However, if you want to ignore something based solely on its
62
+ # name, you can use ignore_patterns instead:
63
+ #
64
+ # RailsMetrics.ignore_patterns << /^some_noise_plugin/
65
+ #
66
+ def self.ignore(name, &block)
67
+ raise ArgumentError, "ignore expects a block" unless block_given?
68
+ ignore_lambdas[name] = block
69
+ end
70
+
71
+ # Stores the blocks given to ignore with their respective identifier in a hash.
72
+ def self.ignore_lambdas
73
+ @@ignore_lambdas ||= {}
74
+ end
75
+
76
+ # Stores ignore patterns that can be given as strings or regexps.
77
+ def self.ignore_patterns
78
+ @@ignore_patterns ||= []
79
+ end
80
+
81
+ # Holds the queue which store stuff in the database.
82
+ def self.async_consumer
83
+ @@async_consumer ||= AsyncConsumer.new do |events|
84
+ next if events.empty?
85
+ root = RailsMetrics.store.events_to_metrics_tree(events)
86
+ root.save_metrics!
87
+ end
88
+ end
89
+
90
+ # Wait until the async queue is consumed.
91
+ def self.wait
92
+ sleep(0.01) until async_consumer.finished?
93
+ end
94
+
95
+ # A notification is valid for storing if two conditions are met:
96
+ #
97
+ # 1) The instrumenter id which created the notification is not the same
98
+ # instrumenter id of this thread. This means that notifications generated
99
+ # inside this thread are stored in the database;
100
+ #
101
+ # 2) If the notification name does not match any ignored pattern;
102
+ #
103
+ def self.valid_for_storing?(args) #:nodoc:
104
+ name, payload = args[0].to_s, args[4]
105
+
106
+ RailsMetrics.listening? && RailsMetrics.store &&
107
+ !self.ignore_patterns.find { |p| String === p ? name == p : name =~ p } &&
108
+ !self.ignore_lambdas.values.any? { |b| b.call(name, payload) }
109
+ end
110
+ end
111
+
112
+ require 'rails_metrics/engine'
@@ -0,0 +1,54 @@
1
+ require 'thread'
2
+
3
+ module RailsMetrics
4
+ # An instrumenter that does not send notifications. This is used in the
5
+ # AsyncQueue so saving events does not send any notifications, not even
6
+ # for logging.
7
+ class VoidInstrumenter < ::ActiveSupport::Notifications::Instrumenter
8
+ def instrument(name, payload={})
9
+ yield(payload) if block_given?
10
+ end
11
+ end
12
+
13
+ class AsyncConsumer
14
+ attr_reader :thread
15
+
16
+ def initialize(queue=Queue.new, &block)
17
+ @off = true
18
+ @block = block
19
+ @queue = queue
20
+ @mutex = Mutex.new
21
+
22
+ @thread = Thread.new do
23
+ set_void_instrumenter
24
+ consume
25
+ end
26
+ end
27
+
28
+ def push(*args)
29
+ @mutex.synchronize { @off = false }
30
+ @queue.push(*args)
31
+ end
32
+
33
+ def finished?
34
+ @off
35
+ end
36
+
37
+ protected
38
+
39
+ def set_void_instrumenter #:nodoc:
40
+ Thread.current[:"instrumentation_#{notifier.object_id}"] = VoidInstrumenter.new(notifier)
41
+ end
42
+
43
+ def notifier #:nodoc:
44
+ ActiveSupport::Notifications.notifier
45
+ end
46
+
47
+ def consume #:nodoc:
48
+ while args = @queue.shift
49
+ @block.call(args)
50
+ @mutex.synchronize { @off = @queue.empty? }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,29 @@
1
+ module RailsMetrics
2
+ class Engine < ::Rails::Engine
3
+ engine_name :rails_metrics
4
+
5
+ # Add middleware
6
+ config.middleware.use RailsMetrics::Middleware
7
+
8
+ # Initialize configure parameters
9
+ config.rails_metrics.ignore_lambdas = {}
10
+ config.rails_metrics.ignore_patterns = [ "action_controller.start_processing" ]
11
+
12
+ initializer "rails_metrics.set_ignores" do |app|
13
+ RailsMetrics.ignore_lambdas.merge!(app.config.rails_metrics.ignore_lambdas)
14
+ RailsMetrics.ignore_patterns.concat(app.config.rails_metrics.ignore_patterns)
15
+ end
16
+
17
+ initializer "rails_metrics.set_store" do |app|
18
+ if app.config.rails_metrics.set_store
19
+ RailsMetrics.set_store(&app.config.rails_metrics.set_store)
20
+ end
21
+ end
22
+
23
+ initializer "rails_metrics.start_subscriber" do
24
+ ActiveSupport::Notifications.subscribe do |*args|
25
+ RailsMetrics.events.push(args) if RailsMetrics.valid_for_storing?(args)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ module RailsMetrics
2
+ class Middleware
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ if env["PATH_INFO"] =~ /^\/rails_metrics/
9
+ @app.call(env)
10
+ else
11
+ RailsMetrics.listen_request do
12
+ response = notifications.instrument "rack.request",
13
+ :path => env["PATH_INFO"], :method => env["REQUEST_METHOD"],
14
+ :instrumenter_id => notifications.instrumenter.id do
15
+ @app.call(env)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ protected
22
+
23
+ def notifications
24
+ ActiveSupport::Notifications
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,66 @@
1
+ # Setup to ignore any query which is not a SELECT, INSERT, UPDATE
2
+ # or DELETE and queries made by the own store.
3
+ RailsMetrics.ignore :invalid_queries do |name, payload|
4
+ name == "active_record.sql" &&
5
+ (payload[:sql] !~ /^(SELECT|INSERT|UPDATE|DELETE)/ ||
6
+ RailsMetrics.store.connections_ids.include?(payload[:connection_id]))
7
+ end
8
+
9
+ module RailsMetrics
10
+ module ORM
11
+ # Include in your model to store metrics. For ActiveRecord, you need the
12
+ # following setup:
13
+ #
14
+ # script/generate model Metric script/generate name:string duration:integer
15
+ # request_id:integer parent_id:integer payload:text started_at:datetime created_at:datetime --skip-timestamps
16
+ #
17
+ # You can use any model name you wish. Next, you need to include
18
+ # RailsMetrics::ORM::ActiveRecord:
19
+ #
20
+ # class Metric < ActiveRecord::Base
21
+ # include RailsMetrics::ORM::ActiveRecord
22
+ # end
23
+ #
24
+ module ActiveRecord
25
+ extend ActiveSupport::Concern
26
+ include RailsMetrics::Store
27
+
28
+ included do
29
+ # Create a new connection pool just for the given resource
30
+ establish_connection(Rails.env)
31
+
32
+ # Set required validations
33
+ validates_presence_of :name, :started_at, :duration
34
+
35
+ # Serialize payload data
36
+ serialize :payload
37
+
38
+ # Select scopes
39
+ scope :requests, where(:name => "rack.request")
40
+ scope :by_name, lambda { |name| where(:name => name) }
41
+ scope :by_request_id, lambda { |request_id| where(:request_id => request_id) }
42
+
43
+ # Order scopes
44
+ # We need to add the id in the earliest and latest scope since the database
45
+ # does not store miliseconds. The id then comes as second criteria, since
46
+ # the ones started first are first saved in the database.
47
+ scope :earliest, order("started_at ASC, id ASC")
48
+ scope :latest, order("started_at DESC, id DESC")
49
+ scope :slowest, order("duration DESC")
50
+ scope :fastest, order("duration ASC")
51
+ end
52
+
53
+ module ClassMethods
54
+ def connections_ids
55
+ self.connection_pool.connections.map(&:object_id)
56
+ end
57
+ end
58
+
59
+ protected
60
+
61
+ def save_metric!
62
+ save!
63
+ end
64
+ end
65
+ end
66
+ end