scout_apm 1.6.8 → 2.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
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