scout_apm 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,85 @@
1
+ module ScoutApm::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
+ ScoutApm::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
+ # sql: SELECT "places".* FROM "places" ORDER BY "places"."position" ASC
25
+ # name: Place Load
26
+ if name && (parts = name.split " ") && parts.size == 2
27
+ model = parts.first
28
+ operation = parts.last.downcase
29
+ metric_name = case operation
30
+ when 'load' then 'find'
31
+ when 'indexes', 'columns' then nil # not under developer control
32
+ when 'destroy', 'find', 'save', 'create', 'exists' then operation
33
+ when 'update' then 'save'
34
+ else
35
+ if model == 'Join'
36
+ operation
37
+ end
38
+ end
39
+ metric = "ActiveRecord/#{model}/#{metric_name}" if metric_name
40
+ metric = "ActiveRecord/SQL/other" if metric.nil?
41
+ else
42
+ metric = "ActiveRecord/SQL/Unknown"
43
+ end
44
+ metric
45
+ end
46
+
47
+ # Removes actual values from SQL. Used to both obfuscate the SQL and group
48
+ # similar queries in the UI.
49
+ def scout_sanitize_sql(sql)
50
+ return nil if sql.length > 1000 # safeguard - don't sanitize large SQL statements
51
+ sql = sql.dup
52
+ sql.gsub!(/\\"/, '') # removing escaping double quotes
53
+ sql.gsub!(/\\'/, '') # removing escaping single quotes
54
+ sql.gsub!(/'(?:[^']|'')*'/, '?') # removing strings (single quote)
55
+ sql.gsub!(/"(?:[^"]|"")*"/, '?') # removing strings (double quote)
56
+ sql.gsub!(/\b\d+\b/, '?') # removing integers
57
+ sql.gsub!(/\?(,\?)+/,'?') # replace multiple ? w/a single ?
58
+ sql
59
+ end
60
+
61
+ end # module ActiveRecordInstruments
62
+ end # module Instruments
63
+
64
+ def add_instruments
65
+ if defined?(ActiveRecord) && defined?(ActiveRecord::Base)
66
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.module_eval do
67
+ include ::ScoutApm::Instruments::ActiveRecordInstruments
68
+ include ::ScoutApm::Tracer
69
+ end
70
+ ActiveRecord::Base.class_eval do
71
+ include ::ScoutApm::Tracer
72
+ end
73
+ end
74
+ rescue
75
+ ScoutApm::Agent.instance.logger.warn "ActiveRecord instrumentation exception: #{$!.message}"
76
+ end
77
+
78
+ if defined?(::Rails) && ::Rails::VERSION::MAJOR.to_i == 3 && ::Rails.respond_to?(:configuration)
79
+ Rails.configuration.after_initialize do
80
+ ScoutApm::Agent.instance.logger.debug "Adding ActiveRecord instrumentation to a Rails 3 app"
81
+ add_instruments
82
+ end
83
+ else
84
+ add_instruments
85
+ end
@@ -0,0 +1,10 @@
1
+ # Mongoid versions that use Moped should instrument Moped.
2
+ if defined?(::Mongoid) and !defined?(::Moped)
3
+ ScoutApm::Agent.instance.logger.debug "Instrumenting Mongoid"
4
+ Mongoid::Collection.class_eval do
5
+ include ScoutApm::Tracer
6
+ (Mongoid::Collections::Operations::ALL - [:<<, :[]]).each do |method|
7
+ instrument_method method, :metric_name => "MongoDB/\#{@klass}/#{method}"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,24 @@
1
+ if defined?(::Moped)
2
+ ScoutApm::Agent.instance.logger.debug "Instrumenting Moped"
3
+ Moped::Node.class_eval do
4
+ include ScoutApm::Tracer
5
+ def process_with_scout_instruments(operation, &callback)
6
+ if operation.respond_to?(:collection)
7
+ collection = operation.collection
8
+ self.class.instrument("MongoDB/Process/#{collection}/#{operation.class.to_s.split('::').last}", :desc => scout_sanitize_log(operation.log_inspect)) do
9
+ process_without_scout_instruments(operation, &callback)
10
+ end
11
+ end
12
+ end
13
+ alias_method :process_without_scout_instruments, :process
14
+ alias_method :process, :process_with_scout_instruments
15
+
16
+ # replaces values w/ ?
17
+ def scout_sanitize_log(log)
18
+ return nil if log.length > 1000 # safeguard - don't sanitize large SQL statements
19
+ log.gsub(/(=>")((?:[^"]|"")*)"/) do
20
+ $1 + '?' + '"'
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ if defined?(::Net) && defined?(Net::HTTP)
2
+ ScoutApm::Agent.instance.logger.debug "Instrumenting Net::HTTP"
3
+ Net::HTTP.class_eval do
4
+ include ScoutApm::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 ScoutApm::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 ScoutApm::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 ScoutApm::Instruments
2
+ module ActionControllerInstruments
3
+ def self.included(instrumented_class)
4
+ ScoutApm::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.scout_apm_trace(scout_controller_action, :uri => request.request_uri, :ip => request.remote_ip) 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 ScoutApm::Tracer
29
+ include ::ScoutApm::Instruments::ActionControllerInstruments
30
+
31
+ def rescue_action_with_scout(exception)
32
+ ScoutApm::Agent.instance.store.track!("Errors/Request",1, :scope => nil)
33
+ ScoutApm::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
+ ScoutApm::Agent.instance.logger.debug "Instrumenting ActionView::Template"
42
+ ActionView::Template.class_eval do
43
+ include ::ScoutApm::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/4
2
+ module ScoutApm::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
+ #ScoutApm::Agent.instance.logger.debug "Processing #{scout_controller_action}"
8
+ self.class.scout_apm_trace(scout_controller_action, :uri => request.fullpath, :ip => request.remote_ip) do
9
+ begin
10
+ super
11
+ rescue Exception => e
12
+ ScoutApm::Agent.instance.store.track!("Errors/Request",1, :scope => nil)
13
+ raise
14
+ ensure
15
+ Thread::current[:scout_apm_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
+ ScoutApm::Agent.instance.logger.debug "Instrumenting ActionController::Metal"
26
+ ActionController::Metal.class_eval do
27
+ include ScoutApm::Tracer
28
+ include ::ScoutApm::Instruments::ActionControllerInstruments
29
+ end
30
+ end
31
+
32
+ if defined?(ActionView) && defined?(ActionView::PartialRenderer)
33
+ ScoutApm::Agent.instance.logger.debug "Instrumenting ActionView::PartialRenderer"
34
+ ActionView::PartialRenderer.class_eval do
35
+ include ScoutApm::Tracer
36
+ instrument_method :render_partial, :metric_name => 'View/#{@template.virtual_path}/Rendering', :scope => true
37
+ end
38
+ end
@@ -0,0 +1,100 @@
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
+ # Data is stored in a Hash, where the keys are Time.to_i on the minute. The value is a Hash {:metrics => Hash, :slow_transactions => Array}.
6
+ # When depositing data, the new data is either merged with an existing time or placed in a new key.
7
+ class ScoutApm::Layaway
8
+ attr_accessor :file
9
+ def initialize
10
+ @file = ScoutApm::LayawayFile.new
11
+ end
12
+
13
+ def deposit_and_deliver
14
+ new_metrics = ScoutApm::Agent.instance.store.metric_hash
15
+ log_deposited_metrics(new_metrics)
16
+ log_deposited_slow_transactions(ScoutApm::Agent.instance.store.slow_transactions)
17
+ to_deliver = {}
18
+ file.read_and_write do |old_data|
19
+ old_data ||= Hash.new
20
+ # merge data
21
+ # if (1) there's data in the file and (2) there isn't any data yet for the current minute, this means we've
22
+ # collected all metrics for the previous slots and we're ready to deliver.
23
+ #
24
+ # Example w/2 processes:
25
+ #
26
+ # 12:00:34 ---
27
+ # Process 1: old_data.any? => false, so deposits.
28
+ # Process 2: old_data_any? => true and old_data[12:00].nil? => false, so deposits.
29
+ #
30
+ # 12:01:34 ---
31
+ # Process 1: old_data.any? => true and old_data[12:01].nil? => true, so delivers metrics.
32
+ # Process 2: old_data.any? => true and old_data[12:01].nil? => false, so deposits.
33
+ if old_data.any? and old_data[slot].nil?
34
+ to_deliver = old_data
35
+ old_data = Hash.new
36
+ elsif old_data.any?
37
+ ScoutApm::Agent.instance.logger.debug "Not yet time to deliver payload for slot [#{Time.at(old_data.keys.sort.last).strftime("%m/%d/%y %H:%M:%S %z")}]"
38
+ else
39
+ ScoutApm::Agent.instance.logger.debug "There is no data in the layaway file to deliver."
40
+ end
41
+ old_data[slot]=ScoutApm::Agent.instance.store.merge_data_and_clear(old_data[slot] || {:metrics => {}, :slow_transactions => []})
42
+ log_saved_data(old_data,new_metrics)
43
+ old_data
44
+ end
45
+ to_deliver.any? ? validate_data(to_deliver) : {}
46
+ end
47
+
48
+ # Ensures the data we're sending to the server isn't stale.
49
+ # This can occur if the agent is collecting data, and app server goes down w/data in the local storage.
50
+ # When it is restarted later data will remain in local storage but it won't be for the current reporting interval.
51
+ #
52
+ # If the data is stale, an empty Hash is returned. Otherwise, the data from the most recent slot is returned.
53
+ def validate_data(data)
54
+ data = data.to_a.sort
55
+ now = Time.now
56
+ if (most_recent = data.first.first) < now.to_i - 2*60
57
+ ScoutApm::Agent.instance.logger.debug "Local Storage is stale (#{Time.at(most_recent).strftime("%m/%d/%y %H:%M:%S %z")}). Not sending data."
58
+ {}
59
+ else
60
+ data.first.last
61
+ end
62
+ rescue
63
+ ScoutApm::Agent.instance.logger.debug $!.message
64
+ ScoutApm::Agent.instance.logger.debug $!.backtrace
65
+ end
66
+
67
+ # Data is stored under timestamp-keys (without the second).
68
+ def slot
69
+ t = Time.now
70
+ t -= t.sec
71
+ t.to_i
72
+ end
73
+
74
+ def log_deposited_metrics(new_metrics)
75
+ controller_count = 0
76
+ new_metrics.each do |meta,stats|
77
+ if meta.metric_name =~ /\AController/
78
+ controller_count += stats.call_count
79
+ end
80
+ end
81
+ ScoutApm::Agent.instance.logger.debug "Depositing #{controller_count} requests into #{Time.at(slot).strftime("%m/%d/%y %H:%M:%S %z")} slot."
82
+ end
83
+
84
+ def log_deposited_slow_transactions(new_slow_transactions)
85
+ ScoutApm::Agent.instance.logger.debug "Depositing #{new_slow_transactions.size} slow transactions into #{Time.at(slot).strftime("%m/%d/%y %H:%M:%S %z")} slot."
86
+ end
87
+
88
+ def log_saved_data(old_data,new_metrics)
89
+ ScoutApm::Agent.instance.logger.debug "Saving the following #{old_data.size} time slots locally:"
90
+ old_data.each do |k,v|
91
+ controller_count = 0
92
+ new_metrics.each do |meta,stats|
93
+ if meta.metric_name =~ /\AController/
94
+ controller_count += stats.call_count
95
+ end
96
+ end
97
+ ScoutApm::Agent.instance.logger.debug "#{Time.at(k).strftime("%m/%d/%y %H:%M:%S %z")} => #{controller_count} requests and #{v[:slow_transactions].size} slow transactions"
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,72 @@
1
+ # Logic for the serialized file access
2
+ class ScoutApm::LayawayFile
3
+ def path
4
+ "#{ScoutApm::Agent.instance.log_path}/scout_apm.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
+ ScoutApm::Agent.instance.logger.debug("No data in layaway file.")
14
+ return nil
15
+ end
16
+ Marshal.load(dump)
17
+ rescue ArgumentError, TypeError => e
18
+ ScoutApm::Agent.instance.logger.debug("Error loading data from layaway file: #{e.inspect}")
19
+ ScoutApm::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
+ ScoutApm::Agent.instance.logger.error("Unable to access the layaway file [#{e.message}]. The user running the app must have read+write access.")
39
+ ScoutApm::Agent.instance.logger.debug(e.backtrace.split("\n"))
40
+ # ensure the in-memory metric hash is cleared so data doesn't continue to accumulate.
41
+ ScoutApm::Agent.instance.store.metric_hash = {}
42
+ end
43
+
44
+ def get_data(f)
45
+ data = read_until_end(f)
46
+ result = load(data)
47
+ f.truncate(0)
48
+ result
49
+ end
50
+
51
+ def write(f, string)
52
+ result = 0
53
+ while (result < string.length)
54
+ result += f.write_nonblock(string)
55
+ end
56
+ rescue Errno::EAGAIN, Errno::EINTR
57
+ IO.select(nil, [f])
58
+ retry
59
+ end
60
+
61
+ def read_until_end(f)
62
+ contents = ""
63
+ while true
64
+ contents << f.read_nonblock(10_000)
65
+ end
66
+ rescue Errno::EAGAIN, Errno::EINTR
67
+ IO.select([f])
68
+ retry
69
+ rescue EOFError
70
+ contents
71
+ end
72
+ end
@@ -0,0 +1,34 @@
1
+ # Contains the meta information associated with a metric. Used to lookup Metrics in to Store's metric_hash.
2
+ class ScoutApm::MetricMeta
3
+ def initialize(metric_name, options = {})
4
+ @metric_name = metric_name
5
+ @metric_id = nil
6
+ @scope = Thread::current[:scout_apm_sub_scope] || Thread::current[:scout_apm_scope_name]
7
+ @desc = options[:desc]
8
+ @extra = {}
9
+ end
10
+ attr_accessor :metric_id, :metric_name
11
+ attr_accessor :scope
12
+ attr_accessor :client_id
13
+ attr_accessor :desc, :extra
14
+
15
+ # To avoid conflicts with different JSON libaries
16
+ def to_json(*a)
17
+ %Q[{"metric_id":#{metric_id || 'null'},"metric_name":#{metric_name.to_json},"scope":#{scope.to_json || 'null'}}]
18
+ end
19
+
20
+ def ==(o)
21
+ self.eql?(o)
22
+ end
23
+
24
+ def hash
25
+ h = metric_name.downcase.hash
26
+ h ^= scope.downcase.hash unless scope.nil?
27
+ h ^= desc.downcase.hash unless desc.nil?
28
+ h
29
+ end
30
+
31
+ def eql?(o)
32
+ self.class == o.class && metric_name.downcase.eql?(o.metric_name.downcase) && scope == o.scope && client_id == o.client_id && desc == o.desc
33
+ end
34
+ end # class MetricMeta
@@ -0,0 +1,49 @@
1
+ # Stats that are associated with each instrumented method.
2
+ class ScoutApm::MetricStats
3
+ attr_accessor :call_count
4
+ attr_accessor :min_call_time
5
+ attr_accessor :max_call_time
6
+ attr_accessor :total_call_time
7
+ attr_accessor :total_exclusive_time
8
+ attr_accessor :sum_of_squares
9
+
10
+ def initialize(scoped = false)
11
+ @scoped = scoped
12
+ self.call_count = 0
13
+ self.total_call_time = 0.0
14
+ self.total_exclusive_time = 0.0
15
+ self.min_call_time = 0.0
16
+ self.max_call_time = 0.0
17
+ self.sum_of_squares = 0.0
18
+ end
19
+
20
+ def update!(call_time,exclusive_time)
21
+ # If this metric is scoped inside another, use exclusive time for min/max and sum_of_squares. Non-scoped metrics
22
+ # (like controller actions) track the total call time.
23
+ t = (@scoped ? exclusive_time : call_time)
24
+ self.min_call_time = t if self.call_count == 0 or t < min_call_time
25
+ self.max_call_time = t if self.call_count == 0 or t > max_call_time
26
+ self.call_count +=1
27
+ self.total_call_time += call_time
28
+ self.total_exclusive_time += exclusive_time
29
+ self.sum_of_squares += (t * t)
30
+ self
31
+ end
32
+
33
+ # combines data from another MetricStats object
34
+ def combine!(other)
35
+ self.call_count += other.call_count
36
+ self.total_call_time += other.total_call_time
37
+ self.total_exclusive_time += other.total_exclusive_time
38
+ self.min_call_time = other.min_call_time if self.min_call_time.zero? or other.min_call_time < self.min_call_time
39
+ self.max_call_time = other.max_call_time if other.max_call_time > self.max_call_time
40
+ self.sum_of_squares += other.sum_of_squares
41
+ self
42
+ end
43
+
44
+ # To avoid conflicts with different JSON libaries handle JSON ourselves.
45
+ # Time-based metrics are converted to milliseconds from seconds.
46
+ def to_json(*a)
47
+ %Q[{"total_exclusive_time":#{total_exclusive_time*1000},"min_call_time":#{min_call_time*1000},"call_count":#{call_count},"sum_of_squares":#{sum_of_squares*1000},"total_call_time":#{total_call_time*1000},"max_call_time":#{max_call_time*1000}}]
48
+ end
49
+ end # class MetricStats
@@ -0,0 +1,35 @@
1
+ class ScoutApm::SlowTransaction
2
+ BACKTRACE_THRESHOLD = 0.5 # the minimum threshold to record the backtrace for a metric.
3
+ BACKTRACE_LIMIT = 5 # Max length of callers to display
4
+ MAX_SIZE = 100 # Limits the size of the metric hash to prevent a metric explosion.
5
+ attr_reader :metric_name, :total_call_time, :metrics, :meta, :uri, :context
6
+
7
+ # Given a call stack, generates a filtered backtrace that:
8
+ # * Limits to the app/models, app/controllers, or app/views directories
9
+ # * Limits to 5 total callers
10
+ # * Makes the app folder the top-level folder used in trace info
11
+ def self.backtrace_parser(backtrace)
12
+ stack = []
13
+ backtrace.each do |c|
14
+ if m=c.match(/(\/app\/(controllers|models|views)\/.+)/)
15
+ stack << m[1]
16
+ break if stack.size == BACKTRACE_LIMIT
17
+ end
18
+ end
19
+ stack
20
+ end
21
+
22
+ def initialize(uri,metric_name,total_call_time,metrics,context)
23
+ @uri = uri
24
+ @metric_name = metric_name
25
+ @total_call_time = total_call_time
26
+ @metrics = metrics
27
+ @context = context
28
+ end
29
+
30
+ # Used to remove metrics when the payload will be too large.
31
+ def clear_metrics!
32
+ @metrics = nil
33
+ self
34
+ end
35
+ end
@@ -0,0 +1,18 @@
1
+ class ScoutApm::StackItem
2
+ attr_accessor :children_time
3
+ attr_reader :metric_name, :start_time
4
+
5
+ def initialize(metric_name)
6
+ @metric_name = metric_name
7
+ @start_time = Time.now
8
+ @children_time = 0
9
+ end
10
+
11
+ def ==(o)
12
+ self.eql?(o)
13
+ end
14
+
15
+ def eql?(o)
16
+ self.class == o.class && metric_name.eql?(o.metric_name)
17
+ end
18
+ end # class StackItem