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
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module RailsCustomerbeats
|
|
2
|
+
class Engine < Rails::Engine
|
|
3
|
+
|
|
4
|
+
# Initialize configure parameters
|
|
5
|
+
config.rails_customerbeats = ActiveSupport::OrderedOptions.new
|
|
6
|
+
|
|
7
|
+
config.rails_customerbeats.ignore_lambdas = {}
|
|
8
|
+
config.rails_customerbeats.ignore_patterns = [ "start_processing.action_controller",
|
|
9
|
+
"sql.active_record",
|
|
10
|
+
"!render_template.action_view",
|
|
11
|
+
"render_partial.action_view",
|
|
12
|
+
"render_template.action_view" ]
|
|
13
|
+
|
|
14
|
+
config.rails_customerbeats.ignore_lambdas['rack.request'] = lambda { |name, payload|
|
|
15
|
+
payload[:path] =~ /assets/
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
initializer "rails_customerbeats.add_middleware" do |app|
|
|
19
|
+
app.config.middleware.use RailsCustomerbeats::Middleware
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
initializer "static assets" do |app|
|
|
23
|
+
app.middleware.insert_before ::ActionDispatch::Static, ::ActionDispatch::Static, "#{root}/public/assets"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
initializer "rails_customerbeats.set_ignores" do |app|
|
|
27
|
+
RailsCustomerbeats.ignore_lambdas.merge!(app.config.rails_customerbeats.ignore_lambdas)
|
|
28
|
+
RailsCustomerbeats.ignore_patterns.concat(app.config.rails_customerbeats.ignore_patterns)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
initializer "rails_customerbeats.set_store" do |app|
|
|
32
|
+
if app.config.rails_customerbeats.set_store
|
|
33
|
+
RailsCustomerbeats.set_store(&app.config.rails_customerbeats.set_store)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
initializer "rails_customerbeats.start_subscriber" do
|
|
38
|
+
ActiveSupport::Notifications.subscribe /[^!]$/ do |*args|
|
|
39
|
+
RailsCustomerbeats.events.push(args) if RailsCustomerbeats.valid_for_storing?(args)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module RailsCustomerbeats
|
|
2
|
+
class Middleware
|
|
3
|
+
def initialize(app)
|
|
4
|
+
@app = app
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def call(env)
|
|
8
|
+
if env["PATH_INFO"] =~ /^\/rails_customerbeats/
|
|
9
|
+
@app.call(env)
|
|
10
|
+
else
|
|
11
|
+
RailsCustomerbeats.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,79 @@
|
|
|
1
|
+
# Setup to ignore any query which is not a SELECT, INSERT, UPDATE
|
|
2
|
+
# or DELETE and queries made by the own store.
|
|
3
|
+
RailsCustomerbeats.ignore :invalid_queries do |name, payload|
|
|
4
|
+
name == "active_record.sql" &&
|
|
5
|
+
payload[:sql] !~ /^(SELECT|INSERT|UPDATE|DELETE)/
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
module RailsCustomerbeats
|
|
9
|
+
module ORM
|
|
10
|
+
# Include in your model to store metrics. For ActiveRecord, you need the
|
|
11
|
+
# following setup:
|
|
12
|
+
#
|
|
13
|
+
# script/generate model Metric script/generate name:string duration:integer
|
|
14
|
+
# request_id:integer parent_id:integer payload:text started_at:datetime created_at:datetime --skip-timestamps
|
|
15
|
+
#
|
|
16
|
+
# You can use any model name you wish. Next, you need to include
|
|
17
|
+
# RailsCustomerbeats::ORM::ActiveRecord:
|
|
18
|
+
#
|
|
19
|
+
# class Metric < ActiveRecord::Base
|
|
20
|
+
# include RailsCustomerbeats::ORM::ActiveRecord
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
|
|
24
|
+
ORM.primary_key_finder = :find
|
|
25
|
+
ORM.delete_all = :delete_all
|
|
26
|
+
|
|
27
|
+
ORM.metric_model_properties = %w[
|
|
28
|
+
name:string
|
|
29
|
+
duration:integer
|
|
30
|
+
request_id:integer
|
|
31
|
+
parent_id:integer
|
|
32
|
+
payload:text
|
|
33
|
+
started_at:datetime
|
|
34
|
+
created_at:datetime
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
def self.add_metric_model_config(generator, file_name, class_name)
|
|
38
|
+
generator.inject_into_class "app/models/#{file_name}.rb", class_name, <<-CONTENT
|
|
39
|
+
include RailsCustomerbeats::ORM::#{Rails::Generators.options[:rails][:orm].to_s.camelize}
|
|
40
|
+
CONTENT
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
module ActiveRecord
|
|
44
|
+
extend ActiveSupport::Concern
|
|
45
|
+
include RailsCustomerbeats::Store
|
|
46
|
+
|
|
47
|
+
included do
|
|
48
|
+
# Create a new connection pool just for the given resource
|
|
49
|
+
establish_connection(Rails.env)
|
|
50
|
+
|
|
51
|
+
# Set required validations
|
|
52
|
+
validates_presence_of :name, :started_at, :duration
|
|
53
|
+
|
|
54
|
+
# Serialize payload data
|
|
55
|
+
serialize :payload
|
|
56
|
+
|
|
57
|
+
# Select scopes
|
|
58
|
+
scope :requests, where(:name => "rack.request")
|
|
59
|
+
scope :by_name, lambda { |name| where(:name => name) }
|
|
60
|
+
scope :by_request_id, lambda { |request_id| where(:request_id => request_id) }
|
|
61
|
+
|
|
62
|
+
# Order scopes
|
|
63
|
+
# We need to add the id in the earliest and latest scope since the database
|
|
64
|
+
# does not store miliseconds. The id then comes as second criteria, since
|
|
65
|
+
# the ones started first are first saved in the database.
|
|
66
|
+
scope :earliest, order("started_at ASC, id ASC")
|
|
67
|
+
scope :latest, order("started_at DESC, id DESC")
|
|
68
|
+
scope :slowest, order("duration DESC")
|
|
69
|
+
scope :fastest, order("duration ASC")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
protected
|
|
73
|
+
|
|
74
|
+
def save_metric!
|
|
75
|
+
save!
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Setup to ignore any query which is not a SELECT, INSERT, UPDATE
|
|
2
|
+
# or DELETE and queries made by the own store.
|
|
3
|
+
RailsCustomerbeats.ignore :invalid_queries do |name, payload|
|
|
4
|
+
name == "data_mapper.sql" &&
|
|
5
|
+
payload[:sql] !~ /^(SELECT|INSERT|UPDATE|DELETE)/
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
module RailsCustomerbeats
|
|
9
|
+
module ORM
|
|
10
|
+
|
|
11
|
+
# Include in your model to store metrics. For DataMapper, 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:object started_at:datetime created_at:datetime --skip-timestamps
|
|
16
|
+
#
|
|
17
|
+
# You can use any model name you wish. Next, you need to include
|
|
18
|
+
# RailsCustomerbeats::ORM::DataMapper:
|
|
19
|
+
#
|
|
20
|
+
# class Metric
|
|
21
|
+
# include DataMapper::Resource
|
|
22
|
+
# include RailsCustomerbeats::ORM::DataMapper
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
|
|
26
|
+
ORM.primary_key_finder = :get
|
|
27
|
+
ORM.delete_all = :destroy! # use bang version here cause we don't need no hooks
|
|
28
|
+
|
|
29
|
+
ORM.metric_model_properties = %w[
|
|
30
|
+
name:string
|
|
31
|
+
duration:integer
|
|
32
|
+
request_id:integer
|
|
33
|
+
parent_id:integer
|
|
34
|
+
payload:object
|
|
35
|
+
started_at:datetime
|
|
36
|
+
created_at:datetime
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
def self.add_metric_model_config(generator, file_name, class_name)
|
|
40
|
+
generator.inject_into_file "app/models/#{file_name}.rb",
|
|
41
|
+
" include RailsCustomerbeats::ORM::DataMapper\n",
|
|
42
|
+
{:after => " include DataMapper::Resource\n"}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
module DataMapper
|
|
46
|
+
extend ActiveSupport::Concern
|
|
47
|
+
include RailsCustomerbeats::Store
|
|
48
|
+
|
|
49
|
+
included do
|
|
50
|
+
# Set required validations
|
|
51
|
+
validates_presence_of :name, :started_at, :duration
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
module ClassMethods
|
|
55
|
+
|
|
56
|
+
# Select scopes
|
|
57
|
+
|
|
58
|
+
def requests; all(:name => 'rack.request') end
|
|
59
|
+
def by_name(name); all(:name => name ) end
|
|
60
|
+
def by_request_id(request_id); all(:request_id => request_id ) end
|
|
61
|
+
|
|
62
|
+
# Order scopes
|
|
63
|
+
# We need to add the id in the earliest and latest scope since the database
|
|
64
|
+
# does not store miliseconds. The id then comes as second criteria, since
|
|
65
|
+
# the ones started first are first saved in the database.
|
|
66
|
+
|
|
67
|
+
def earliest; all(:order => [:started_at.asc, :id.asc ]) end
|
|
68
|
+
def latest; all(:order => [:started_at.desc, :id.desc]) end
|
|
69
|
+
def slowest; all(:order => [:duration.desc ]) end
|
|
70
|
+
def fastest; all(:order => [:duration.asc ]) end
|
|
71
|
+
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Destroy all children if it's a request metric.
|
|
75
|
+
def destroy
|
|
76
|
+
self.class.by_request_id(self.id).destroy! if rack_request?
|
|
77
|
+
super
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
protected
|
|
81
|
+
|
|
82
|
+
def save_metric!
|
|
83
|
+
save!
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
module RailsCustomerbeats
|
|
2
|
+
# ActiveSupport::Notifications usually comes with extra information as the
|
|
3
|
+
# SQL query, response status and many others. This information is called payload.
|
|
4
|
+
#
|
|
5
|
+
# By default, RailsCustomerbeats stores the whole payload in the database but it allows
|
|
6
|
+
# you to manipulate it or even ignore some through the add and ignore methods.
|
|
7
|
+
#
|
|
8
|
+
# For example, "activerecord.sql" has as paylaod a hash with :name (like "Product
|
|
9
|
+
# Load"), the :sql to be performed and the :connection_id. We can remove the connection
|
|
10
|
+
# from the hash by simply providing :except:
|
|
11
|
+
#
|
|
12
|
+
# RailsCustomerbeats::PayloadParser.add "active_record.sql", :except => :name
|
|
13
|
+
#
|
|
14
|
+
# Or, we could also:
|
|
15
|
+
#
|
|
16
|
+
# RailsCustomerbeats::PayloadParser.add "active_record.sql", :slice => [:name, :sql]
|
|
17
|
+
#
|
|
18
|
+
# Finally, in some cases manipulating the hash is not enough and you might need
|
|
19
|
+
# to customize it further, as in "action_view.render_template". You can do
|
|
20
|
+
# that by giving a block which will receive the payload as argument:
|
|
21
|
+
#
|
|
22
|
+
# RailsCustomerbeats::PayloadParser.add "action_view.render_template" do |payload|
|
|
23
|
+
# payload = payload.dup
|
|
24
|
+
# payload[:template] = payload[:template].gsub("RAILS_ROOT", Rails.root)
|
|
25
|
+
# payload
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# ATTENTION: if you need to modify the payload or any of its values, be sure to
|
|
29
|
+
# .dup if first, as in the example above.
|
|
30
|
+
#
|
|
31
|
+
# If you want to ignore any payload, you can use the ignore method:
|
|
32
|
+
#
|
|
33
|
+
# RailsCustomerbeats::PayloadParser.ignore "active_record.sql"
|
|
34
|
+
#
|
|
35
|
+
module PayloadParser
|
|
36
|
+
# Holds the parsers used by RailsCustomerbeats.
|
|
37
|
+
def self.parsers
|
|
38
|
+
@parsers ||= {}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Holds the mapped paths used in prunning.
|
|
42
|
+
def self.mapped_paths
|
|
43
|
+
@mapped_paths ||= {}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Add a new parser.
|
|
47
|
+
def self.add(*names, &block)
|
|
48
|
+
options = names.extract_options!
|
|
49
|
+
|
|
50
|
+
names.each do |name|
|
|
51
|
+
parsers[name.to_s] = if block_given?
|
|
52
|
+
block
|
|
53
|
+
elsif options.present?
|
|
54
|
+
options.to_a.flatten
|
|
55
|
+
else
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Delete a previous parser
|
|
62
|
+
def self.ignore(*names)
|
|
63
|
+
names.each { |name| parsers[name.to_s] = false }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Filter the given payload based on the name given and configured parsers
|
|
67
|
+
def self.filter(name, payload)
|
|
68
|
+
parser = parsers[name]
|
|
69
|
+
case parser
|
|
70
|
+
when Array
|
|
71
|
+
payload.send(*parser)
|
|
72
|
+
when Proc
|
|
73
|
+
parser.call(payload)
|
|
74
|
+
when TrueClass, NilClass
|
|
75
|
+
payload
|
|
76
|
+
when FalseClass
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Prune paths based on the mapped paths set.
|
|
82
|
+
def self.prune_path(raw_path)
|
|
83
|
+
mapped_paths.each do |path, replacement|
|
|
84
|
+
raw_path = raw_path.gsub(path, replacement)
|
|
85
|
+
end
|
|
86
|
+
raw_path
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Make Rails.root appear as APP in pruned paths.
|
|
90
|
+
mapped_paths[Rails.root.to_s] = "RAILS_ROOT"
|
|
91
|
+
|
|
92
|
+
# Make Gem paths appear as GEM in pruned paths.
|
|
93
|
+
Gem.path.each do |path|
|
|
94
|
+
mapped_paths[File.join(path, "gems")] = "GEMS_ROOT"
|
|
95
|
+
end if defined?(Gem)
|
|
96
|
+
|
|
97
|
+
# ActiveRecord
|
|
98
|
+
add "sql.active_record" do |payload|
|
|
99
|
+
payload = payload.dup
|
|
100
|
+
payload[:sql] = payload[:sql].squeeze(" ")
|
|
101
|
+
payload.delete(:connection_id)
|
|
102
|
+
payload
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# ActionController - process action
|
|
106
|
+
add "process_action.action_controller" do |payload|
|
|
107
|
+
payload = payload.dup
|
|
108
|
+
#payload = payload.except(:path, :method, :params, :db_runtime, :view_runtime)
|
|
109
|
+
payload[:end_point] = "#{payload.delete(:controller)}##{payload.delete(:action)}"
|
|
110
|
+
#new_data = { 'session_id' => session.session_id , 'browser' => request.env['HTTP_USER_AGENT'] , 'ip_address' => request.env['REMOTE_ADDR'] }
|
|
111
|
+
#payload.merge!(new_data)
|
|
112
|
+
payload
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# ActionView
|
|
116
|
+
add "render_template.action_view", "render_partial.action_view",
|
|
117
|
+
"render_collection.action_view" do |payload|
|
|
118
|
+
returning Hash.new do |new_payload|
|
|
119
|
+
payload.each do |key, value|
|
|
120
|
+
case value
|
|
121
|
+
when NilClass
|
|
122
|
+
when String
|
|
123
|
+
new_payload[key] = prune_path(value)
|
|
124
|
+
else
|
|
125
|
+
new_payload[key] = value
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# ActionMailer
|
|
132
|
+
add "action_mailer.deliver", "action_mailer.receive", :except => :mail
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
module RailsCustomerbeats
|
|
2
|
+
# This module contains the default API for storing notifications.
|
|
3
|
+
# Imagine that you configure your store to be the Metric class:
|
|
4
|
+
#
|
|
5
|
+
# RailsCustomerbeats.set_store { Metric }
|
|
6
|
+
#
|
|
7
|
+
# Whenever a notification comes, RailsCustomerbeats instantiates a new
|
|
8
|
+
# store and call configure on it with the instrumentation arguments:
|
|
9
|
+
#
|
|
10
|
+
# metric = Metric.new
|
|
11
|
+
# metric.configure(args)
|
|
12
|
+
# metric
|
|
13
|
+
#
|
|
14
|
+
# After all metrics are configured they are nested and save_metrics! is called,
|
|
15
|
+
# where each metric saves itself and its children.
|
|
16
|
+
#
|
|
17
|
+
# The method save_metrics! is implemented below and it requires the method
|
|
18
|
+
# save_metric! to be implemented in the target class.
|
|
19
|
+
#
|
|
20
|
+
module Store
|
|
21
|
+
VALID_ORDERS = %w(earliest latest slowest fastest).freeze
|
|
22
|
+
extend ActiveSupport::Concern
|
|
23
|
+
|
|
24
|
+
module ClassMethods
|
|
25
|
+
def mount_tree(metrics)
|
|
26
|
+
while metric = metrics.shift
|
|
27
|
+
if parent = metrics.find { |n| n.parent_of?(metric) }
|
|
28
|
+
parent.children << metric
|
|
29
|
+
elsif metrics.empty?
|
|
30
|
+
return metric if metric.rack_request?
|
|
31
|
+
raise %(Expected tree root to be a "rack.request", got #{metric.name.inspect})
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def events_to_metrics_tree(events)
|
|
37
|
+
verify_active_connections! if respond_to?(:verify_active_connections!)
|
|
38
|
+
|
|
39
|
+
metrics = events.map do |event|
|
|
40
|
+
metric = new
|
|
41
|
+
metric.configure(event)
|
|
42
|
+
metric
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
mount_tree(metrics)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Configure the current metric by setting the values yielded by
|
|
50
|
+
# the instrumentation event.
|
|
51
|
+
def configure(args)
|
|
52
|
+
self.payload = RailsCustomerbeats::PayloadParser.filter(args[0].to_s, args[4])
|
|
53
|
+
self.name = args[0].to_s
|
|
54
|
+
self.started_at = args[1]
|
|
55
|
+
self.duration = normalized_duration(self.payload, args)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def duration_in_us
|
|
59
|
+
self.duration
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def duration_in_ms
|
|
63
|
+
self.duration * 0.001
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def exclusive_duration
|
|
67
|
+
@exclusive_duration ||= self.duration - children.sum(&:duration)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def exclusive_duration_in_us
|
|
71
|
+
self.exclusive_duration
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def exclusive_duration_in_ms
|
|
75
|
+
self.exclusive_duration * 0.001
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Stores the children of this metric when a tree is created.
|
|
79
|
+
def children
|
|
80
|
+
@children ||= []
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def rack_request?
|
|
84
|
+
self.name == "rack.request"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns if the current node is the parent of the given node.
|
|
88
|
+
# If this is a new record, we can use started_at values to detect parenting.
|
|
89
|
+
# However, if it was already saved, we lose microseconds information from
|
|
90
|
+
# timestamps and we must rely solely in id and parent_id information.
|
|
91
|
+
def parent_of?(node)
|
|
92
|
+
if !persisted?
|
|
93
|
+
start = (self.started_at - node.started_at) * 1000000
|
|
94
|
+
start <= 0 && (start + self.duration >= node.duration)
|
|
95
|
+
else
|
|
96
|
+
self.id == node.parent_id
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def child_of?(node)
|
|
101
|
+
node.parent_of?(self)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Save the current metric and all of its children by properly setting
|
|
105
|
+
# the request and parent ids.
|
|
106
|
+
def save_metrics!(request_id=nil, parent_id=nil)
|
|
107
|
+
self.request_id, self.parent_id = request_id, parent_id
|
|
108
|
+
save_metric!
|
|
109
|
+
|
|
110
|
+
children.each do |child|
|
|
111
|
+
child.save_metrics!(request_id || self.id, self.id)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
unless self.request_id
|
|
115
|
+
self.request_id ||= self.id
|
|
116
|
+
save_metric!
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Destroy all children if it's a request metric.
|
|
121
|
+
def destroy
|
|
122
|
+
self.class.by_request_id(self.id).delete_all if rack_request?
|
|
123
|
+
super
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
protected
|
|
127
|
+
|
|
128
|
+
def save_metric!
|
|
129
|
+
raise NotImplementedError
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def normalized_duration(payload, args)
|
|
133
|
+
payload[:duration] ? payload[:duration] : (args[2] - args[1]) * 1000000
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
|
2
|
+
require 'active_support/core_ext/class/attribute'
|
|
3
|
+
|
|
4
|
+
Thread.abort_on_exception = Rails.env.development? || Rails.env.test?
|
|
5
|
+
|
|
6
|
+
module RailsCustomerbeats
|
|
7
|
+
autoload :AsyncConsumer, 'rails_customerbeats/async_consumer'
|
|
8
|
+
autoload :Middleware, 'rails_customerbeats/middleware'
|
|
9
|
+
autoload :PayloadParser, 'rails_customerbeats/payload_parser'
|
|
10
|
+
autoload :Store, 'rails_customerbeats/store'
|
|
11
|
+
autoload :VERSION, 'rails_customerbeats/version'
|
|
12
|
+
autoload :VoidInstrumenter, 'rails_customerbeats/async_consumer'
|
|
13
|
+
|
|
14
|
+
module ORM
|
|
15
|
+
autoload :ActiveRecord, 'rails_customerbeats/orm/active_record'
|
|
16
|
+
autoload :DataMapper, 'rails_customerbeats/orm/data_mapper'
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
class_attribute :primary_key_finder
|
|
20
|
+
class_attribute :delete_all
|
|
21
|
+
class_attribute :metric_model_properties
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Set which store to use in RailsCustomerbeats.
|
|
26
|
+
#
|
|
27
|
+
# RailsCustomerbeats.set_store { Metric }
|
|
28
|
+
#
|
|
29
|
+
def self.set_store(&block)
|
|
30
|
+
singleton_class.send :define_method, :store, &block
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Place holder for the store.
|
|
34
|
+
def self.store; end
|
|
35
|
+
|
|
36
|
+
# Holds the events for a specific thread.
|
|
37
|
+
def self.events
|
|
38
|
+
Thread.current[:rails_customerbeats_events] ||= []
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Turn RailsCustomerbeats on, i.e. make it listen to notifications during the block.
|
|
42
|
+
# At the end, it pushes notifications to the async consumer.
|
|
43
|
+
def self.listen_request
|
|
44
|
+
events = RailsCustomerbeats.events
|
|
45
|
+
events.clear
|
|
46
|
+
|
|
47
|
+
Thread.current[:rails_customerbeats_listening] = true
|
|
48
|
+
result = yield
|
|
49
|
+
|
|
50
|
+
RailsCustomerbeats.async_consumer.push(events.dup)
|
|
51
|
+
result
|
|
52
|
+
ensure
|
|
53
|
+
Thread.current[:rails_customerbeats_listening] = false
|
|
54
|
+
RailsCustomerbeats.events.clear
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns if events are being registered or not.
|
|
58
|
+
def self.listening?
|
|
59
|
+
Thread.current[:rails_customerbeats_listening] || false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Allow you to specify a condition to ignore a notification based
|
|
63
|
+
# on its name and/or payload. For example, if you want to ignore
|
|
64
|
+
# all notifications with empty payload, one can do:
|
|
65
|
+
#
|
|
66
|
+
# RailsCustomerbeats.ignore :with_empty_payload do |name, payload|
|
|
67
|
+
# payload.empty?
|
|
68
|
+
# end
|
|
69
|
+
#
|
|
70
|
+
# However, if you want to ignore something based solely on its
|
|
71
|
+
# name, you can use ignore_patterns instead:
|
|
72
|
+
#
|
|
73
|
+
# RailsCustomerbeats.ignore_patterns << /^some_noise_plugin/
|
|
74
|
+
#
|
|
75
|
+
def self.ignore(name, &block)
|
|
76
|
+
raise ArgumentError, "ignore expects a block" unless block_given?
|
|
77
|
+
ignore_lambdas[name] = block
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Stores the blocks given to ignore with their respective identifier in a hash.
|
|
81
|
+
def self.ignore_lambdas
|
|
82
|
+
@@ignore_lambdas ||= {}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Stores ignore patterns that can be given as strings or regexps.
|
|
86
|
+
def self.ignore_patterns
|
|
87
|
+
@@ignore_patterns ||= []
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Holds the queue which store stuff in the database.
|
|
91
|
+
def self.async_consumer
|
|
92
|
+
@@async_consumer ||= AsyncConsumer.new do |events|
|
|
93
|
+
next if events.empty?
|
|
94
|
+
root = RailsCustomerbeats.store.events_to_metrics_tree(events)
|
|
95
|
+
root.save_metrics!
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Wait until the async queue is consumed.
|
|
100
|
+
def self.wait
|
|
101
|
+
sleep(0.01) until async_consumer.finished?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# A notification is valid for storing if two conditions are met:
|
|
105
|
+
#
|
|
106
|
+
# 1) The instrumenter id which created the notification is not the same
|
|
107
|
+
# instrumenter id of this thread. This means that notifications generated
|
|
108
|
+
# inside this thread are stored in the database;
|
|
109
|
+
#
|
|
110
|
+
# 2) If the notification name does not match any ignored pattern;
|
|
111
|
+
#
|
|
112
|
+
def self.valid_for_storing?(args) #:nodoc:
|
|
113
|
+
name, payload = args[0].to_s, args[4]
|
|
114
|
+
|
|
115
|
+
RailsCustomerbeats.listening? && RailsCustomerbeats.store &&
|
|
116
|
+
!self.ignore_patterns.find { |p| String === p ? name == p : name =~ p } &&
|
|
117
|
+
!self.ignore_lambdas.values.any? { |b| b.call(name, payload) }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
require 'rails_customerbeats/engine'
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/* g.Raphael 0.4 - Charting library, based on Raphaël
|
|
2
|
+
* Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
* This code was modified by José Valim to not include "Others" neither sort values.
|
|
5
|
+
*/
|
|
6
|
+
Raphael.fn.g.piechart=function(d,c,n,b,k){k=k||{};var j=this,l=[],f=this.set(),m=this.set(),h=this.set(),t=[],v=b.length,w=0,x=0;m.covers=f;if(v==1){h.push(this.circle(d,c,n).attr({fill:this.g.colors[0],stroke:opt.stroke||"#fff","stroke-width":k.strokewidth==null?1:k.strokewidth}));f.push(this.circle(d,c,n).attr(this.g.shim));x=b[0];b[0]={value:b[0],order:0,valueOf:function(){return this.value}};h[0].middle={x:d,y:c};h[0].mangle=180}else{function s(C,B,i,E,A,J){var G=Math.PI/180,y=C+i*Math.cos(-E*G),p=C+i*Math.cos(-A*G),D=C+i/2*Math.cos(-(E+(A-E)/2)*G),I=B+i*Math.sin(-E*G),H=B+i*Math.sin(-A*G),z=B+i/2*Math.sin(-(E+(A-E)/2)*G),F=["M",C,B,"L",y,I,"A",i,i,0,+(Math.abs(A-E)>180),1,p,H,"z"];F.middle={x:D,y:z};return F}for(var u=0;u<v;u++){x+=b[u];b[u]={value:b[u],order:u,valueOf:function(){return this.value}}}for(var u=0;u<v;u++){var e=w-360*b[u]/x/2;if(!u){w=90-e;e=w-360*b[u]/x/2}if(k.init){var g=s(d,c,1,w,w-360*b[u]/x).join(",")}var q=s(d,c,n,w,w-=360*b[u]/x);var o=this.path(k.init?g:q).attr({fill:k.colors&&k.colors[u]||this.g.colors[u]||"#666",stroke:k.stroke||"#fff","stroke-width":(k.strokewidth==null?1:k.strokewidth),"stroke-linejoin":"round"});o.value=b[u];o.middle=q.middle;o.mangle=e;l.push(o);h.push(o);k.init&&o.animate({path:q.join(",")},(+k.init-1)||1000,">")}for(var u=0;u<v;u++){var o=j.path(l[u].attr("path")).attr(this.g.shim);k.href&&k.href[u]&&o.attr({href:k.href[u]});o.attr=function(){};f.push(o);h.push(o)}}m.hover=function(z,r){r=r||function(){};var y=this;for(var p=0;p<v;p++){(function(A,B,i){var C={sector:A,cover:B,cx:d,cy:c,mx:A.middle.x,my:A.middle.y,mangle:A.mangle,r:n,value:b[i],total:x,label:y.labels&&y.labels[i]};B.mouseover(function(){z.call(C)}).mouseout(function(){r.call(C)})})(h[p],f[p],p)}return this};m.each=function(y){var r=this;for(var p=0;p<v;p++){(function(z,A,i){var B={sector:z,cover:A,cx:d,cy:c,x:z.middle.x,y:z.middle.y,mangle:z.mangle,r:n,value:b[i],total:x,label:r.labels&&r.labels[i]};y.call(B)})(h[p],f[p],p)}return this};m.click=function(y){var r=this;for(var p=0;p<v;p++){(function(z,A,i){var B={sector:z,cover:A,cx:d,cy:c,mx:z.middle.x,my:z.middle.y,mangle:z.mangle,r:n,value:b[i],total:x,label:r.labels&&r.labels[i]};A.click(function(){y.call(B)})})(h[p],f[p],p)}return this};m.inject=function(i){i.insertBefore(f[0])};var a=function(E,z,r,p){var I=d+n+n/5,H=c,D=H+10;E=E||[];p=(p&&p.toLowerCase&&p.toLowerCase())||"east";r=j.g.markers[r&&r.toLowerCase()]||"disc";m.labels=j.set();for(var C=0;C<v;C++){var J=h[C].attr("fill"),A=b[C].order,B;E[A]=j.g.labelise(E[A],b[C],x);m.labels.push(j.set());m.labels[C].push(j.g[r](I+5,D,5).attr({fill:J,stroke:"none"}));m.labels[C].push(B=j.text(I+20,D,E[A]||b[A]).attr(j.g.txtattr).attr({fill:k.legendcolor||"#000","text-anchor":"start"}));f[C].label=m.labels[C];D+=B.getBBox().height*1.2}var F=m.labels.getBBox(),G={east:[0,-F.height/2],west:[-F.width-2*n-20,-F.height/2],north:[-n-F.width/2,-n-F.height-10],south:[-n-F.width/2,n+10]}[p];m.labels.translate.apply(m.labels,G);m.push(m.labels)};if(k.legend){a(k.legend,k.legendothers,k.legendmark,k.legendpos)}m.push(h,f);m.series=h;m.covers=f;return m};
|