rails_customerbeats 0.0.4
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.
- data/CHANGELOG.rdoc +3 -0
- data/Gemfile +10 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +2 -0
- data/Rakefile +58 -0
- data/TODO.rdoc +1 -0
- data/app/controllers/rails_customerbeats_controller.rb +82 -0
- data/app/helpers/rails_customerbeats_helper.rb +164 -0
- data/app/views/layouts/rails_customerbeats.html.erb +21 -0
- data/app/views/rails_customerbeats/_request.html.erb +21 -0
- data/app/views/rails_customerbeats/_row.html.erb +28 -0
- data/app/views/rails_customerbeats/all.html.erb +26 -0
- data/app/views/rails_customerbeats/chart.html.erb +49 -0
- data/app/views/rails_customerbeats/index.html.erb +21 -0
- data/app/views/rails_customerbeats/show.html.erb +41 -0
- data/config/routes.rb +10 -0
- data/lib/generators/rails_customerbeats_generator.rb +33 -0
- data/lib/rails_customerbeats/async_consumer.rb +54 -0
- data/lib/rails_customerbeats/engine.rb +43 -0
- data/lib/rails_customerbeats/middleware.rb +27 -0
- data/lib/rails_customerbeats/orm/active_record.rb +79 -0
- data/lib/rails_customerbeats/orm/data_mapper.rb +88 -0
- data/lib/rails_customerbeats/payload_parser.rb +134 -0
- data/lib/rails_customerbeats/store.rb +137 -0
- data/lib/rails_customerbeats/version.rb +3 -0
- data/lib/rails_customerbeats.rb +121 -0
- data/public/images/rails_customerbeats/arrow_down.png +0 -0
- data/public/images/rails_customerbeats/arrow_up.png +0 -0
- data/public/images/rails_customerbeats/cancel.png +0 -0
- data/public/images/rails_customerbeats/chart_pie.png +0 -0
- data/public/images/rails_customerbeats/page_white_delete.png +0 -0
- data/public/images/rails_customerbeats/page_white_go.png +0 -0
- data/public/images/rails_customerbeats/tick.png +0 -0
- data/public/javascripts/rails_customerbeats/g.pie-min.js +6 -0
- data/public/javascripts/rails_customerbeats/g.raphael-min.js +5 -0
- data/public/javascripts/rails_customerbeats/raphael-min.js +5 -0
- data/public/stylesheets/rails_customerbeats.css +135 -0
- data/test/dummy/app/controllers/application_controller.rb +4 -0
- data/test/dummy/app/controllers/users_controller.rb +43 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/models/metric.rb +3 -0
- data/test/dummy/app/models/notification.rb +7 -0
- data/test/dummy/app/models/user.rb +2 -0
- data/test/dummy/config/application.rb +52 -0
- data/test/dummy/config/boot.rb +9 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +19 -0
- data/test/dummy/config/environments/production.rb +33 -0
- data/test/dummy/config/environments/test.rb +31 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookie_verification_secret.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/routes.rb +60 -0
- data/test/dummy/db/migrate/20100106152343_create_metrics.rb +17 -0
- data/test/dummy/db/migrate/20100108120821_create_users.rb +13 -0
- data/test/integration/instrumentation_test.rb +100 -0
- data/test/integration/navigation_test.rb +103 -0
- data/test/orm/active_record_test.rb +47 -0
- data/test/payload_parser_test.rb +36 -0
- data/test/rails_customerbeats_test.rb +43 -0
- data/test/store_test.rb +81 -0
- data/test/support/helpers.rb +16 -0
- data/test/support/instrumentation.rb +18 -0
- data/test/support/mock_store.rb +34 -0
- data/test/support/webrat/integrations/rails.rb +31 -0
- data/test/test_helper.rb +25 -0
- metadata +142 -0
data/CHANGELOG.rdoc
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
Copyright 2010 Engine Yard. http://www.engineyard.com
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
5
|
+
a copy of this software and associated documentation files (the
|
|
6
|
+
"Software"), to deal in the Software without restriction, including
|
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
8
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
9
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
10
|
+
the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be
|
|
13
|
+
included in all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
19
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
20
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
21
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
|
|
3
|
+
require "rake"
|
|
4
|
+
require "rake/testtask"
|
|
5
|
+
require "rdoc/task"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
require File.expand_path("../lib/rails_customerbeat/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
|
+
RDoc::Task.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_customerbeat"
|
|
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_customerbeat.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
|
data/TODO.rdoc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
class RailsCustomerbeatsController < ApplicationController
|
|
2
|
+
respond_to :html
|
|
3
|
+
|
|
4
|
+
# GET /rails_customerbeats
|
|
5
|
+
def index
|
|
6
|
+
@metrics = order_scopes(RailsCustomerbeats.store.requests)
|
|
7
|
+
@metrics_count = @metrics.count
|
|
8
|
+
@metrics = with_pagination(@metrics)
|
|
9
|
+
respond_with(@metrics)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# GET /rails_customerbeats/1/chart
|
|
13
|
+
def chart
|
|
14
|
+
@metrics = RailsCustomerbeats.store.earliest.by_request_id(params[:id]).all
|
|
15
|
+
@request = RailsCustomerbeats.store.mount_tree(@metrics.reverse)
|
|
16
|
+
respond_with(@metrics)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# GET /rails_customerbeats
|
|
20
|
+
def all
|
|
21
|
+
@metrics = all_scopes(RailsCustomerbeats.store)
|
|
22
|
+
@metrics_count = @metrics.count
|
|
23
|
+
@metrics = with_pagination(@metrics)
|
|
24
|
+
respond_with(@metrics)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# GET /rails_customerbeats/1
|
|
28
|
+
def show
|
|
29
|
+
@metric = find_store(params[:id])
|
|
30
|
+
respond_with(@metric)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# DELETE /rails_customerbeats/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_customerbeats_path)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# DELETE /rails_customerbeats/destroy_all
|
|
42
|
+
def destroy_all
|
|
43
|
+
count = all_scopes(RailsCustomerbeats.store).send(RailsCustomerbeats::ORM.delete_all)
|
|
44
|
+
flash[:notice] = "All #{count} selected metrics were deleted."
|
|
45
|
+
redirect_to rails_customerbeats_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
|
+
if scope.respond_to?(:limit)
|
|
54
|
+
scope.limit(@limit).offset(@offset).all
|
|
55
|
+
else
|
|
56
|
+
scope.all(:limit => @limit, :offset => @offset)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def by_scopes(store)
|
|
61
|
+
@by_name = params[:by_name].presence
|
|
62
|
+
store = store.by_name(@by_name) if @by_name
|
|
63
|
+
store
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def order_scopes(store)
|
|
67
|
+
@order_by = (valid_order_by? ? params[:order_by] : :latest).to_sym
|
|
68
|
+
store = store.send(@order_by)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def all_scopes(store)
|
|
72
|
+
order_scopes(by_scopes(store))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def valid_order_by?
|
|
76
|
+
RailsCustomerbeats::Store::VALID_ORDERS.include?(params[:order_by])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def find_store(id)
|
|
80
|
+
RailsCustomerbeats.store.send(RailsCustomerbeats::ORM.primary_key_finder, id)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
module RailsCustomerbeatsHelper
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
module Pagination
|
|
6
|
+
# Returns information about pagination
|
|
7
|
+
def pagination_info
|
|
8
|
+
maximum = [@metrics_count, @offset + @limit].min
|
|
9
|
+
"#{@offset + 1} - #{maximum} of #{@metrics_count}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Shows per page links
|
|
13
|
+
def show_per_page(values)
|
|
14
|
+
values.map do |i|
|
|
15
|
+
link_to_unless(@limit == i, i.to_s, url_for(params.merge(:limit => i)))
|
|
16
|
+
end.join(" | ").html_safe
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Shows previous link for pagination
|
|
20
|
+
def previous_link
|
|
21
|
+
link = url_for(params.merge(:offset => [0, @offset - @limit].max))
|
|
22
|
+
link_to_if(@offset > 0, "Previous", link)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Show next link for pagination
|
|
26
|
+
def next_link
|
|
27
|
+
link = url_for(params.merge(:offset => @offset + @limit))
|
|
28
|
+
link_to_if(@offset + @limit < @metrics_count, "Next", link)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Add pagination to footlinks
|
|
32
|
+
def paginate!
|
|
33
|
+
content_for :rails_customerbeats_footlinks do
|
|
34
|
+
content_tag(:p, [previous_link, pagination_info, next_link].join(" | "),nil,false) <<
|
|
35
|
+
content_tag(:p, "Show per page: #{show_per_page([10, 25, 50, 100])}",nil,false)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
module PayloadInspect
|
|
41
|
+
# Inspect payload to show more human readable information.
|
|
42
|
+
def payload_inspect(hash)
|
|
43
|
+
hash = hash.sort {|a,b| a[0].to_s <=> b[0].to_s }
|
|
44
|
+
content = []
|
|
45
|
+
|
|
46
|
+
hash.each do |key, value|
|
|
47
|
+
content << (content_tag(:b, key.to_s.humanize).safe_concat("<br />") << pretty_inspect(value))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
content.map!{ |c| content_tag(:p, c) }
|
|
51
|
+
content.join("\n").html_safe
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Inspect a value using a more readable format.
|
|
55
|
+
def pretty_inspect(object)
|
|
56
|
+
case object
|
|
57
|
+
when String
|
|
58
|
+
object
|
|
59
|
+
when Array
|
|
60
|
+
"[#{object.map(&:inspect).join(", ")}]"
|
|
61
|
+
when Hash
|
|
62
|
+
hash = object.map { |k,v| " #{k.inspect} => #{pretty_inspect(v)}" }.join(",\n")
|
|
63
|
+
if object.size == 1
|
|
64
|
+
"{ #{hash[2..-1]} }"
|
|
65
|
+
else
|
|
66
|
+
"{\n#{hash}\n}"
|
|
67
|
+
end
|
|
68
|
+
else
|
|
69
|
+
object.inspect
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
module Scoping
|
|
75
|
+
# Returns information about scope
|
|
76
|
+
def scopes_info
|
|
77
|
+
filters = []
|
|
78
|
+
filters << "name" if @by_name
|
|
79
|
+
filters.map!{ |i| content_tag(:b, i) }
|
|
80
|
+
|
|
81
|
+
content = []
|
|
82
|
+
content << "filtered by #{filters.to_sentence}" unless filters.empty?
|
|
83
|
+
content << "ordered by <b>#{@order_by.to_s.humanize.downcase}</b>"
|
|
84
|
+
content.to_sentence.html_safe
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Link to set a by_scope using the given content. If no value is given,
|
|
88
|
+
# the content is used as link value as well.
|
|
89
|
+
def link_to_set_by_scope(metric, what)
|
|
90
|
+
value = metric.send(what)
|
|
91
|
+
return value if instance_variable_get(:"@by_#{what}")
|
|
92
|
+
link_to value, url_for_scope(:"by_#{what}" => value, :action => "all"), :title => value
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Link to clear a by_scope using a cancel image.
|
|
96
|
+
def link_to_clear_by_scope(what)
|
|
97
|
+
return unless instance_variable_get(:"@by_#{what}")
|
|
98
|
+
link_to_set_scope_with_image("rails_customerbeats/cancel.png",
|
|
99
|
+
"Remove #{what.to_s.humanize.inspect} filter", :"by_#{what}" => nil)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Link to order by scopes by using two arrows, one up and other down
|
|
103
|
+
def link_to_order_by_scopes(up, down)
|
|
104
|
+
link_to_set_scope_with_image("images/r_metrics/arrow_up.png", "Order by #{up}", :order_by => up) <<
|
|
105
|
+
link_to_set_scope_with_image("images/r_metrics/arrow_down.png", "Order by #{down}", :order_by => down)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
protected
|
|
109
|
+
|
|
110
|
+
def url_for_scope(hash) #:nodoc:
|
|
111
|
+
url_for(params.except(:limit, :offset, :id).merge!(hash))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def link_to_set_scope_with_image(src, title, scope) #:nodoc:
|
|
115
|
+
image = image_tag(src, :title => title, :alt => title)
|
|
116
|
+
link = url_for_scope(scope)
|
|
117
|
+
link_to image, link, :title => title
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
module Links
|
|
122
|
+
# Links to image inside rails_customerbeats if the given path it's not the current page using the given title.
|
|
123
|
+
def link_to_image_unless_current(icon, path, title)
|
|
124
|
+
return if current_page?(path)
|
|
125
|
+
image = image_tag("images/r_metrics/#{icon}.png", :title => title, :alt => title)
|
|
126
|
+
link_to image, path, :title => title
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Add action icons to the current page.
|
|
130
|
+
def add_action_links!(metric)
|
|
131
|
+
concat link_to_image_unless_current(:chart_pie, chart_rails_customerbeat_path(metric.request_id), "Chart")
|
|
132
|
+
concat link_to_image_unless_current(:page_white_go, rails_customerbeat_path(metric), "Show")
|
|
133
|
+
form_tag(rails_customerbeat_path(metric), :method => :delete) do
|
|
134
|
+
image_submit_tag "images/r_metrics/page_white_delete.png", :onclick => "return confirm('Are you sure?')", :alt => "Delete", :title => "Delete"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def nagivation_links
|
|
139
|
+
@navigation_links ||= begin
|
|
140
|
+
links = []
|
|
141
|
+
links << link_to("All metrics", all_rails_customerbeats_path)
|
|
142
|
+
links << link_to("Requests", rails_customerbeats_path)
|
|
143
|
+
links << link_to("Back", :back)
|
|
144
|
+
links.join(" | ").html_safe
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
include Pagination
|
|
150
|
+
include PayloadInspect
|
|
151
|
+
include Scoping
|
|
152
|
+
include Links
|
|
153
|
+
|
|
154
|
+
# Returns pagination and scopes information
|
|
155
|
+
def pagination_and_scopes_info(countable)
|
|
156
|
+
countable = countable.to_s.pluralize unless @metrics_count == 1
|
|
157
|
+
"Showing #{pagination_info} #{countable} ".html_safe << scopes_info
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Create a table row using rails_customerbeats_#{id} as row id and odd and even as classes.
|
|
161
|
+
def rails_customerbeats_table_row_for(metric, &block)
|
|
162
|
+
content_tag(:tr, :id => "rails_customerbeat_#{metric.id}", :class => cycle("odd", "even"), &block)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>RailsCustomerbeats: <%= controller.action_name %></title>
|
|
5
|
+
<%= stylesheet_link_tag 'stylesheets/r_metrics' %>
|
|
6
|
+
<%= javascript_include_tag 'javascripts/r_metrics/raphael-min', 'javascripts/r_metrics/g.raphael-min', 'javascripts/r_metrics/g.pie-min'%>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<%= content_tag(:div, content_tag(:p, notice), :class => :notice) if notice.present? %>
|
|
10
|
+
<div id="rails_customerbeats_header">
|
|
11
|
+
<h1><%= link_to "RailsCustomerbeats", rails_customerbeats_path %></h1>
|
|
12
|
+
<div><%= yield :rails_customerbeats_header %></div>
|
|
13
|
+
<p><%= nagivation_links %></p>
|
|
14
|
+
</div>
|
|
15
|
+
<%= yield %>
|
|
16
|
+
<div id="rails_customerbeats_footlinks">
|
|
17
|
+
<%= yield :rails_customerbeats_footlinks %>
|
|
18
|
+
<p><%= nagivation_links %></p>
|
|
19
|
+
</div>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<%= rails_customerbeats_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,28 @@
|
|
|
1
|
+
<%= rails_customerbeats_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="Id">
|
|
9
|
+
<%= metric.request_id %>
|
|
10
|
+
</td>
|
|
11
|
+
|
|
12
|
+
<td class="name">
|
|
13
|
+
<%= link_to_set_by_scope metric, :name %>
|
|
14
|
+
</td>
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
<td class="duration">
|
|
18
|
+
<%= "%.1f" % metric.duration_in_ms %> <%= "(#{"%.1f" % metric.exclusive_duration_in_ms})" if metric.children.any? %> ms
|
|
19
|
+
</td>
|
|
20
|
+
|
|
21
|
+
<td class="payload">
|
|
22
|
+
<%= payload_inspect(metric.payload) %>
|
|
23
|
+
</td>
|
|
24
|
+
|
|
25
|
+
<td class="actions">
|
|
26
|
+
<% add_action_links!(metric) %>
|
|
27
|
+
</td>
|
|
28
|
+
<% end %>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<% if @metrics.empty? %>
|
|
2
|
+
<h2>No metrics so far, navigate on your app and come back.</h2>
|
|
3
|
+
<% else %>
|
|
4
|
+
<% content_for(:rails_customerbeats_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_customerbeats_table" class="all">
|
|
13
|
+
<tr>
|
|
14
|
+
<th>When<br /><%= link_to_order_by_scopes(:earliest, :latest) %></th>
|
|
15
|
+
<th>Id</th>
|
|
16
|
+
<th>Name<br /><%= link_to_clear_by_scope(:name) %></th>
|
|
17
|
+
<th>Duration<br /><%= link_to_order_by_scopes(:slowest, :fastest) %></th>
|
|
18
|
+
<th>Payload</th>
|
|
19
|
+
<th></th>
|
|
20
|
+
</tr>
|
|
21
|
+
|
|
22
|
+
<%= render :partial => "row", :collection => @metrics, :as => :metric %>
|
|
23
|
+
</table>
|
|
24
|
+
|
|
25
|
+
<% paginate! %>
|
|
26
|
+
<% end %>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<% content_for(:rails_customerbeats_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_customerbeats_#{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_customerbeats_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_customerbeats_header) do %>
|
|
5
|
+
<%= pagination_and_scopes_info(:requests) %>
|
|
6
|
+
<% end %>
|
|
7
|
+
|
|
8
|
+
<table id="rails_customerbeats_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_customerbeats_header) do %>
|
|
2
|
+
Showing metric #<%= @metric.id %>
|
|
3
|
+
<div class="actions"><% add_action_links!(@metric) %></div>
|
|
4
|
+
<% end %>
|
|
5
|
+
|
|
6
|
+
<table id="rails_customerbeats_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_customerbeat_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>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
class RailsCustomerbeatsGenerator < 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
|
+
require "rails_customerbeats/orm/#{Rails::Generators.options[:rails][:orm]}"
|
|
18
|
+
invoke "model", [name].concat(RailsCustomerbeats::ORM.metric_model_properties),
|
|
19
|
+
:timestamps => false, :test_framework => false, :migration => options.migration?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def add_model_config
|
|
23
|
+
RailsCustomerbeats::ORM.add_metric_model_config(self, file_name, class_name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def add_application_config
|
|
27
|
+
inject_into_class "config/application.rb", "Application", <<-CONTENT
|
|
28
|
+
# Set rails metrics store
|
|
29
|
+
config.rails_customerbeats.set_store = lambda { ::#{class_name} }
|
|
30
|
+
|
|
31
|
+
CONTENT
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require 'thread'
|
|
2
|
+
|
|
3
|
+
module RailsCustomerbeats
|
|
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
|