scout_rails 0.0.1

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.
@@ -0,0 +1,88 @@
1
+ module ScoutRails::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
+ ScoutRails::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)) do
19
+ log_without_scout_instruments(sql, name, &block)
20
+ end
21
+ end
22
+
23
+ # Searches for the first AR model in the call stack. If found, adds it to a Hash of
24
+ # classes and methods to later instrument. Used to provide a better breakdown.
25
+ def scout_instrument_caller(called)
26
+ model_call = called.find { |call| call =~ /\/app\/models\/(.+)\.rb:\d+:in `(.+)'/ }
27
+ if model_call and !model_call.include?("without_scout_instrument")
28
+ set=ScoutRails::Agent.instance.dynamic_instruments[$1.camelcase] || Set.new
29
+ ScoutRails::Agent.instance.dynamic_instruments[$1.camelcase] = (set << $2)
30
+ end
31
+ end
32
+
33
+ # Only instrument the caller if dynamic_instruments isn't disabled. By default, it is enabled.
34
+ def scout_dynamic?
35
+ dynamic=ScoutRails::Agent.instance.config.settings['dynamic_instruments']
36
+ dynamic.nil? or dynamic
37
+ end
38
+
39
+ def scout_ar_metric_name(sql,name)
40
+ if name && (parts = name.split " ") && parts.size == 2
41
+ model = parts.first
42
+ # samples 10% of calls
43
+ if scout_dynamic? and rand*10 < 1
44
+ scout_instrument_caller(caller(10)[0..9]) # for performance, limits the number of call stack items to examine
45
+ end
46
+ operation = parts.last.downcase
47
+ metric_name = case operation
48
+ when 'load' then 'find'
49
+ when 'indexes', 'columns' then nil # not under developer control
50
+ when 'destroy', 'find', 'save', 'create' then operation
51
+ when 'update' then 'save'
52
+ else
53
+ if model == 'Join'
54
+ operation
55
+ end
56
+ end
57
+ metric = "ActiveRecord/#{model}/#{metric_name}" if metric_name
58
+ metric = "Database/SQL/other" if metric.nil?
59
+ else
60
+ metric = "Database/SQL/Unknown"
61
+ end
62
+ metric
63
+ end
64
+
65
+ end # module ActiveRecordInstruments
66
+ end # module Instruments
67
+
68
+ def add_instruments
69
+ if defined?(ActiveRecord) && defined?(ActiveRecord::Base)
70
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.module_eval do
71
+ include ::ScoutRails::Instruments::ActiveRecordInstruments
72
+ include ::ScoutRails::Tracer
73
+ end
74
+ ActiveRecord::Base.class_eval do
75
+ include ::ScoutRails::Tracer
76
+ end
77
+ ScoutRails::Agent.instance.logger.debug "Dynamic instrumention is #{ActiveRecord::Base.connection.scout_dynamic? ? 'enabled' : 'disabled'}"
78
+ end
79
+ end
80
+
81
+ if defined?(::Rails) && ::Rails::VERSION::MAJOR.to_i == 3
82
+ Rails.configuration.after_initialize do
83
+ ScoutRails::Agent.instance.logger.debug "Adding ActiveRecord instrumentation to a Rails 3 app"
84
+ add_instruments
85
+ end
86
+ else
87
+ add_instruments
88
+ end
@@ -0,0 +1,27 @@
1
+ module ScoutRails::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 ScoutRails::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,47 @@
1
+ module ScoutRails::Instruments
2
+ module ActionControllerInstruments
3
+ def self.included(instrumented_class)
4
+ ScoutRails::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.instrument(scout_controller_action) do
20
+ Thread::current[:scout_scope_name] = scout_controller_action
21
+ perform_action_without_scout_instruments(*args, &block)
22
+ Thread::current[:scout_scope_name] = nil
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ if defined?(ActionController) && defined?(ActionController::Base)
29
+ ActionController::Base.class_eval do
30
+ include ScoutRails::Tracer
31
+ include ::ScoutRails::Instruments::ActionControllerInstruments
32
+
33
+ def rescue_action_with_scout(exception)
34
+ ScoutRails::Agent.instance.store.track!("Errors/Request",1, :scope => nil)
35
+ rescue_action_without_scout exception
36
+ end
37
+
38
+ alias_method :rescue_action_without_scout, :rescue_action
39
+ alias_method :rescue_action, :rescue_action_with_scout
40
+ protected :rescue_action
41
+ end
42
+ ScoutRails::Agent.instance.logger.debug "Instrumenting ActionView::Template"
43
+ ActionView::Template.class_eval do
44
+ include ::ScoutRails::Tracer
45
+ instrument_method :render, 'View/#{path[%r{^(/.*/)?(.*)$},2]}/Rendering'
46
+ end
47
+ end
@@ -0,0 +1,35 @@
1
+ module ScoutRails::Instruments
2
+ module ActionControllerInstruments
3
+ # Instruments the action and tracks errors.
4
+ def process_action(*args)
5
+ scout_controller_action = "Controller/#{controller_path}/#{action_name}"
6
+ self.class.instrument(scout_controller_action) do
7
+ Thread::current[:scout_scope_name] = scout_controller_action
8
+ begin
9
+ super
10
+ rescue Exception => e
11
+ ScoutRails::Agent.instance.store.track!("Errors/Request",1, :scope => nil)
12
+ raise
13
+ ensure
14
+ Thread::current[:scout_scope_name] = nil
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ if defined?(ActionController) && defined?(ActionController::Base)
22
+ ScoutRails::Agent.instance.logger.debug "Instrumenting ActionController::Base"
23
+ ActionController::Base.class_eval do
24
+ include ScoutRails::Tracer
25
+ include ::ScoutRails::Instruments::ActionControllerInstruments
26
+ end
27
+ end
28
+
29
+ if defined?(ActionView) && defined?(ActionView::PartialRenderer)
30
+ ScoutRails::Agent.instance.logger.debug "Instrumenting ActionView::PartialRenderer"
31
+ ActionView::PartialRenderer.class_eval do
32
+ include ScoutRails::Tracer
33
+ instrument_method :render_partial, 'View/#{@template.virtual_path}/Rendering'
34
+ end
35
+ end
@@ -0,0 +1,34 @@
1
+ module ScoutRails::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.instrument(scout_controller_action) do
19
+ Thread::current[:scout_scope_name] = scout_controller_action
20
+ route_eval_without_scout_instruments(&blockarg)
21
+ end
22
+ end # route_eval_with_scout_instrumentss
23
+ end # SinatraInstruments
24
+ end # ScoutRails::Instruments
25
+
26
+ if defined?(::Sinatra) && defined?(::Sinatra::Base)
27
+ ScoutRails::Agent.instance.logger.debug "Instrumenting Sinatra"
28
+ ::Sinatra::Base.class_eval do
29
+ include ScoutRails::Tracer
30
+ include ::ScoutRails::Instruments::SinatraInstruments
31
+ alias route_eval_without_scout_instruments route_eval
32
+ alias route_eval route_eval_with_scout_instruments
33
+ end
34
+ end
@@ -0,0 +1,77 @@
1
+ # Stores metrics in a file before sending them to the server. Two uses:
2
+ # 1. A centralized store for multiple Agent processes. This way, only 1 checkin is sent to Scout rather than 1 per-process.
3
+ # 2. Bundling up reports from multiple timeslices to make updates more efficent server-side.
4
+ #
5
+ # Metrics are stored in a Hash, where the keys are Time.to_i on the minute. When depositing data,
6
+ # metrics are either merged with an existing time or placed in a new key.
7
+ class ScoutRails::Layaway
8
+ attr_accessor :file
9
+ def initialize
10
+ @file = ScoutRails::LayawayFile.new
11
+ end
12
+
13
+ def deposit_and_deliver
14
+ new_data = ScoutRails::Agent.instance.store.metric_hash
15
+ controller_count = 0
16
+ new_data.each do |meta,stats|
17
+ if meta.metric_name =~ /\AController/
18
+ controller_count += stats.call_count
19
+ end
20
+ end
21
+ ScoutRails::Agent.instance.logger.debug "Depositing #{controller_count} requests into #{Time.at(slot).strftime("%m/%d/%y %H:%M:%S %z")} slot."
22
+
23
+ to_deliver = {}
24
+ file.read_and_write do |old_data|
25
+ old_data ||= Hash.new
26
+ # merge data
27
+ # if the previous minute has ended, its time to send those metrics
28
+ if old_data.any? and old_data[slot].nil?
29
+ to_deliver = old_data
30
+ old_data = Hash.new
31
+ elsif old_data.any?
32
+ ScoutRails::Agent.instance.logger.debug "Not yet time to deliver metrics for slot [#{Time.at(old_data.keys.sort.last).strftime("%m/%d/%y %H:%M:%S %z")}]"
33
+ else
34
+ ScoutRails::Agent.instance.logger.debug "There is no data in the layaway file to deliver."
35
+ end
36
+ old_data[slot]=ScoutRails::Agent.instance.store.merge_data_and_clear(old_data[slot] || Hash.new)
37
+ ScoutRails::Agent.instance.logger.debug "Saving the following #{old_data.size} time slots locally:"
38
+ old_data.each do |k,v|
39
+ controller_count = 0
40
+ new_data.each do |meta,stats|
41
+ if meta.metric_name =~ /\AController/
42
+ controller_count += stats.call_count
43
+ end
44
+ end
45
+ ScoutRails::Agent.instance.logger.debug "#{Time.at(k).strftime("%m/%d/%y %H:%M:%S %z")} => #{controller_count} requests"
46
+ end
47
+ old_data
48
+ end
49
+ to_deliver.any? ? validate_data(to_deliver) : {}
50
+ end
51
+
52
+ # Ensures the data we're sending to the server isn't stale.
53
+ # This can occur if the agent is collecting data, and app server goes down w/data in the local storage.
54
+ # When it is restarted later data will remain in local storage but it won't be for the current reporting interval.
55
+ #
56
+ # If the data is stale, an empty Hash is returned. Otherwise, the data from the most recent slot is returned.
57
+ def validate_data(data)
58
+ data = data.to_a.sort
59
+ now = Time.now
60
+ if (most_recent = data.first.first) < now.to_i - 2*60
61
+ ScoutRails::Agent.instance.logger.debug "Local Storage is stale (#{Time.at(most_recent).strftime("%m/%d/%y %H:%M:%S %z")}). Not sending data."
62
+ {}
63
+ else
64
+ #ScoutRails::Agent.instance.logger.debug "Validated time slot: #{Time.at(most_recent).strftime("%m/%d/%y %H:%M:%S %z")}"
65
+ data.first.last
66
+ end
67
+ rescue
68
+ ScoutRails::Agent.instance.logger.debug $!.message
69
+ ScoutRails::Agent.instance.logger.debug $!.backtrace
70
+ end
71
+
72
+ def slot
73
+ t = Time.now
74
+ t -= t.sec
75
+ t.to_i
76
+ end
77
+ end
@@ -0,0 +1,70 @@
1
+ # Logic for the serialized file access
2
+ class ScoutRails::LayawayFile
3
+ def path
4
+ "#{ScoutRails::Agent.instance.log_path}/scout_rails.db"
5
+ end
6
+
7
+ def dump(object)
8
+ Marshal.dump(object)
9
+ end
10
+
11
+ def load(dump)
12
+ if dump.size == 0
13
+ ScoutRails::Agent.instance.logger.debug("No data in layaway file.")
14
+ return nil
15
+ end
16
+ Marshal.load(dump)
17
+ rescue ArgumentError, TypeError => e
18
+ ScoutRails::Agent.instance.logger.debug("Error loading data from layaway file: #{e.inspect}")
19
+ ScoutRails::Agent.instance.logger.debug(e.backtrace.inspect)
20
+ nil
21
+ end
22
+
23
+ def read_and_write
24
+ File.open(path, File::RDWR | File::CREAT) do |f|
25
+ f.flock(File::LOCK_EX)
26
+ begin
27
+ result = (yield get_data(f))
28
+ f.rewind
29
+ f.truncate(0)
30
+ if result
31
+ write(f, dump(result))
32
+ end
33
+ ensure
34
+ f.flock(File::LOCK_UN)
35
+ end
36
+ end
37
+ rescue Errno::ENOENT, Exception => e
38
+ ScoutRails::Agent.instance.logger.error(e.message)
39
+ ScoutRails::Agent.instance.logger.debug(e.backtrace.split("\n"))
40
+ end
41
+
42
+ def get_data(f)
43
+ data = read_until_end(f)
44
+ result = load(data)
45
+ f.truncate(0)
46
+ result
47
+ end
48
+
49
+ def write(f, string)
50
+ result = 0
51
+ while (result < string.length)
52
+ result += f.write_nonblock(string)
53
+ end
54
+ rescue Errno::EAGAIN, Errno::EINTR
55
+ IO.select(nil, [f])
56
+ retry
57
+ end
58
+
59
+ def read_until_end(f)
60
+ contents = ""
61
+ while true
62
+ contents << f.read_nonblock(10_000)
63
+ end
64
+ rescue Errno::EAGAIN, Errno::EINTR
65
+ IO.select([f])
66
+ retry
67
+ rescue EOFError
68
+ contents
69
+ end
70
+ end
@@ -0,0 +1,36 @@
1
+ # Contains the meta information associated with a metric. Used to lookup Metrics in to Store's metric_hash.
2
+ class ScoutRails::MetricMeta
3
+ def initialize(metric_name)
4
+ @metric_name = metric_name
5
+ @metric_id = nil
6
+ @scope = Thread::current[:scout_scope_name]
7
+ end
8
+ attr_accessor :metric_id, :metric_name
9
+ attr_accessor :scope
10
+ attr_accessor :client_id
11
+
12
+ # To avoid conflicts with different JSON libaries
13
+ def to_json(*a)
14
+ %Q[{"metric_id":#{metric_id || 'null'},"metric_name":#{metric_name.to_json},"scope":#{scope.to_json || 'null'}}]
15
+ end
16
+
17
+ def ==(o)
18
+ self.eql?(o)
19
+ end
20
+
21
+ def hash
22
+ h = metric_name.hash
23
+ h ^= scope.hash unless scope.nil?
24
+ h
25
+ end
26
+
27
+ def <=>(o)
28
+ namecmp = self.name <=> o.name
29
+ return namecmp if namecmp != 0
30
+ return (self.scope || '') <=> (o.scope || '')
31
+ end
32
+
33
+ def eql?(o)
34
+ self.class == o.class && metric_name.eql?(o.metric_name) && scope == o.scope && client_id == o.client_id
35
+ end
36
+ end # class MetricMeta