scout_rails_proxy_proxy 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.DS_Store +0 -0
- data/.gitignore +5 -0
- data/CHANGELOG.markdown +50 -0
- data/Gemfile +4 -0
- data/README.markdown +44 -0
- data/Rakefile +1 -0
- data/lib/scout_rails_proxy.rb +32 -0
- data/lib/scout_rails_proxy/agent.rb +319 -0
- data/lib/scout_rails_proxy/config.rb +34 -0
- data/lib/scout_rails_proxy/environment.rb +122 -0
- data/lib/scout_rails_proxy/instruments/active_record_instruments.rb +83 -0
- data/lib/scout_rails_proxy/instruments/net_http.rb +14 -0
- data/lib/scout_rails_proxy/instruments/process/process_cpu.rb +27 -0
- data/lib/scout_rails_proxy/instruments/process/process_memory.rb +40 -0
- data/lib/scout_rails_proxy/instruments/rails/action_controller_instruments.rb +46 -0
- data/lib/scout_rails_proxy/instruments/rails3/action_controller_instruments.rb +38 -0
- data/lib/scout_rails_proxy/instruments/sinatra_instruments.rb +33 -0
- data/lib/scout_rails_proxy/layaway.rb +76 -0
- data/lib/scout_rails_proxy/layaway_file.rb +70 -0
- data/lib/scout_rails_proxy/metric_meta.rb +34 -0
- data/lib/scout_rails_proxy/metric_stats.rb +49 -0
- data/lib/scout_rails_proxy/stack_item.rb +18 -0
- data/lib/scout_rails_proxy/store.rb +159 -0
- data/lib/scout_rails_proxy/tracer.rb +105 -0
- data/lib/scout_rails_proxy/transaction_sample.rb +10 -0
- data/lib/scout_rails_proxy/version.rb +3 -0
- data/scout_rails_proxy.gemspec +24 -0
- metadata +75 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
module ScoutRailsProxy
|
2
|
+
class Config
|
3
|
+
def initialize(config_path = nil)
|
4
|
+
@config_path = config_path
|
5
|
+
end
|
6
|
+
|
7
|
+
def settings
|
8
|
+
return @settings if @settings
|
9
|
+
load_file
|
10
|
+
end
|
11
|
+
|
12
|
+
def config_path
|
13
|
+
@config_path || File.join(ScoutRailsProxy::Agent.instance.environment.root,"config","scout_rails_proxy.yml")
|
14
|
+
end
|
15
|
+
|
16
|
+
def config_file
|
17
|
+
File.expand_path(config_path)
|
18
|
+
end
|
19
|
+
|
20
|
+
def load_file
|
21
|
+
if !File.exist?(config_file)
|
22
|
+
ScoutRailsProxy::Agent.instance.logger.warn "No config file found at [#{config_file}]."
|
23
|
+
@settings = {}
|
24
|
+
else
|
25
|
+
@settings = YAML.load(ERB.new(File.read(config_file)).result(binding))[ScoutRailsProxy::Agent.instance.environment.env] || {}
|
26
|
+
end
|
27
|
+
rescue Exception => e
|
28
|
+
ScoutRailsProxy::Agent.instance.logger.warn "Unable to load the config file."
|
29
|
+
ScoutRailsProxy::Agent.instance.logger.warn e.message
|
30
|
+
ScoutRailsProxy::Agent.instance.logger.warn e.backtrace
|
31
|
+
@settings = {}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# Used to retrieve environment information for this application.
|
2
|
+
module ScoutRailsProxy
|
3
|
+
class Environment
|
4
|
+
def env
|
5
|
+
@env ||= case framework
|
6
|
+
when :rails then RAILS_ENV.dup
|
7
|
+
when :rails3 then Rails.env
|
8
|
+
when :sinatra
|
9
|
+
ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def framework
|
14
|
+
@framework ||= case
|
15
|
+
when defined?(::Rails) && defined?(ActionController)
|
16
|
+
if Rails::VERSION::MAJOR < 3
|
17
|
+
:rails
|
18
|
+
else
|
19
|
+
:rails3
|
20
|
+
end
|
21
|
+
when defined?(::Sinatra) && defined?(::Sinatra::Base) then :sinatra
|
22
|
+
else :ruby
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def processors
|
27
|
+
return @processors if @processors
|
28
|
+
unless @processors
|
29
|
+
proc_file = '/proc/cpuinfo'
|
30
|
+
if !File.exist?(proc_file)
|
31
|
+
@processors = 1
|
32
|
+
elsif `cat #{proc_file} | grep 'model name' | wc -l` =~ /(\d+)/
|
33
|
+
@processors = $1.to_i
|
34
|
+
end
|
35
|
+
if @processors < 1
|
36
|
+
@processors = 1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
@processors
|
40
|
+
end
|
41
|
+
|
42
|
+
def root
|
43
|
+
if framework == :rails
|
44
|
+
RAILS_ROOT.to_s
|
45
|
+
elsif framework == :rails3
|
46
|
+
Rails.root
|
47
|
+
elsif framework == :sinatra
|
48
|
+
Sinatra::Application.root
|
49
|
+
else
|
50
|
+
'.'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def app_server
|
55
|
+
@app_server ||= if thin? then :thin
|
56
|
+
elsif passenger? then :passenger
|
57
|
+
elsif webrick? then :webrick
|
58
|
+
elsif unicorn? then :unicorn
|
59
|
+
else nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
### app server related-checks
|
64
|
+
|
65
|
+
def thin?
|
66
|
+
if defined?(::Thin) && defined?(::Thin::Server)
|
67
|
+
# Ensure Thin is actually initialized. It could just be required and not running.
|
68
|
+
ObjectSpace.each_object(Thin::Server) { |x| return true }
|
69
|
+
false
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def unicorn?
|
74
|
+
if defined?(::Unicorn) && defined?(::Unicorn::HttpServer)
|
75
|
+
# Ensure Unicorn is actually initialized. It could just be required and not running.
|
76
|
+
ObjectSpace.each_object(::Unicorn::HttpServer) { |x| return true }
|
77
|
+
false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Called via +#forking?+ since Passenger forks. Adds an event listener to start the worker thread
|
82
|
+
# inside the passenger worker process.
|
83
|
+
# Background: http://www.modrails.com/documentation/Users%20guide%20Nginx.html#spawning%5Fmethods%5Fexplained
|
84
|
+
def passenger?
|
85
|
+
(defined?(::Passenger) && defined?(::Passenger::AbstractServer)) || defined?(::IN_PHUSION_PASSENGER)
|
86
|
+
end
|
87
|
+
|
88
|
+
def webrick?
|
89
|
+
defined?(::WEBrick) && defined?(::WEBrick::VERSION)
|
90
|
+
end
|
91
|
+
|
92
|
+
def unicorn?
|
93
|
+
if defined?(::Unicorn) && defined?(::Unicorn::HttpServer)
|
94
|
+
# ensure Unicorn is actually initialized.
|
95
|
+
ObjectSpace.each_object(::Unicorn::HttpServer) { |x| return true }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# If forking, don't start worker thread in the master process. Since it's started as a Thread, it won't survive
|
100
|
+
# the fork.
|
101
|
+
def forking?
|
102
|
+
passenger? or unicorn?
|
103
|
+
end
|
104
|
+
|
105
|
+
### ruby checks
|
106
|
+
|
107
|
+
def rubinius?
|
108
|
+
RUBY_VERSION =~ /rubinius/i
|
109
|
+
end
|
110
|
+
|
111
|
+
def jruby?
|
112
|
+
defined?(JRuby)
|
113
|
+
end
|
114
|
+
|
115
|
+
### framework checks
|
116
|
+
|
117
|
+
def sinatra?
|
118
|
+
defined?(Sinatra::Application)
|
119
|
+
end
|
120
|
+
|
121
|
+
end # class Environemnt
|
122
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module ScoutRailsProxy::Instruments
|
2
|
+
# Contains ActiveRecord instrument, aliasing +ActiveRecord::ConnectionAdapters::AbstractAdapter#log+ calls
|
3
|
+
# to trace calls to the database.
|
4
|
+
module ActiveRecordInstruments
|
5
|
+
def self.included(instrumented_class)
|
6
|
+
ScoutRailsProxy::Agent.instance.logger.debug "Instrumenting #{instrumented_class.inspect}"
|
7
|
+
instrumented_class.class_eval do
|
8
|
+
unless instrumented_class.method_defined?(:log_without_scout_instruments)
|
9
|
+
alias_method :log_without_scout_instruments, :log
|
10
|
+
alias_method :log, :log_with_scout_instruments
|
11
|
+
protected :log
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end # self.included
|
15
|
+
|
16
|
+
def log_with_scout_instruments(*args, &block)
|
17
|
+
sql, name = args
|
18
|
+
self.class.instrument(scout_ar_metric_name(sql,name), :desc => scout_sanitize_sql(sql)) do
|
19
|
+
log_without_scout_instruments(sql, name, &block)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def scout_ar_metric_name(sql,name)
|
24
|
+
if name && (parts = name.split " ") && parts.size == 2
|
25
|
+
model = parts.first
|
26
|
+
operation = parts.last.downcase
|
27
|
+
metric_name = case operation
|
28
|
+
when 'load' then 'find'
|
29
|
+
when 'indexes', 'columns' then nil # not under developer control
|
30
|
+
when 'destroy', 'find', 'save', 'create' then operation
|
31
|
+
when 'update' then 'save'
|
32
|
+
else
|
33
|
+
if model == 'Join'
|
34
|
+
operation
|
35
|
+
end
|
36
|
+
end
|
37
|
+
metric = "ActiveRecord/#{model}/#{metric_name}" if metric_name
|
38
|
+
metric = "ActiveRecord/SQL/other" if metric.nil?
|
39
|
+
else
|
40
|
+
metric = "ActiveRecord/SQL/Unknown"
|
41
|
+
end
|
42
|
+
metric
|
43
|
+
end
|
44
|
+
|
45
|
+
# Removes actual values from SQL. Used to both obfuscate the SQL and group
|
46
|
+
# similar queries in the UI.
|
47
|
+
def scout_sanitize_sql(sql)
|
48
|
+
return nil if sql.length > 1000 # safeguard - don't sanitize large SQL statements
|
49
|
+
sql = sql.dup
|
50
|
+
sql.gsub!(/\\"/, '') # removing escaping double quotes
|
51
|
+
sql.gsub!(/\\'/, '') # removing escaping single quotes
|
52
|
+
sql.gsub!(/'(?:[^']|'')*'/, '?') # removing strings (single quote)
|
53
|
+
sql.gsub!(/"(?:[^"]|"")*"/, '?') # removing strings (double quote)
|
54
|
+
sql.gsub!(/\b\d+\b/, '?') # removing integers
|
55
|
+
sql.gsub!(/\?(,\?)+/,'?') # replace multiple ? w/a single ?
|
56
|
+
sql
|
57
|
+
end
|
58
|
+
|
59
|
+
end # module ActiveRecordInstruments
|
60
|
+
end # module Instruments
|
61
|
+
|
62
|
+
def add_instruments
|
63
|
+
if defined?(ActiveRecord) && defined?(ActiveRecord::Base)
|
64
|
+
ActiveRecord::ConnectionAdapters::AbstractAdapter.module_eval do
|
65
|
+
include ::ScoutRailsProxy::Instruments::ActiveRecordInstruments
|
66
|
+
include ::ScoutRailsProxy::Tracer
|
67
|
+
end
|
68
|
+
ActiveRecord::Base.class_eval do
|
69
|
+
include ::ScoutRailsProxy::Tracer
|
70
|
+
end
|
71
|
+
end
|
72
|
+
rescue
|
73
|
+
ScoutRailsProxy::Agent.instance.logger.warn "ActiveRecord instrumentation exception: #{$!.message}"
|
74
|
+
end
|
75
|
+
|
76
|
+
if defined?(::Rails) && ::Rails::VERSION::MAJOR.to_i == 3
|
77
|
+
Rails.configuration.after_initialize do
|
78
|
+
ScoutRailsProxy::Agent.instance.logger.debug "Adding ActiveRecord instrumentation to a Rails 3 app"
|
79
|
+
add_instruments
|
80
|
+
end
|
81
|
+
else
|
82
|
+
add_instruments
|
83
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
if defined?(::Net) && defined?(Net::HTTP)
|
2
|
+
ScoutRailsProxy::Agent.instance.logger.debug "Instrumenting Net::HTTP"
|
3
|
+
Net::HTTP.class_eval do
|
4
|
+
include ScoutRailsProxy::Tracer
|
5
|
+
|
6
|
+
def request_with_scout_instruments(*args,&block)
|
7
|
+
self.class.instrument("HTTP/request", :desc => "#{(@address+args.first.path.split('?').first)[0..99]}") do
|
8
|
+
request_without_scout_instruments(*args,&block)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
alias request_without_scout_instruments request
|
12
|
+
alias request request_with_scout_instruments
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ScoutRailsProxy::Instruments
|
2
|
+
module Process
|
3
|
+
class ProcessCpu
|
4
|
+
def initialize(num_processors)
|
5
|
+
@num_processors = num_processors || 1
|
6
|
+
end
|
7
|
+
|
8
|
+
def run
|
9
|
+
res=nil
|
10
|
+
now = Time.now
|
11
|
+
t = ::Process.times
|
12
|
+
if @last_run
|
13
|
+
elapsed_time = now - @last_run
|
14
|
+
if elapsed_time >= 1
|
15
|
+
user_time_since_last_sample = t.utime - @last_utime
|
16
|
+
system_time_since_last_sample = t.stime - @last_stime
|
17
|
+
res = ((user_time_since_last_sample + system_time_since_last_sample)/(elapsed_time * @num_processors))*100
|
18
|
+
end
|
19
|
+
end
|
20
|
+
@last_utime = t.utime
|
21
|
+
@last_stime = t.stime
|
22
|
+
@last_run = now
|
23
|
+
return res
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ScoutRailsProxy::Instruments
|
2
|
+
module Process
|
3
|
+
class ProcessMemory
|
4
|
+
def run
|
5
|
+
res=nil
|
6
|
+
platform = RUBY_PLATFORM.downcase
|
7
|
+
|
8
|
+
if platform =~ /linux/
|
9
|
+
res = get_mem_from_procfile
|
10
|
+
elsif platform =~ /darwin9/ # 10.5
|
11
|
+
res = get_mem_from_shell("ps -o rsz")
|
12
|
+
elsif platform =~ /darwin1[01]/ # 10.6 & 10.7
|
13
|
+
res = get_mem_from_shell("ps -o rss")
|
14
|
+
end
|
15
|
+
return res
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def get_mem_from_procfile
|
21
|
+
res = nil
|
22
|
+
proc_status = File.open(procfile, "r") { |f| f.read_nonblock(4096).strip }
|
23
|
+
if proc_status =~ /RSS:\s*(\d+) kB/i
|
24
|
+
res= $1.to_f / 1024.0
|
25
|
+
end
|
26
|
+
res
|
27
|
+
end
|
28
|
+
|
29
|
+
def procfile
|
30
|
+
"/proc/#{$$}/status"
|
31
|
+
end
|
32
|
+
|
33
|
+
# memory in MB the current process is using
|
34
|
+
def get_mem_from_shell(command)
|
35
|
+
res = `#{command} #{$$}`.split("\n")[1].to_f / 1024.0 #rescue nil
|
36
|
+
res
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module ScoutRailsProxy::Instruments
|
2
|
+
module ActionControllerInstruments
|
3
|
+
def self.included(instrumented_class)
|
4
|
+
ScoutRailsProxy::Agent.instance.logger.debug "Instrumenting #{instrumented_class.inspect}"
|
5
|
+
instrumented_class.class_eval do
|
6
|
+
unless instrumented_class.method_defined?(:perform_action_without_scout_instruments)
|
7
|
+
alias_method :perform_action_without_scout_instruments, :perform_action
|
8
|
+
alias_method :perform_action, :perform_action_with_scout_instruments
|
9
|
+
private :perform_action
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end # self.included
|
13
|
+
|
14
|
+
# In addition to instrumenting actions, this also sets the scope to the controller action name. The scope is later
|
15
|
+
# applied to metrics recorded during this transaction. This lets us associate ActiveRecord calls with
|
16
|
+
# specific controller actions.
|
17
|
+
def perform_action_with_scout_instruments(*args, &block)
|
18
|
+
scout_controller_action = "Controller/#{controller_path}/#{action_name}"
|
19
|
+
self.class.trace(scout_controller_action, :uri => request.request_uri) do
|
20
|
+
perform_action_without_scout_instruments(*args, &block)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
if defined?(ActionController) && defined?(ActionController::Base)
|
27
|
+
ActionController::Base.class_eval do
|
28
|
+
include ScoutRailsProxy::Tracer
|
29
|
+
include ::ScoutRailsProxy::Instruments::ActionControllerInstruments
|
30
|
+
|
31
|
+
def rescue_action_with_scout(exception)
|
32
|
+
ScoutRailsProxy::Agent.instance.store.track!("Errors/Request",1, :scope => nil)
|
33
|
+
ScoutRailsProxy::Agent.instance.store.ignore_transaction!
|
34
|
+
rescue_action_without_scout exception
|
35
|
+
end
|
36
|
+
|
37
|
+
alias_method :rescue_action_without_scout, :rescue_action
|
38
|
+
alias_method :rescue_action, :rescue_action_with_scout
|
39
|
+
protected :rescue_action
|
40
|
+
end
|
41
|
+
ScoutRailsProxy::Agent.instance.logger.debug "Instrumenting ActionView::Template"
|
42
|
+
ActionView::Template.class_eval do
|
43
|
+
include ::ScoutRailsProxy::Tracer
|
44
|
+
instrument_method :render, :metric_name => 'View/#{path[%r{^(/.*/)?(.*)$},2]}/Rendering', :scope => true
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# Rails 3
|
2
|
+
module ScoutRailsProxy::Instruments
|
3
|
+
module ActionControllerInstruments
|
4
|
+
# Instruments the action and tracks errors.
|
5
|
+
def process_action(*args)
|
6
|
+
scout_controller_action = "Controller/#{controller_path}/#{action_name}"
|
7
|
+
#ScoutRailsProxy::Agent.instance.logger.debug "Processing #{scout_controller_action}"
|
8
|
+
self.class.trace(scout_controller_action, :uri => request.fullpath) do
|
9
|
+
begin
|
10
|
+
super
|
11
|
+
rescue Exception => e
|
12
|
+
ScoutRailsProxy::Agent.instance.store.track!("Errors/Request",1, :scope => nil)
|
13
|
+
raise
|
14
|
+
ensure
|
15
|
+
Thread::current[:scout_scope_name] = nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# ActionController::Base is a subclass of ActionController::Metal, so this instruments both
|
23
|
+
# standard Rails requests + Metal.
|
24
|
+
if defined?(ActionController) && defined?(ActionController::Metal)
|
25
|
+
ScoutRailsProxy::Agent.instance.logger.debug "Instrumenting ActionController::Metal"
|
26
|
+
ActionController::Metal.class_eval do
|
27
|
+
include ScoutRailsProxy::Tracer
|
28
|
+
include ::ScoutRailsProxy::Instruments::ActionControllerInstruments
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
if defined?(ActionView) && defined?(ActionView::PartialRenderer)
|
33
|
+
ScoutRailsProxy::Agent.instance.logger.debug "Instrumenting ActionView::PartialRenderer"
|
34
|
+
ActionView::PartialRenderer.class_eval do
|
35
|
+
include ScoutRailsProxy::Tracer
|
36
|
+
instrument_method :render_partial, :metric_name => 'View/#{@template.virtual_path}/Rendering', :scope => true
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ScoutRailsProxy::Instruments
|
2
|
+
module SinatraInstruments
|
3
|
+
def route_eval_with_scout_instruments(&blockarg)
|
4
|
+
path = unescape(@request.path_info)
|
5
|
+
name = path
|
6
|
+
# Go through each route and look for a match
|
7
|
+
if routes = self.class.routes[@request.request_method]
|
8
|
+
routes.detect do |pattern, keys, conditions, block|
|
9
|
+
if blockarg.equal? block
|
10
|
+
name = pattern.source
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
name.gsub!(%r{^[/^]*(.*?)[/\$\?]*$}, '\1')
|
15
|
+
name = 'root' if name.empty?
|
16
|
+
name = @request.request_method + ' ' + name if @request && @request.respond_to?(:request_method)
|
17
|
+
scout_controller_action = "Controller/Sinatra/#{name}"
|
18
|
+
self.class.trace(scout_controller_action, :uri => @request.path_info) do
|
19
|
+
route_eval_without_scout_instruments(&blockarg)
|
20
|
+
end
|
21
|
+
end # route_eval_with_scout_instrumentss
|
22
|
+
end # SinatraInstruments
|
23
|
+
end # ScoutRailsProxy::Instruments
|
24
|
+
|
25
|
+
if defined?(::Sinatra) && defined?(::Sinatra::Base)
|
26
|
+
ScoutRailsProxy::Agent.instance.logger.debug "Instrumenting Sinatra"
|
27
|
+
::Sinatra::Base.class_eval do
|
28
|
+
include ScoutRailsProxy::Tracer
|
29
|
+
include ::ScoutRailsProxy::Instruments::SinatraInstruments
|
30
|
+
alias route_eval_without_scout_instruments route_eval
|
31
|
+
alias route_eval route_eval_with_scout_instruments
|
32
|
+
end
|
33
|
+
end
|