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,3 @@
1
+ == 0.1
2
+
3
+ * First release of RailsMetrics by José Valim. It includes generators for bootstrap, stores instrumentations using a in process Queue and works as engine allowing you to see your metrics. The only ORM supported so far is ActiveRecord.
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source :gemcutter
2
+
3
+ gem "rails", "3.0.0.beta"
4
+ gem "mysql", "2.7"
5
+ gem "webrat", "0.7.0"
6
+
7
+ if RUBY_VERSION < '1.9'
8
+ gem "ruby-debug", ">= 0.10.3"
9
+ end
@@ -0,0 +1,20 @@
1
+ Copyright 2010 Engine Yard. http://www.engineyard.com
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,58 @@
1
+ == RailsMetrics
2
+
3
+ RailsMetrics is an engine that hooks into your application to listen ActiveSupport::Notifications and show you statistics about them. RailsMetrics uses threads to save those notifications in the database through an in-process Queue. You can see charts based on each request and see what queries, actions, templates rendering, etc. are slow.
4
+
5
+ So far RailsMetrics supports only ActiveRecord as ORM but adding other datastore should be easy (take a look at lib/rails_metrics/orm/active_record.rb). Due to the threaded behavior in RailsMetrics, you cannot use SQLite as database, however MySQL and PostgreSQL work fine.
6
+
7
+ There's an example application at http://github.com/engineyard/rails_metrics_example .
8
+
9
+ == Installation
10
+
11
+ Installing RailsMetrics is simple. Since it's a Rails::Engine, you need to install it as gem:
12
+
13
+ gem install rails_metrics
14
+
15
+ Then add it to your gemfile. So far, I recommend you to install it only on development:
16
+
17
+ group :development do
18
+ gem "rails_metrics"
19
+ end
20
+
21
+ Finally, create your model using the built-in generator:
22
+
23
+ rails g rails_metrics Metric
24
+
25
+ You can choose another name than Metric as your rails metrics store. Run the created migration, restart your server and now you should be able to access "/rails_metrics". Navigate on your application and head back to "/rails_metrics" to see the stored data.
26
+
27
+ == Configuration
28
+
29
+ There are three parameters you can add to your application to configure RailsMetrics. The first one is to set the RailsMetrics store, which is done automatically when you use the generator:
30
+
31
+ config.rails_metrics.set_store = lambda { ::Metric }
32
+
33
+ The other parameters allows you to ignore an specific notification, based on a given block
34
+ or on its name. Let's suppose you have a plugin in your application which is yielding an useless notification. You can silence it by doing:
35
+
36
+ config.rails_metrics.ignore_patterns << /verbose_plugin/
37
+
38
+ If you need to ignore a notification based on its payload, it's also easy:
39
+
40
+ config.rails_metrics.ignore_lambdas[:verbose_plugin] = lambda { |name, payload|
41
+ payload[:some_value] =~ /some_expression/
42
+ }
43
+
44
+ == TODO
45
+
46
+ Please refer to TODO file.
47
+
48
+ == Maintainers
49
+
50
+ * José Valim (http://github.com/josevalim)
51
+
52
+ == Bugs and Feedback
53
+
54
+ If you discover any bugs or want to drop a line, feel free to create an issue on GitHub.
55
+
56
+ http://github.com/engineyard/rails_metrics/issues
57
+
58
+ MIT License. Copyright 2010 Engine Yard. http://www.engineyard.com
@@ -0,0 +1,58 @@
1
+ # encoding: UTF-8
2
+
3
+ require "rake"
4
+ require "rake/testtask"
5
+ require "rake/rdoctask"
6
+ require "fileutils"
7
+ require File.expand_path("../lib/rails_metrics/version", __FILE__)
8
+
9
+ desc "Default: run unit tests"
10
+ task :default => :test
11
+
12
+ desc "Prepare environment for tests"
13
+ task :prepare do
14
+ FileUtils.cd File.expand_path("../test/dummy", __FILE__)
15
+ system("rake db:create:all")
16
+ system("rake db:migrate")
17
+ system("rake db:test:clone")
18
+ end
19
+
20
+ desc "Start the server for the dummy application used in tests"
21
+ task :server do
22
+ exec("test/dummy/script/rails server")
23
+ end
24
+
25
+ desc "Test RailsMetrics"
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << "test"
28
+ t.pattern = "test/**/*_test.rb"
29
+ t.verbose = true
30
+ end
31
+
32
+ desc "Generate documentation for RailsMetrics"
33
+ Rake::RDocTask.new(:rdoc) do |rdoc|
34
+ rdoc.rdoc_dir = "rdoc"
35
+ rdoc.title = "RailsMetrics"
36
+ rdoc.options << "--line-numbers" << "--inline-source"
37
+ rdoc.rdoc_files.include("README.rdoc")
38
+ rdoc.rdoc_files.include("lib/**/*.rb")
39
+ end
40
+
41
+ begin
42
+ require "jeweler"
43
+ Jeweler::Tasks.new do |s|
44
+ s.name = "rails_metrics"
45
+ s.version = RailsMetrics::VERSION
46
+ s.summary = "Metrics measurement for your app on top of ActiveSupport::Notifications"
47
+ s.email = "contact@engineyard.com"
48
+ s.homepage = "http://github.com/engineyard"
49
+ s.description = "Metrics measurement for your app on top of ActiveSupport::Notifications"
50
+ s.authors = ["José Valim"]
51
+ s.files = FileList["[A-Z]*", "{app,config,lib,public}/**/*"]
52
+ s.files.exclude("public/javascripts/rails_metrics.js")
53
+ end
54
+
55
+ Jeweler::GemcutterTasks.new
56
+ rescue LoadError
57
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install jeweler"
58
+ end
@@ -0,0 +1 @@
1
+ * Allow to set the maximum number of requests to store
@@ -0,0 +1,78 @@
1
+ class RailsMetricsController < ApplicationController
2
+ respond_to :html
3
+
4
+ # GET /rails_metrics
5
+ def index
6
+ @metrics = order_scopes(RailsMetrics.store.requests)
7
+ @metrics_count = @metrics.count
8
+ @metrics = with_pagination(@metrics)
9
+ respond_with(@metrics)
10
+ end
11
+
12
+ # GET /rails_metrics/1/chart
13
+ def chart
14
+ @metrics = RailsMetrics.store.earliest.by_request_id(params[:id]).all
15
+ @request = RailsMetrics.store.mount_tree(@metrics.reverse)
16
+ respond_with(@metrics)
17
+ end
18
+
19
+ # GET /rails_metrics
20
+ def all
21
+ @metrics = all_scopes(RailsMetrics.store)
22
+ @metrics_count = @metrics.count
23
+ @metrics = with_pagination(@metrics)
24
+ respond_with(@metrics)
25
+ end
26
+
27
+ # GET /rails_metrics/1
28
+ def show
29
+ @metric = find_store(params[:id])
30
+ respond_with(@metric)
31
+ end
32
+
33
+ # DELETE /rails_metrics/1
34
+ def destroy
35
+ @metric = find_store(params[:id])
36
+ @metric.destroy
37
+ flash[:notice] = "Metric ##{@metric.id} was deleted with success."
38
+ respond_with(@metric, :location => rails_metrics_path)
39
+ end
40
+
41
+ # DELETE /rails_metrics/destroy_all
42
+ def destroy_all
43
+ count = all_scopes(RailsMetrics.store).delete_all
44
+ flash[:notice] = "All #{count} selected metrics were deleted."
45
+ redirect_to rails_metrics_path
46
+ end
47
+
48
+ protected
49
+
50
+ def with_pagination(scope)
51
+ @limit = (params[:limit].presence || 50).to_i
52
+ @offset = (params[:offset].presence || 0).to_i
53
+ scope.limit(@limit).offset(@offset).all
54
+ end
55
+
56
+ def by_scopes(store)
57
+ @by_name = params[:by_name].presence
58
+ store = store.by_name(@by_name) if @by_name
59
+ store
60
+ end
61
+
62
+ def order_scopes(store)
63
+ @order_by = (valid_order_by? ? params[:order_by] : :latest).to_sym
64
+ store = store.send(@order_by)
65
+ end
66
+
67
+ def all_scopes(store)
68
+ order_scopes(by_scopes(store))
69
+ end
70
+
71
+ def valid_order_by?
72
+ RailsMetrics::Store::VALID_ORDERS.include?(params[:order_by])
73
+ end
74
+
75
+ def find_store(id)
76
+ RailsMetrics.store.find(id)
77
+ end
78
+ end
@@ -0,0 +1,161 @@
1
+ module RailsMetricsHelper
2
+ module Pagination
3
+ # Returns information about pagination
4
+ def pagination_info
5
+ maximum = [@metrics_count, @offset + @limit].min
6
+ "#{@offset + 1} - #{maximum} of #{@metrics_count}"
7
+ end
8
+
9
+ # Shows per page links
10
+ def show_per_page(values)
11
+ values.map do |i|
12
+ link_to_unless(@limit == i, i.to_s, url_for(params.merge(:limit => i)))
13
+ end.join(" | ").html_safe
14
+ end
15
+
16
+ # Shows previous link for pagination
17
+ def previous_link
18
+ link = url_for(params.merge(:offset => [0, @offset - @limit].max))
19
+ link_to_if(@offset > 0, "Previous", link)
20
+ end
21
+
22
+ # Show next link for pagination
23
+ def next_link
24
+ link = url_for(params.merge(:offset => @offset + @limit))
25
+ link_to_if(@offset + @limit < @metrics_count, "Next", link)
26
+ end
27
+
28
+ # Add pagination to footlinks
29
+ def paginate!
30
+ content_for :rails_metrics_footlinks do
31
+ content_tag(:p, [previous_link, pagination_info, next_link].join(" | ")) <<
32
+ content_tag(:p, "Show per page: #{show_per_page [10, 25, 50, 100]}")
33
+ end
34
+ end
35
+ end
36
+
37
+ module PayloadInspect
38
+ # Inspect payload to show more human readable information.
39
+ def payload_inspect(hash)
40
+ hash = hash.sort {|a,b| a[0].to_s <=> b[0].to_s }
41
+ content = []
42
+
43
+ hash.each do |key, value|
44
+ content << (content_tag(:b, key.to_s.humanize).safe_concat("<br />") << pretty_inspect(value))
45
+ end
46
+
47
+ content.map!{ |c| content_tag(:p, c) }
48
+ content.join("\n").html_safe
49
+ end
50
+
51
+ # Inspect a value using a more readable format.
52
+ def pretty_inspect(object)
53
+ case object
54
+ when String
55
+ object
56
+ when Array
57
+ "[#{object.map(&:inspect).join(", ")}]"
58
+ when Hash
59
+ hash = object.map { |k,v| " #{k.inspect} => #{pretty_inspect(v)}" }.join(",\n")
60
+ if object.size == 1
61
+ "{ #{hash[2..-1]} }"
62
+ else
63
+ "{\n#{hash}\n}"
64
+ end
65
+ else
66
+ object.inspect
67
+ end
68
+ end
69
+ end
70
+
71
+ module Scoping
72
+ # Returns information about scope
73
+ def scopes_info
74
+ filters = []
75
+ filters << "name" if @by_name
76
+ filters.map!{ |i| content_tag(:b, i) }
77
+
78
+ content = []
79
+ content << "filtered by #{filters.to_sentence}" unless filters.empty?
80
+ content << "ordered by <b>#{@order_by.to_s.humanize.downcase}</b>"
81
+ content.to_sentence.html_safe
82
+ end
83
+
84
+ # Link to set a by_scope using the given content. If no value is given,
85
+ # the content is used as link value as well.
86
+ def link_to_set_by_scope(metric, what)
87
+ value = metric.send(what)
88
+ return value if instance_variable_get(:"@by_#{what}")
89
+ link_to value, url_for_scope(:"by_#{what}" => value, :action => "all"), :title => value
90
+ end
91
+
92
+ # Link to clear a by_scope using a cancel image.
93
+ def link_to_clear_by_scope(what)
94
+ return unless instance_variable_get(:"@by_#{what}")
95
+ link_to_set_scope_with_image("rails_metrics/cancel.png",
96
+ "Remove #{what.to_s.humanize.inspect} filter", :"by_#{what}" => nil)
97
+ end
98
+
99
+ # Link to order by scopes by using two arrows, one up and other down
100
+ def link_to_order_by_scopes(up, down)
101
+ link_to_set_scope_with_image("rails_metrics/arrow_up.png", "Order by #{up}", :order_by => up) <<
102
+ link_to_set_scope_with_image("rails_metrics/arrow_down.png", "Order by #{down}", :order_by => down)
103
+ end
104
+
105
+ protected
106
+
107
+ def url_for_scope(hash) #:nodoc:
108
+ url_for(params.except(:limit, :offset, :id).merge!(hash))
109
+ end
110
+
111
+ def link_to_set_scope_with_image(src, title, scope) #:nodoc:
112
+ image = image_tag(src, :title => title, :alt => title)
113
+ link = url_for_scope(scope)
114
+ link_to image, link, :title => title
115
+ end
116
+ end
117
+
118
+ module Links
119
+ # Links to image inside rails_metrics if the given path it's not the current page using the given title.
120
+ def link_to_image_unless_current(icon, path, title)
121
+ return if current_page?(path)
122
+ image = image_tag("rails_metrics/#{icon}.png", :title => title, :alt => title)
123
+ link_to image, path, :title => title
124
+ end
125
+
126
+ # Add action icons to the current page.
127
+ def add_action_links!(metric)
128
+ concat link_to_image_unless_current(:chart_pie, chart_rails_metric_path(metric.request_id), "Chart")
129
+ concat link_to_image_unless_current(:page_white_go, rails_metric_path(metric), "Show")
130
+ form_tag(rails_metric_path(metric), :method => :delete) do
131
+ image_submit_tag "rails_metrics/page_white_delete.png", :onclick => "return confirm('Are you sure?')", :alt => "Delete", :title => "Delete"
132
+ end
133
+ end
134
+
135
+ def nagivation_links
136
+ @navigation_links ||= begin
137
+ links = []
138
+ links << link_to("All metrics", all_rails_metrics_path)
139
+ links << link_to("Requests", rails_metrics_path)
140
+ links << link_to("Back", :back)
141
+ links.join(" | ").html_safe
142
+ end
143
+ end
144
+ end
145
+
146
+ include Pagination
147
+ include PayloadInspect
148
+ include Scoping
149
+ include Links
150
+
151
+ # Returns pagination and scopes information
152
+ def pagination_and_scopes_info(countable)
153
+ countable = countable.to_s.pluralize unless @metrics_count == 1
154
+ "Showing #{pagination_info} #{countable} ".html_safe << scopes_info
155
+ end
156
+
157
+ # Create a table row using rails_metrics_#{id} as row id and odd and even as classes.
158
+ def rails_metrics_table_row_for(metric, &block)
159
+ content_tag(:tr, :id => "rails_metric_#{metric.id}", :class => cycle("odd", "even"), &block)
160
+ end
161
+ end
@@ -0,0 +1,21 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>RailsMetrics: <%= controller.action_name %></title>
5
+ <%= stylesheet_link_tag 'rails_metrics' %>
6
+ <%= javascript_include_tag 'rails_metrics/raphael-min', 'rails_metrics/g.raphael-min', 'rails_metrics/g.pie-min', :cache => "rails_metrics" %>
7
+ </head>
8
+ <body>
9
+ <%= content_tag(:div, content_tag(:p, notice), :class => :notice) if notice.present? %>
10
+ <div id="rails_metrics_header">
11
+ <h1><%= link_to "RailsMetrics", rails_metrics_path %></h1>
12
+ <div><%= yield :rails_metrics_header %></div>
13
+ <p><%= nagivation_links %></p>
14
+ </div>
15
+ <%= yield %>
16
+ <div id="rails_metrics_footlinks">
17
+ <%= yield :rails_metrics_footlinks %>
18
+ <p><%= nagivation_links %></p>
19
+ </div>
20
+ </body>
21
+ </html>
@@ -0,0 +1,21 @@
1
+ <% rails_metrics_table_row_for metric do %>
2
+ <td class="when">
3
+ <%= metric.started_at.strftime("%d %b %H:%M:%S") %>
4
+ </td>
5
+
6
+ <td class="method">
7
+ <%= metric.payload[:method] %>
8
+ </td>
9
+
10
+ <td class="path">
11
+ <%= metric.payload[:path] %>
12
+ </td>
13
+
14
+ <td class="duration">
15
+ <%= "%.1f" % metric.duration_in_ms %> ms
16
+ </td>
17
+
18
+ <td class="actions">
19
+ <% add_action_links!(metric) %>
20
+ </td>
21
+ <% end %>
@@ -0,0 +1,23 @@
1
+ <% rails_metrics_table_row_for metric do %>
2
+ <% unless (skip_timestamps ||= false) %>
3
+ <td class="started_at">
4
+ <%= metric.started_at.strftime("%d %b %H:%M:%S") %>
5
+ </td>
6
+ <% end %>
7
+
8
+ <td class="name">
9
+ <%= link_to_set_by_scope metric, :name %>
10
+ </td>
11
+
12
+ <td class="duration">
13
+ <%= "%.1f" % metric.duration_in_ms %> <%= "(#{"%.1f" % metric.exclusive_duration_in_ms})" if metric.children.any? %> ms
14
+ </td>
15
+
16
+ <td class="payload">
17
+ <%= payload_inspect(metric.payload) %>
18
+ </td>
19
+
20
+ <td class="actions">
21
+ <% add_action_links!(metric) %>
22
+ </td>
23
+ <% end %>