scout_apm 1.6.8 → 2.0.0.pre

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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -1
  3. data/CHANGELOG.markdown +7 -57
  4. data/ext/allocations/allocations.c +84 -0
  5. data/ext/allocations/extconf.rb +3 -0
  6. data/lib/scout_apm/agent/reporting.rb +9 -32
  7. data/lib/scout_apm/agent.rb +45 -31
  8. data/lib/scout_apm/app_server_load.rb +1 -2
  9. data/lib/scout_apm/attribute_arranger.rb +0 -4
  10. data/lib/scout_apm/background_worker.rb +6 -9
  11. data/lib/scout_apm/bucket_name_splitter.rb +3 -3
  12. data/lib/scout_apm/call_set.rb +1 -0
  13. data/lib/scout_apm/config.rb +110 -66
  14. data/lib/scout_apm/environment.rb +16 -10
  15. data/lib/scout_apm/framework_integrations/rails_2.rb +12 -14
  16. data/lib/scout_apm/framework_integrations/rails_3_or_4.rb +5 -17
  17. data/lib/scout_apm/framework_integrations/ruby.rb +0 -4
  18. data/lib/scout_apm/framework_integrations/sinatra.rb +0 -4
  19. data/lib/scout_apm/histogram.rb +0 -20
  20. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +1 -4
  21. data/lib/scout_apm/instruments/active_record.rb +149 -8
  22. data/lib/scout_apm/instruments/mongoid.rb +5 -78
  23. data/lib/scout_apm/instruments/process/process_cpu.rb +0 -12
  24. data/lib/scout_apm/instruments/process/process_memory.rb +14 -43
  25. data/lib/scout_apm/layaway.rb +34 -134
  26. data/lib/scout_apm/layaway_file.rb +50 -27
  27. data/lib/scout_apm/layer.rb +45 -1
  28. data/lib/scout_apm/layer_converters/allocation_metric_converter.rb +17 -0
  29. data/lib/scout_apm/layer_converters/converter_base.rb +4 -6
  30. data/lib/scout_apm/layer_converters/job_converter.rb +1 -0
  31. data/lib/scout_apm/layer_converters/metric_converter.rb +2 -1
  32. data/lib/scout_apm/layer_converters/slow_job_converter.rb +42 -21
  33. data/lib/scout_apm/layer_converters/slow_request_converter.rb +58 -37
  34. data/lib/scout_apm/metric_meta.rb +1 -5
  35. data/lib/scout_apm/metric_set.rb +6 -15
  36. data/lib/scout_apm/reporter.rb +4 -6
  37. data/lib/scout_apm/serializers/metrics_to_json_serializer.rb +5 -1
  38. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +1 -3
  39. data/lib/scout_apm/serializers/slow_jobs_serializer_to_json.rb +5 -3
  40. data/lib/scout_apm/slow_job_policy.rb +19 -89
  41. data/lib/scout_apm/slow_job_record.rb +12 -20
  42. data/lib/scout_apm/slow_request_policy.rb +12 -80
  43. data/lib/scout_apm/slow_transaction.rb +16 -20
  44. data/lib/scout_apm/stackprof_tree_collapser.rb +103 -0
  45. data/lib/scout_apm/store.rb +16 -78
  46. data/lib/scout_apm/tracked_request.rb +53 -36
  47. data/lib/scout_apm/utils/active_record_metric_name.rb +2 -0
  48. data/lib/scout_apm/utils/fake_stack_prof.rb +40 -0
  49. data/lib/scout_apm/utils/klass_helper.rb +26 -0
  50. data/lib/scout_apm/utils/sql_sanitizer.rb +1 -1
  51. data/lib/scout_apm/utils/sql_sanitizer_regex.rb +2 -2
  52. data/lib/scout_apm/utils/sql_sanitizer_regex_1_8_7.rb +2 -2
  53. data/lib/scout_apm/version.rb +1 -1
  54. data/lib/scout_apm.rb +13 -7
  55. data/scout_apm.gemspec +3 -1
  56. data/test/test_helper.rb +3 -4
  57. data/test/unit/layaway_test.rb +8 -5
  58. data/test/unit/serializers/payload_serializer_test.rb +2 -2
  59. data/test/unit/slow_item_set_test.rb +1 -2
  60. data/test/unit/sql_sanitizer_test.rb +0 -6
  61. metadata +28 -20
  62. data/LICENSE.md +0 -27
  63. data/lib/scout_apm/instruments/grape.rb +0 -69
  64. data/lib/scout_apm/instruments/percentile_sampler.rb +0 -37
  65. data/lib/scout_apm/request_histograms.rb +0 -46
  66. data/lib/scout_apm/scored_item_set.rb +0 -79
  67. data/test/unit/metric_set_test.rb +0 -101
  68. data/test/unit/scored_item_set_test.rb +0 -65
  69. data/test/unit/slow_request_policy_test.rb +0 -42
@@ -17,89 +17,16 @@ module ScoutApm
17
17
 
18
18
  # Mongoid versions that use Moped should instrument Moped.
19
19
  if defined?(::Mongoid) and !defined?(::Moped)
20
- ScoutApm::Agent.instance.logger.info "Instrumenting Mongoid 2.x"
20
+ ScoutApm::Agent.instance.logger.info "Instrumenting Mongoid"
21
21
 
22
- ### OLD (2.x) mongoids
23
- if defined?(::Mongoid::Collection)
24
- ::Mongoid::Collection.class_eval do
25
- include ScoutApm::Tracer
26
- (::Mongoid::Collections::Operations::ALL - [:<<, :[]]).each do |method|
27
- instrument_method method, :type => "MongoDB", :name => '#{@klass}/' + method.to_s
28
- end
29
- end
30
- end
31
-
32
- ### See moped instrument for Moped driven deploys
33
-
34
- ### 5.x Mongoid
35
- if mongoid_v5? && defined?(::Mongoid::Contextual::Mongo)
36
- ScoutApm::Agent.instance.logger.info "Instrumenting Mongoid 5.x"
37
- # All the public methods from Mongoid::Contextual::Mongo.
38
- # TODO: Geo and MapReduce support (?). They are in other Contextual::* classes
39
- methods = [
40
- :count, :delete, :destroy, :distinct, :each,
41
- :explain, :find_first, :find_one_and_delete, :find_one_and_replace,
42
- :find_one_and_update, :first, :geo_near, :initialize, :last,
43
- :length, :limit, :map, :map_reduce, :pluck,
44
- :skip, :sort, :update, :update_all,
45
- ]
46
- # :exists?,
47
-
48
- methods.each do |method|
49
- if ::Mongoid::Contextual::Mongo.method_defined?(method)
50
- with_scout_instruments = %Q[
51
- def #{method}_with_scout_instruments(*args, &block)
52
-
53
- req = ScoutApm::RequestManager.lookup
54
- *db, collection = view.collection.namespace.split(".")
55
-
56
- name = collection + "/#{method}"
57
- filter = ScoutApm::Instruments::Mongoid.anonymize_filter(view.filter)
58
-
59
- layer = ScoutApm::Layer.new("MongoDB", name)
60
- layer.desc = filter.inspect
61
-
62
- req.start_layer( layer )
63
- begin
64
- #{method}_without_scout_instruments(*args, &block)
65
- ensure
66
- req.stop_layer
67
- end
68
- end
69
-
70
- alias_method :#{method}_without_scout_instruments, :#{method}
71
- alias_method :#{method}, :#{method}_with_scout_instruments
72
- ]
73
-
74
- ::Mongoid::Contextual::Mongo.class_eval(with_scout_instruments)
75
- end
22
+ ::Mongoid::Collection.class_eval do
23
+ include ScoutApm::Tracer
24
+ (::Mongoid::Collections::Operations::ALL - [:<<, :[]]).each do |method|
25
+ instrument_method method, :type => "MongoDB", :name => '#{@klass}/' + method.to_s
76
26
  end
77
27
  end
78
28
  end
79
29
  end
80
-
81
- def mongoid_v5?
82
- if defined?(::Mongoid::VERSION)
83
- ::Mongoid::VERSION =~ /\A5/
84
- else
85
- false
86
- end
87
- end
88
-
89
-
90
- # Example of what a filter looks like: => {"founded"=>{"$gte"=>"1980-1-1"}, "name"=>{"$in"=>["Tool", "Deftones", "Melvins"]}}
91
- # Approach: find every leaf-node, clear it. inspect the whole thing when done.
92
- def self.anonymize_filter(filter)
93
- Hash[
94
- filter.map do |k,v|
95
- if v.is_a? Hash
96
- [k, anonymize_filter(v)]
97
- else
98
- [k, "?"]
99
- end
100
- end
101
- ]
102
- end
103
30
  end
104
31
  end
105
32
  end
@@ -29,18 +29,6 @@ module ScoutApm
29
29
  "Process CPU"
30
30
  end
31
31
 
32
- def metrics(_time)
33
- result = run
34
- if result
35
- meta = MetricMeta.new("#{metric_type}/#{metric_name}")
36
- stat = MetricStats.new(false)
37
- stat.update!(result)
38
- { meta => stat }
39
- else
40
- {}
41
- end
42
- end
43
-
44
32
  # TODO: Figure out a good default instead of nil
45
33
  def run
46
34
  res = nil
@@ -4,6 +4,19 @@ module ScoutApm
4
4
  class ProcessMemory
5
5
  attr_reader :logger
6
6
 
7
+ # Account for Darwin returning maxrss in bytes and Linux in KB. Used by the slow converters. Doesn't feel like this should go here though...more of a utility.
8
+ def self.rss_to_mb(rss)
9
+ rss.to_f/1024/(ScoutApm::Agent.instance.environment.os == 'darwin' ? 1024 : 1)
10
+ end
11
+
12
+ def self.rss
13
+ ::Process.rusage.maxrss
14
+ end
15
+
16
+ def self.rss_in_mb
17
+ rss_to_mb(rss)
18
+ end
19
+
7
20
  def initialize(logger)
8
21
  @logger = logger
9
22
  end
@@ -20,50 +33,8 @@ module ScoutApm
20
33
  "Process Memory"
21
34
  end
22
35
 
23
- def metrics(_time)
24
- result = run
25
- if result
26
- meta = MetricMeta.new("#{metric_type}/#{metric_name}")
27
- stat = MetricStats.new(false)
28
- stat.update!(result)
29
- { meta => stat }
30
- else
31
- {}
32
- end
33
- end
34
-
35
36
  def run
36
- case RUBY_PLATFORM.downcase
37
- when /linux/
38
- get_mem_from_procfile
39
- when /darwin9/ # 10.5
40
- get_mem_from_shell("ps -o rsz")
41
- when /darwin1[01234]/ # 10.6 - 10.11
42
- get_mem_from_shell("ps -o rss")
43
- else
44
- 0 # What default? was nil.
45
- end.tap { |res| logger.debug "#{human_name}: #{res.inspect}" }
46
- end
47
-
48
- private
49
-
50
- def get_mem_from_procfile
51
- res = nil
52
- proc_status = File.open(procfile, "r") { |f| f.read_nonblock(4096).strip }
53
- if proc_status =~ /RSS:\s*(\d+) kB/i
54
- res= $1.to_f / 1024.0
55
- end
56
- res
57
- end
58
-
59
- def procfile
60
- "/proc/#{$$}/status"
61
- end
62
-
63
- # memory in MB the current process is using
64
- def get_mem_from_shell(command)
65
- res = `#{command} #{$$}`.split("\n")[1].to_f / 1024.0 #rescue nil
66
- res
37
+ self.class.rss_in_mb.tap { |res| logger.debug "#{human_name}: #{res.inspect}" }
67
38
  end
68
39
  end
69
40
  end
@@ -1,156 +1,56 @@
1
- # Stores StoreReportingPeriod objects in a per-process file before sending them to the server.
2
- # Coordinates a single process to collect up all individual files, merge them, then send.
3
- #
4
- # Each layaway file is named basedir/scout_#{timestamp}_#{pid}.data
5
- # Where timestamp is in the format:
6
- # And PID is the process id of the running process
7
- #
1
+ # Stores StoreReportingPeriod objects in a file before sending them to the server.
2
+ # 1. A centralized store for multiple Agent processes. This way, only 1 checkin is sent to Scout rather than 1 per-process.
8
3
  module ScoutApm
9
4
  class Layaway
10
- # How old a file needs to be in Seconds before it gets reported.
11
- REPORTING_AGE = 120
5
+ attr_accessor :file
12
6
 
13
- # How long to let a stale file sit before deleting it.
14
- # Letting it sit a bit may be useful for debugging
15
- STALE_AGE = 10 * 60
16
-
17
- # A strftime format string for how we render timestamps in filenames.
18
- # Must be sortable as an integer
19
- TIME_FORMAT = "%Y%m%d%H%M"
20
-
21
- def initialize(directory=nil)
22
- @directory = directory
23
- end
24
-
25
- # Returns a Pathname object with the fully qualified directory where the layaway files can be placed.
26
- # That directory must be writable by this process.
27
- #
28
- # Don't set this in initializer, since it relies on agent instance existing to figure out the value.
29
- #
30
- def directory
31
- return @directory if @directory
32
-
33
- data_file = ScoutApm::Agent.instance.config.value("data_file")
34
- data_file = File.dirname(data_file) if data_file && !File.directory?
35
-
36
- candidates = [
37
- data_file,
38
- "#{ScoutApm::Agent.instance.environment.root}/tmp",
39
- "/tmp"
40
- ].compact
41
-
42
- found = candidates.detect { |dir| File.writable?(dir) }
43
- ScoutApm::Agent.instance.logger.debug("Storing Layaway Files in #{found}")
44
- @directory = Pathname.new(found)
45
- end
46
-
47
- def write_reporting_period(reporting_period)
48
- filename = file_for(reporting_period.timestamp)
49
- layaway_file = LayawayFile.new(filename)
50
- layaway_file.write(reporting_period)
7
+ def initialize
8
+ @file = ScoutApm::LayawayFile.new
51
9
  end
52
10
 
53
- # Claims a given timestamp (getting a lock on a particular filename),
54
- # then yields ReportingPeriods collected up from all the files.
55
- # If the yield returns truthy, delete the layaway files that made it up.
56
- def with_claim(timestamp)
57
- coordinator_file = glob_pattern(timestamp, :coordinator)
58
-
59
-
60
- # This file gets deleted only by a process that successfully obtained a lock
61
- f = File.open(coordinator_file, File::RDWR | File::CREAT)
62
- begin
63
- # Nonblocking, Exclusive lock.
64
- if f.flock(File::LOCK_EX | File::LOCK_NB)
65
-
66
- ScoutApm::Agent.instance.logger.debug("Obtained Reporting Lock")
11
+ def add_reporting_period(time, reporting_period)
12
+ file.read_and_write do |existing_data|
13
+ existing_data ||= Hash.new
14
+ ScoutApm::Agent.instance.logger.debug("AddReportingPeriod: Adding a reporting_period with timestamp: #{reporting_period.timestamp.to_s}, and #{reporting_period.request_count} requests")
67
15
 
68
- files = all_files_for(timestamp).reject{|l| l.to_s == coordinator_file.to_s }
69
- rps = files.map{ |layaway| LayawayFile.new(layaway).load }.compact
70
- if rps.any?
71
- yield rps
16
+ existing_data = existing_data.merge(time => reporting_period) {|key, old_val, new_val|
17
+ old_req = old_val.request_count
18
+ new_req = new_val.request_count
19
+ ScoutApm::Agent.instance.logger.debug("Merging Two reporting periods (#{old_val.timestamp.to_s}, #{new_val.timestamp.to_s}): old req #{old_req}, new req #{new_req}")
72
20
 
73
- delete_files_for(timestamp) # also removes the coodinator_file
74
- delete_stale_files(timestamp.to_time - STALE_AGE)
75
- else
76
- File.unlink(coordinator_file)
77
- ScoutApm::Agent.instance.logger.debug("No layaway files to report")
78
- end
21
+ old_val.
22
+ merge_metrics!(new_val.metrics_payload).
23
+ merge_slow_transactions!(new_val.slow_transactions).
24
+ merge_jobs!(new_val.jobs)
25
+ }
79
26
 
80
- # Unlock the file when done!
81
- f.flock(File::LOCK_UN | File::LOCK_NB)
82
- f.close
83
- true
84
- else
85
- # Didn't obtain lock, another process is reporting. Return false from this function, but otherwise no work
86
- f.close
87
- false
88
- end
27
+ ScoutApm::Agent.instance.logger.debug("AddReportingPeriod: AfterMerge Timestamps: #{existing_data.keys.map(&:to_s).inspect}")
28
+ existing_data
89
29
  end
90
30
  end
91
31
 
92
- def delete_files_for(timestamp)
93
- all_files_for(timestamp).each { |layaway| File.unlink(layaway) }
94
- end
95
-
96
- def delete_stale_files(older_than)
97
- all_files_for(:all).
98
- map { |filename| timestamp_from_filename(filename) }.
99
- compact.
100
- uniq.
101
- select { |timestamp| timestamp.to_i < older_than.strftime(TIME_FORMAT).to_i }.
102
- tap { |timestamps| ScoutApm::Agent.instance.logger.debug("Deleting stale layaway files with timestamps: #{timestamps.inspect}") }.
103
- map { |timestamp| delete_files_for(timestamp) }
104
- end
32
+ REPORTING_INTERVAL = 60 # seconds
105
33
 
106
- private
107
-
108
- ##########################################
109
- # Looking up files
110
-
111
- def file_for(timestamp)
112
- glob_pattern(timestamp)
113
- end
34
+ # Returns an array of ReportingPeriod objects that are ready to be pushed to the server
35
+ def periods_ready_for_delivery
36
+ ready_for_delivery = []
37
+ file.read_and_write do |existing_data|
38
+ existing_data ||= {}
114
39
 
115
- def all_files_for(timestamp)
116
- Dir[glob_pattern(timestamp, :all)]
117
- end
40
+ ScoutApm::Agent.instance.logger.debug("PeriodsReadyForDeliver: All Timestamps: #{existing_data.keys.map(&:to_s).inspect}")
118
41
 
119
- # Timestamp should be either :all or a Time-ish object that responds to strftime (StoreReportingPeriodTimestamp does)
120
- # if timestamp == :all then find all timestamps, otherwise format it.
121
- # if pid == :all, get the files for all
122
- def glob_pattern(timestamp, pid=$$)
123
- timestamp_pattern = format_timestamp(timestamp)
124
- pid_pattern = format_pid(pid)
125
- directory + "scout_#{timestamp_pattern}_#{pid_pattern}.data"
126
- end
42
+ ready_for_delivery = existing_data.to_a.select {|time, rp| should_send?(rp) } # Select off the values we want. to_a is needed for compatibility with Ruby 1.8.7.
127
43
 
128
- def format_timestamp(timestamp)
129
- if timestamp == :all
130
- "*"
131
- elsif timestamp.respond_to?(:strftime)
132
- timestamp.strftime(TIME_FORMAT)
133
- else
134
- timestamp.to_s
44
+ # Rewrite anything not plucked out back to the file
45
+ existing_data.reject {|k, v| ready_for_delivery.map(&:first).include?(k) }
135
46
  end
136
- end
137
47
 
138
- def format_pid(pid)
139
- if pid == :all
140
- "*"
141
- else
142
- pid.to_s
143
- end
48
+ return ready_for_delivery.map(&:last)
144
49
  end
145
50
 
146
- def timestamp_from_filename(filename)
147
- match = filename.match(%r{scout_(.*)_.*\.data})
148
- if match
149
- match[1]
150
- else
151
- nil
152
- end
51
+ # We just want to send anything older than X
52
+ def should_send?(reporting_period)
53
+ reporting_period.timestamp.age_in_seconds > (REPORTING_INTERVAL * 2)
153
54
  end
154
55
  end
155
56
  end
156
-
@@ -1,36 +1,70 @@
1
- # A single layaway file. See Layaway for the management of the group of files.
1
+ # Logic for the serialized file access
2
2
  module ScoutApm
3
3
  class LayawayFile
4
- attr_reader :path
4
+ def path
5
+ ScoutApm::Agent.instance.config.value("data_file") ||
6
+ "#{ScoutApm::Agent.instance.default_log_path}/scout_apm.db"
7
+ end
5
8
 
6
- def initialize(path)
7
- @path = path
9
+ def dump(object)
10
+ Marshal.dump(object)
8
11
  end
9
12
 
10
- def load
11
- data = File.open(path, "r") { |f| read_raw(f) }
12
- deserialize(data)
13
+ def load(dump)
14
+ if dump.size == 0
15
+ ScoutApm::Agent.instance.logger.debug("No data in layaway file.")
16
+ return nil
17
+ end
18
+ Marshal.load(dump)
13
19
  rescue NameError, ArgumentError, TypeError => e
14
- # Marshal error
15
20
  ScoutApm::Agent.instance.logger.info("Unable to load data from Layaway file, resetting.")
16
21
  ScoutApm::Agent.instance.logger.debug("#{e.message}, #{e.backtrace.join("\n\t")}")
17
22
  nil
18
23
  end
19
24
 
20
- def write(data)
21
- serialized_data = serialize(data)
22
- File.open(path, "w") { |f| write_raw(f, serialized_data) }
25
+ def read_and_write
26
+ File.open(path, File::RDWR | File::CREAT) do |f|
27
+ f.flock(File::LOCK_EX)
28
+ begin
29
+ result = (yield get_data(f))
30
+ f.rewind
31
+ f.truncate(0)
32
+ if result
33
+ write(f, dump(result))
34
+ end
35
+ ensure
36
+ f.flock(File::LOCK_UN)
37
+ end
38
+ end
39
+ rescue Errno::ENOENT, Exception => e
40
+ ScoutApm::Agent.instance.logger.error("Unable to access the layaway file [#{e.class} - #{e.message}]. " +
41
+ "The user running the app must have read & write access. " +
42
+ "Change the path by setting the `data_file` key in scout_apm.yml"
43
+ )
44
+ ScoutApm::Agent.instance.logger.debug(e.backtrace.join("\n\t"))
45
+
46
+ # ensure the in-memory metric hash is cleared so data doesn't continue to accumulate.
47
+ # ScoutApm::Agent.instance.store.metric_hash = {}
23
48
  end
24
49
 
25
- def serialize(data)
26
- Marshal.dump(data)
50
+ def get_data(f)
51
+ data = read_until_end(f)
52
+ result = load(data)
53
+ f.truncate(0)
54
+ result
27
55
  end
28
56
 
29
- def deserialize(data)
30
- Marshal.load(data)
57
+ def write(f, string)
58
+ result = 0
59
+ while (result < string.length)
60
+ result += f.write_nonblock(string)
61
+ end
62
+ rescue Errno::EAGAIN, Errno::EINTR
63
+ IO.select(nil, [f])
64
+ retry
31
65
  end
32
66
 
33
- def read_raw(f)
67
+ def read_until_end(f)
34
68
  contents = ""
35
69
  while true
36
70
  contents << f.read_nonblock(10_000)
@@ -41,16 +75,5 @@ module ScoutApm
41
75
  rescue EOFError
42
76
  contents
43
77
  end
44
-
45
- def write_raw(f, data)
46
- result = 0
47
- while (result < data.length)
48
- result += f.write_nonblock(data)
49
- end
50
- rescue Errno::EAGAIN, Errno::EINTR
51
- IO.select(nil, [f])
52
- retry
53
- end
54
78
  end
55
79
  end
56
-
@@ -7,7 +7,11 @@ module ScoutApm
7
7
 
8
8
  # Name: a more specific name of this single item
9
9
  # Examples: "Rack::Cache", "User#find", "users/index", "users/index.html.erb"
10
- attr_reader :name
10
+ #
11
+ # Accessor, so we can update a layer if multiple pieces of instrumentation work
12
+ # together at different layers to fill in the full data. See the ActiveRecord
13
+ # instrumentation for an example of how this is useful
14
+ attr_accessor :name
11
15
 
12
16
  # An array of children layers, in call order.
13
17
  # For instance, if we are in a middleware, there will likely be only a single
@@ -30,12 +34,21 @@ module ScoutApm
30
34
  # backtrace of where it occurred.
31
35
  attr_reader :backtrace
32
36
 
37
+ # As we go through a part of a request, instrumentation can store additional data
38
+ # Known Keys:
39
+ # :record_count - The number of rows returned by an AR query (From notification instantiation.active_record)
40
+ # :class_name - The ActiveRecord class name (From notification instantiation.active_record)
41
+ attr_reader :annotations
42
+
33
43
  BACKTRACE_CALLER_LIMIT = 30 # maximum number of lines to send thru for backtrace analysis
34
44
 
35
45
  def initialize(type, name, start_time = Time.now)
36
46
  @type = type
37
47
  @name = name
48
+ @annotations = {}
38
49
  @start_time = start_time
50
+ @allocations_start = ScoutApm::Instruments::Allocations.count
51
+ @allocations_stop = 0
39
52
  @children = [] # In order of calls
40
53
  @desc = nil
41
54
  end
@@ -48,10 +61,20 @@ module ScoutApm
48
61
  @stop_time = stop_time
49
62
  end
50
63
 
64
+ # Fetch the current number of allocated objects. This will always increment - we fetch when initializing and when stopping the layer.
65
+ def record_allocations!
66
+ @allocations_stop = ScoutApm::Instruments::Allocations.count
67
+ end
68
+
51
69
  def desc=(desc)
52
70
  @desc = desc
53
71
  end
54
72
 
73
+ # This data is internal to ScoutApm, to add custom information, use the Context api.
74
+ def annotate_layer(hsh)
75
+ @annotations.merge!(hsh)
76
+ end
77
+
55
78
  def subscopable!
56
79
  @subscopable = true
57
80
  end
@@ -123,5 +146,26 @@ module ScoutApm
123
146
  map { |child| child.total_call_time }.
124
147
  inject(0) { |sum, time| sum + time }
125
148
  end
149
+
150
+ ######################################
151
+ # Allocation Calculations
152
+ ######################################
153
+
154
+ # These are almost identical to the timing metrics.
155
+
156
+ def total_allocations
157
+ allocations = (@allocations_stop - @allocations_start)
158
+ allocations < 0 ? 0 : allocations
159
+ end
160
+
161
+ def total_exclusive_allocations
162
+ total_allocations - child_allocations
163
+ end
164
+
165
+ def child_allocations
166
+ children.
167
+ map { |child| child.total_allocations }.
168
+ inject(0) { |sum, obj| sum + obj }
169
+ end
126
170
  end
127
171
  end
@@ -0,0 +1,17 @@
1
+ module ScoutApm
2
+ module LayerConverters
3
+ class AllocationMetricConverter < ConverterBase
4
+ def call
5
+ scope = scope_layer
6
+ return {} unless scope
7
+ return {} unless ScoutApm::Instruments::Allocations::ENABLED
8
+
9
+ meta = MetricMeta.new("ObjectAllocations", {:scope => scope.legacy_metric_name})
10
+ stat = MetricStats.new
11
+ stat.update!(root_layer.total_allocations)
12
+
13
+ { meta => stat }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -19,12 +19,10 @@ module ScoutApm
19
19
  # render :update
20
20
  # end
21
21
  def scope_layer
22
- @scope_layer ||= find_first_layer_of_type("Controller") || find_first_layer_of_type("Job")
23
- end
24
-
25
- def find_first_layer_of_type(layer_type)
26
- walker.walk do |layer|
27
- return layer if layer.type == layer_type
22
+ @scope_layer ||= walker.walk do |layer|
23
+ if layer.type == "Controller"
24
+ break layer
25
+ end
28
26
  end
29
27
  end
30
28
  end
@@ -57,6 +57,7 @@ module ScoutApm
57
57
  walker.walk do |layer|
58
58
  next if layer == job_layer
59
59
  next if layer == queue_layer
60
+ next if layer.annotations[:ignorable]
60
61
 
61
62
  # we don't need to use the full metric name for scoped metrics as we
62
63
  # only display metrics aggregrated by type, just use "ActiveRecord"
@@ -1,6 +1,5 @@
1
1
  # Take a TrackedRequest and turn it into a hash of:
2
2
  # MetricMeta => MetricStats
3
-
4
3
  module ScoutApm
5
4
  module LayerConverters
6
5
  class MetricConverter < ConverterBase
@@ -22,6 +21,8 @@ module ScoutApm
22
21
  metric_hash = Hash.new
23
22
 
24
23
  walker.walk do |layer|
24
+ next if layer.annotations[:ignorable]
25
+
25
26
  meta_options = if layer == scope_layer # We don't scope the controller under itself
26
27
  {}
27
28
  else