rails_metrics 0.1

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