scout_apm 2.1.32 → 2.2.0.pre0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/CHANGELOG.markdown +2 -161
  4. data/Rakefile +2 -2
  5. data/ext/allocations/allocations.c +0 -6
  6. data/ext/allocations/extconf.rb +0 -1
  7. data/ext/stacks/extconf.rb +33 -0
  8. data/ext/stacks/scout_atomics.h +86 -0
  9. data/ext/stacks/stacks.c +744 -0
  10. data/lib/scout_apm.rb +16 -24
  11. data/lib/scout_apm/agent.rb +38 -93
  12. data/lib/scout_apm/agent/logging.rb +1 -6
  13. data/lib/scout_apm/agent/reporting.rb +6 -8
  14. data/lib/scout_apm/app_server_load.rb +10 -21
  15. data/lib/scout_apm/attribute_arranger.rb +2 -0
  16. data/lib/scout_apm/background_job_integrations/delayed_job.rb +1 -71
  17. data/lib/scout_apm/background_job_integrations/sidekiq.rb +27 -66
  18. data/lib/scout_apm/background_worker.rb +15 -19
  19. data/lib/scout_apm/capacity.rb +57 -0
  20. data/lib/scout_apm/config.rb +29 -135
  21. data/lib/scout_apm/context.rb +5 -9
  22. data/lib/scout_apm/deploy_integrations/capistrano_2.cap +12 -0
  23. data/lib/scout_apm/deploy_integrations/capistrano_2.rb +83 -0
  24. data/lib/scout_apm/deploy_integrations/capistrano_3.cap +12 -0
  25. data/lib/scout_apm/deploy_integrations/capistrano_3.rb +88 -0
  26. data/lib/scout_apm/environment.rb +15 -22
  27. data/lib/scout_apm/histogram.rb +2 -11
  28. data/lib/scout_apm/instant/assets/xmlhttp_instrumentation.html +2 -2
  29. data/lib/scout_apm/instant/middleware.rb +57 -198
  30. data/lib/scout_apm/instruments/action_controller_rails_2.rb +2 -1
  31. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +59 -90
  32. data/lib/scout_apm/instruments/active_record.rb +5 -7
  33. data/lib/scout_apm/instruments/delayed_job.rb +57 -0
  34. data/lib/scout_apm/instruments/grape.rb +3 -4
  35. data/lib/scout_apm/instruments/middleware_detailed.rb +6 -4
  36. data/lib/scout_apm/instruments/middleware_summary.rb +1 -39
  37. data/lib/scout_apm/instruments/mongoid.rb +3 -24
  38. data/lib/scout_apm/instruments/net_http.rb +2 -7
  39. data/lib/scout_apm/instruments/percentile_sampler.rb +19 -36
  40. data/lib/scout_apm/instruments/process/process_cpu.rb +2 -3
  41. data/lib/scout_apm/instruments/process/process_memory.rb +3 -3
  42. data/lib/scout_apm/layaway.rb +33 -76
  43. data/lib/scout_apm/layer.rb +59 -16
  44. data/lib/scout_apm/layer_converters/converter_base.rb +0 -199
  45. data/lib/scout_apm/layer_converters/job_converter.rb +1 -1
  46. data/lib/scout_apm/layer_converters/metric_converter.rb +1 -1
  47. data/lib/scout_apm/layer_converters/slow_job_converter.rb +90 -15
  48. data/lib/scout_apm/layer_converters/slow_request_converter.rb +101 -13
  49. data/lib/scout_apm/metric_set.rb +1 -9
  50. data/lib/scout_apm/metric_stats.rb +8 -8
  51. data/lib/scout_apm/reporter.rb +15 -51
  52. data/lib/scout_apm/request_histograms.rb +0 -4
  53. data/lib/scout_apm/request_manager.rb +1 -2
  54. data/lib/scout_apm/scored_item_set.rb +0 -7
  55. data/lib/scout_apm/serializers/deploy_serializer.rb +16 -0
  56. data/lib/scout_apm/serializers/payload_serializer.rb +3 -9
  57. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +5 -2
  58. data/lib/scout_apm/serializers/slow_jobs_serializer_to_json.rb +1 -2
  59. data/lib/scout_apm/server_integrations/puma.rb +2 -5
  60. data/lib/scout_apm/slow_item_set.rb +80 -0
  61. data/lib/scout_apm/slow_job_record.rb +1 -6
  62. data/lib/scout_apm/slow_transaction.rb +2 -20
  63. data/lib/scout_apm/store.rb +12 -50
  64. data/lib/scout_apm/trace_compactor.rb +311 -0
  65. data/lib/scout_apm/tracked_request.rb +37 -128
  66. data/lib/scout_apm/utils/backtrace_parser.rb +5 -7
  67. data/lib/scout_apm/utils/fake_stacks.rb +83 -0
  68. data/lib/scout_apm/version.rb +1 -1
  69. data/scout_apm.gemspec +4 -6
  70. data/test/test_helper.rb +0 -56
  71. data/test/unit/config_test.rb +9 -60
  72. data/test/unit/histogram_test.rb +0 -14
  73. data/test/unit/layaway_test.rb +16 -31
  74. data/test/unit/serializers/payload_serializer_test.rb +105 -3
  75. data/test/unit/slow_item_set_test.rb +94 -0
  76. data/test/unit/slow_job_policy_test.rb +49 -0
  77. data/test/unit/slow_request_policy_test.rb +5 -4
  78. data/test/unit/utils/backtrace_parser_test.rb +0 -19
  79. data/tester.rb +53 -0
  80. metadata +29 -124
  81. data/.rubocop.yml +0 -8
  82. data/Guardfile +0 -42
  83. data/ext/rusage/README.md +0 -26
  84. data/ext/rusage/extconf.rb +0 -5
  85. data/ext/rusage/rusage.c +0 -52
  86. data/lib/scout_apm/background_job_integrations/resque.rb +0 -85
  87. data/lib/scout_apm/background_recorder.rb +0 -43
  88. data/lib/scout_apm/debug.rb +0 -37
  89. data/lib/scout_apm/git_revision.rb +0 -51
  90. data/lib/scout_apm/instruments/action_view.rb +0 -49
  91. data/lib/scout_apm/instruments/resque.rb +0 -40
  92. data/lib/scout_apm/layer_children_set.rb +0 -77
  93. data/lib/scout_apm/limited_layer.rb +0 -122
  94. data/lib/scout_apm/rack.rb +0 -26
  95. data/lib/scout_apm/remote/message.rb +0 -23
  96. data/lib/scout_apm/remote/recorder.rb +0 -57
  97. data/lib/scout_apm/remote/router.rb +0 -49
  98. data/lib/scout_apm/remote/server.rb +0 -58
  99. data/lib/scout_apm/serializers/histograms_serializer_to_json.rb +0 -21
  100. data/lib/scout_apm/synchronous_recorder.rb +0 -26
  101. data/lib/scout_apm/utils/gzip_helper.rb +0 -24
  102. data/lib/scout_apm/utils/numbers.rb +0 -14
  103. data/lib/scout_apm/utils/scm.rb +0 -14
  104. data/test/unit/background_job_integrations/sidekiq_test.rb +0 -104
  105. data/test/unit/context_test.rb +0 -30
  106. data/test/unit/git_revision_test.rb +0 -15
  107. data/test/unit/instruments/net_http_test.rb +0 -21
  108. data/test/unit/instruments/percentile_sampler_test.rb +0 -137
  109. data/test/unit/layer_children_set_test.rb +0 -88
  110. data/test/unit/limited_layer_test.rb +0 -53
  111. data/test/unit/remote/test_message.rb +0 -13
  112. data/test/unit/remote/test_router.rb +0 -33
  113. data/test/unit/remote/test_server.rb +0 -15
  114. data/test/unit/store_test.rb +0 -89
  115. data/test/unit/test_tracked_request.rb +0 -87
  116. data/test/unit/utils/numbers_test.rb +0 -15
  117. data/test/unit/utils/scm.rb +0 -17
@@ -1,53 +1,36 @@
1
1
  module ScoutApm
2
2
  module Instruments
3
-
4
- class HistogramReport
5
- attr_reader :name
6
- attr_reader :histogram
7
-
8
- def initialize(name, histogram)
9
- @name = name
10
- @histogram = histogram
11
- end
12
-
13
- def combine!(other)
14
- raise "Mismatched Histogram Names" unless name == other.name
15
- histogram.combine!(other.histogram)
16
- self
17
- end
18
- end
19
-
20
3
  class PercentileSampler
21
4
  attr_reader :logger
22
5
 
23
- # A hash of { time => RequestHistograms }
24
- attr_reader :histograms
6
+ attr_reader :percentiles
25
7
 
26
- def initialize(logger, histograms)
8
+ def initialize(logger, percentiles)
27
9
  @logger = logger
28
- @histograms = histograms
10
+ @percentiles = Array(percentiles)
29
11
  end
30
12
 
31
13
  def human_name
32
- 'Percentiles'
14
+ "Percentiles"
33
15
  end
34
16
 
35
- def metrics(timestamp, store)
36
- store.track_histograms!(percentiles(timestamp), :timestamp => timestamp)
37
- end
38
-
39
- def percentiles(time)
40
- result = []
41
-
42
- histogram = histograms.delete(time)
43
-
44
- return result unless histogram
45
-
46
- histogram.each_name do |name|
47
- result << HistogramReport.new(name, histogram.raw(name))
17
+ # Gets the 95th%ile for the time requested
18
+ def metrics(time)
19
+ ms = {}
20
+ histos = ScoutApm::Agent.instance.request_histograms_by_time[time]
21
+ histos.each_name do |name|
22
+ percentiles.each do |percentile|
23
+ meta = MetricMeta.new("Percentile/#{percentile}/#{name}")
24
+ stat = MetricStats.new
25
+ stat.update!(histos.quantile(name, percentile))
26
+ ms[meta] = stat
27
+ end
48
28
  end
49
29
 
50
- result
30
+ # Wipe the histograms we just collected data on
31
+ ScoutApm::Agent.instance.request_histograms_by_time.delete(time)
32
+
33
+ ms
51
34
  end
52
35
  end
53
36
  end
@@ -29,17 +29,16 @@ module ScoutApm
29
29
  "Process CPU"
30
30
  end
31
31
 
32
- def metrics(timestamp, store)
32
+ def metrics(_time)
33
33
  result = run
34
34
  if result
35
35
  meta = MetricMeta.new("#{metric_type}/#{metric_name}")
36
36
  stat = MetricStats.new(false)
37
37
  stat.update!(result)
38
- store.track!({ meta => stat }, :timestamp => timestamp)
38
+ { meta => stat }
39
39
  else
40
40
  {}
41
41
  end
42
-
43
42
  end
44
43
 
45
44
  # TODO: Figure out a good default instead of nil
@@ -33,20 +33,20 @@ module ScoutApm
33
33
  "Process Memory"
34
34
  end
35
35
 
36
- def metrics(timestamp, store)
36
+ def metrics(_time)
37
37
  result = run
38
38
  if result
39
39
  meta = MetricMeta.new("#{metric_type}/#{metric_name}")
40
40
  stat = MetricStats.new(false)
41
41
  stat.update!(result)
42
- store.track!({ meta => stat }, :timestamp => timestamp)
42
+ { meta => stat }
43
43
  else
44
44
  {}
45
45
  end
46
46
  end
47
47
 
48
48
  def run
49
- self.class.rss_in_mb.tap { |res| logger.debug "#{human_name}: #{res.inspect}" }
49
+ self.class.rss_in_mb.tap { |res| logger.debug "#{human_name}: #{res.inspect}" }
50
50
  end
51
51
  end
52
52
  end
@@ -7,23 +7,19 @@
7
7
  #
8
8
  module ScoutApm
9
9
  class Layaway
10
+ # How old a file needs to be in Seconds before it gets reported.
11
+ REPORTING_AGE = 120
12
+
10
13
  # How long to let a stale file sit before deleting it.
11
14
  # Letting it sit a bit may be useful for debugging
12
15
  STALE_AGE = 10 * 60
13
16
 
14
- # Failsafe to prevent writing layaway files if for some reason they are not being cleaned up
15
- MAX_FILES_LIMIT = 5000
16
-
17
17
  # A strftime format string for how we render timestamps in filenames.
18
18
  # Must be sortable as an integer
19
19
  TIME_FORMAT = "%Y%m%d%H%M"
20
20
 
21
- attr_accessor :config
22
- attr_reader :environment
23
-
24
- def initialize(config, environment)
25
- @config = config
26
- @environment = environment
21
+ def initialize(directory=nil)
22
+ @directory = directory
27
23
  end
28
24
 
29
25
  # Returns a Pathname object with the fully qualified directory where the layaway files can be placed.
@@ -34,12 +30,12 @@ module ScoutApm
34
30
  def directory
35
31
  return @directory if @directory
36
32
 
37
- data_file = config.value("data_file")
38
- data_file = File.dirname(data_file) if data_file && !File.directory?(data_file)
33
+ data_file = ScoutApm::Agent.instance.config.value("data_file")
34
+ data_file = File.dirname(data_file) if data_file && !File.directory?
39
35
 
40
36
  candidates = [
41
37
  data_file,
42
- "#{environment.root}/tmp",
38
+ "#{ScoutApm::Agent.instance.environment.root}/tmp",
43
39
  "/tmp"
44
40
  ].compact
45
41
 
@@ -48,11 +44,7 @@ module ScoutApm
48
44
  @directory = Pathname.new(found)
49
45
  end
50
46
 
51
- def write_reporting_period(reporting_period, files_limit = MAX_FILES_LIMIT)
52
- if at_layaway_file_limit?(files_limit)
53
- ScoutApm::Agent.instance.logger.error("Hit layaway file limit. Not writing to layaway file")
54
- return false
55
- end
47
+ def write_reporting_period(reporting_period)
56
48
  filename = file_for(reporting_period.timestamp)
57
49
  layaway_file = LayawayFile.new(filename)
58
50
  layaway_file.write(reporting_period)
@@ -64,56 +56,41 @@ module ScoutApm
64
56
  def with_claim(timestamp)
65
57
  coordinator_file = glob_pattern(timestamp, :coordinator)
66
58
 
67
- begin
68
- # This file gets deleted only by a process that successfully created and obtained the exclusive lock
69
- f = File.open(coordinator_file, File::RDWR | File::CREAT | File::EXCL | File::NONBLOCK)
70
- rescue Errno::EEXIST
71
- false
72
- end
73
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)
74
62
  begin
75
- if f
76
- begin
77
- ScoutApm::Agent.instance.logger.debug("Obtained Reporting Lock")
78
-
79
- log_layaway_file_information
80
-
81
- files = all_files_for(timestamp).reject{|l| l.to_s == coordinator_file.to_s }
82
- rps = files.map{ |layaway| LayawayFile.new(layaway).load }.compact
83
- if rps.any?
84
- yield rps
85
-
86
- ScoutApm::Agent.instance.logger.debug("Deleting the now-reported layaway files for #{timestamp.to_s}")
87
- delete_files_for(timestamp) # also removes the coodinator_file
88
-
89
- ScoutApm::Agent.instance.logger.debug("Checking for any Stale layaway files")
90
- delete_stale_files(timestamp.to_time - STALE_AGE)
91
- else
92
- File.unlink(coordinator_file)
93
- ScoutApm::Agent.instance.logger.debug("No layaway files to report")
94
- end
95
-
96
- true
97
- rescue Exception => e
98
- ScoutApm::Agent.instance.logger.debug("Caught an exception in with_claim, with the coordination file locked: #{e.message}, #{e.backtrace.inspect}")
99
- raise
100
- ensure
101
- # Unlock the file when done!
102
- f.flock(File::LOCK_UN | File::LOCK_NB)
103
- f.close
63
+ # Nonblocking, Exclusive lock.
64
+ if f.flock(File::LOCK_EX | File::LOCK_NB)
65
+
66
+ ScoutApm::Agent.instance.logger.debug("Obtained Reporting Lock")
67
+
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
72
+
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")
104
78
  end
79
+
80
+ # Unlock the file when done!
81
+ f.flock(File::LOCK_UN | File::LOCK_NB)
82
+ f.close
83
+ true
105
84
  else
106
85
  # Didn't obtain lock, another process is reporting. Return false from this function, but otherwise no work
86
+ f.close
107
87
  false
108
88
  end
109
89
  end
110
90
  end
111
91
 
112
92
  def delete_files_for(timestamp)
113
- all_files_for(timestamp).each { |layaway|
114
- ScoutApm::Agent.instance.logger.debug("Deleting layaway file: #{layaway}")
115
- File.unlink(layaway)
116
- }
93
+ all_files_for(timestamp).each { |layaway| File.unlink(layaway) }
117
94
  end
118
95
 
119
96
  def delete_stale_files(older_than)
@@ -124,8 +101,6 @@ module ScoutApm
124
101
  select { |timestamp| timestamp.to_i < older_than.strftime(TIME_FORMAT).to_i }.
125
102
  tap { |timestamps| ScoutApm::Agent.instance.logger.debug("Deleting stale layaway files with timestamps: #{timestamps.inspect}") }.
126
103
  map { |timestamp| delete_files_for(timestamp) }
127
- rescue => e
128
- ScoutApm::Agent.instance.logger.debug("Problem deleting stale files: #{e.message}, #{e.backtrace.inspect}")
129
104
  end
130
105
 
131
106
  private
@@ -176,24 +151,6 @@ module ScoutApm
176
151
  nil
177
152
  end
178
153
  end
179
-
180
- def at_layaway_file_limit?(files_limit = MAX_FILES_LIMIT)
181
- all_files_for(:all).count >= files_limit
182
- end
183
-
184
- def log_layaway_file_information
185
- files_in_temp = Dir["#{directory}/*"].count
186
-
187
- all_filenames = all_files_for(:all)
188
- count_per_timestamp = Hash[
189
- all_filenames.
190
- group_by {|f| timestamp_from_filename(f) }.
191
- map{ |timestamp, list| [timestamp, list.length] }
192
- ]
193
-
194
-
195
- ScoutApm::Agent.instance.logger.debug("Total in #{directory}: #{files_in_temp}. Total Layaway Files: #{all_filenames.size}. By Timestamp: #{count_per_timestamp.inspect}")
196
- end
197
154
  end
198
155
  end
199
156
 
@@ -13,17 +13,13 @@ module ScoutApm
13
13
  # instrumentation for an example of how this is useful
14
14
  attr_accessor :name
15
15
 
16
- # An array of children layers
16
+ # An array of children layers, in call order.
17
17
  # For instance, if we are in a middleware, there will likely be only a single
18
18
  # child, which is another middleware. In a Controller, we may have a handful
19
19
  # of children: [ActiveRecord, ActiveRecord, View, HTTP Call].
20
20
  #
21
21
  # This useful to get actual time spent in this layer vs. children time
22
- #
23
- # TODO: Check callers for compatibility w/ nil to avoid making an empty array
24
- def children
25
- @children || LayerChildrenSet.new
26
- end
22
+ attr_reader :children
27
23
 
28
24
  # Time objects recording the start & stop times of this layer
29
25
  attr_reader :start_time, :stop_time
@@ -42,27 +38,41 @@ module ScoutApm
42
38
  # Known Keys:
43
39
  # :record_count - The number of rows returned by an AR query (From notification instantiation.active_record)
44
40
  # :class_name - The ActiveRecord class name (From notification instantiation.active_record)
45
- #
46
- # If no annotations are ever set, this will return nil
47
41
  attr_reader :annotations
48
42
 
43
+ # ScoutProf - trace_index is an index into the Stack structure in the C
44
+ # code, used to store captured traces.
45
+ attr_reader :trace_index
46
+
47
+ # ScoutProf - frame_index is an optimization to not capture a few frames
48
+ # during scoutprof instrumentation
49
+ attr_reader :frame_index
50
+
51
+ # Captured backtraces from ScoutProf. This is distinct from the backtrace
52
+ # attribute, which gets the ruby backtrace of any given layer. StackProf
53
+ # focuses on Controller layers, and requires a native extension and a
54
+ # reasonably recent Ruby.
55
+ attr_reader :traces
56
+
49
57
  BACKTRACE_CALLER_LIMIT = 50 # maximum number of lines to send thru for backtrace analysis
50
58
 
51
59
  def initialize(type, name, start_time = Time.now)
52
60
  @type = type
53
61
  @name = name
62
+ @annotations = {}
54
63
  @start_time = start_time
55
64
  @allocations_start = ScoutApm::Instruments::Allocations.count
56
65
  @allocations_stop = 0
57
-
58
- # initialize these only on first use
59
- @children = nil
60
- @annotations = nil
66
+ @children = [] # In order of calls
61
67
  @desc = nil
68
+
69
+ @traces = ScoutApm::TraceSet.new
70
+ @raw_frames = []
71
+ @frame_index = ScoutApm::Instruments::Stacks.current_frame_index # For efficiency sake, try to skip the bottom X frames when collecting traces
72
+ @trace_index = ScoutApm::Instruments::Stacks.current_trace_index
62
73
  end
63
74
 
64
75
  def add_child(child)
65
- @children ||= LayerChildrenSet.new
66
76
  @children << child
67
77
  end
68
78
 
@@ -81,7 +91,6 @@ module ScoutApm
81
91
 
82
92
  # This data is internal to ScoutApm, to add custom information, use the Context api.
83
93
  def annotate_layer(hsh)
84
- @annotations ||= {}
85
94
  @annotations.merge!(hsh)
86
95
  end
87
96
 
@@ -93,6 +102,14 @@ module ScoutApm
93
102
  @subscopable
94
103
  end
95
104
 
105
+ def traced!
106
+ @traced = true
107
+ end
108
+
109
+ def traced?
110
+ @traced
111
+ end
112
+
96
113
  # This is the old style name. This function is used for now, but should be
97
114
  # removed, and the new type & name split should be enforced through the
98
115
  # app.
@@ -114,6 +131,34 @@ module ScoutApm
114
131
  end
115
132
  end
116
133
 
134
+ # Set the name of the file that this action is coming from.
135
+ # TraceSet uses this to more accurately filter backtraces
136
+ def set_root_class(klass_name)
137
+ @traces.set_root_class(klass_name)
138
+ end
139
+
140
+ def start_sampling
141
+ if ScoutApm::Agent.instance.config.value('profile') && traced?
142
+ ScoutApm::Instruments::Stacks.update_indexes(frame_index, trace_index)
143
+ ScoutApm::Instruments::Stacks.start_sampling
144
+ else
145
+ ScoutApm::Instruments::Stacks.stop_sampling(false)
146
+ end
147
+ end
148
+
149
+ def record_traces!
150
+ if ScoutApm::Agent.instance.config.value('profile')
151
+ ScoutApm::Instruments::Stacks.stop_sampling(false)
152
+ if traced?
153
+ traces.raw_traces = ScoutApm::Instruments::Stacks.profile_frames
154
+ traces.skipped_in_gc = ScoutApm::Instruments::Stacks.skipped_in_gc
155
+ traces.skipped_in_handler = ScoutApm::Instruments::Stacks.skipped_in_handler
156
+ traces.skipped_in_job_registered = ScoutApm::Instruments::Stacks.skipped_in_job_registered
157
+ end
158
+ end
159
+ end
160
+
161
+
117
162
  ######################################
118
163
  # Debugging Helpers
119
164
  ######################################
@@ -154,7 +199,6 @@ module ScoutApm
154
199
  map { |child| child.total_call_time }.
155
200
  inject(0) { |sum, time| sum + time }
156
201
  end
157
- private :child_time
158
202
 
159
203
  ######################################
160
204
  # Allocation Calculations
@@ -180,6 +224,5 @@ module ScoutApm
180
224
  map { |child| child.total_allocations }.
181
225
  inject(0) { |sum, obj| sum + obj }
182
226
  end
183
- private :child_allocations
184
227
  end
185
228
  end
@@ -1,7 +1,6 @@
1
1
  module ScoutApm
2
2
  module LayerConverters
3
3
  class ConverterBase
4
-
5
4
  attr_reader :walker
6
5
  attr_reader :request
7
6
  attr_reader :root_layer
@@ -9,10 +8,7 @@ module ScoutApm
9
8
  def initialize(request)
10
9
  @request = request
11
10
  @root_layer = request.root_layer
12
- @backtraces = []
13
11
  @walker = DepthFirstWalker.new(root_layer)
14
-
15
- @limited = false
16
12
  end
17
13
 
18
14
  # Scope is determined by the first Controller we hit. Most of the time
@@ -31,201 +27,6 @@ module ScoutApm
31
27
  return layer if layer.type == layer_type
32
28
  end
33
29
  end
34
-
35
- ################################################################################
36
- # Subscoping
37
- ################################################################################
38
- #
39
- # Keep a list of subscopes, but only ever use the front one. The rest
40
- # get pushed/popped in cases when we have many levels of subscopable
41
- # layers. This lets us push/pop without otherwise keeping track very closely.
42
- def setup_subscopable_callbacks
43
- @subscope_layers = []
44
-
45
- walker.before do |layer|
46
- if layer.subscopable?
47
- @subscope_layers.push(layer)
48
- end
49
- end
50
-
51
- walker.after do |layer|
52
- if layer.subscopable?
53
- @subscope_layers.pop
54
- end
55
- end
56
- end
57
-
58
- def subscoped?(layer)
59
- @subscope_layers.first && layer != @subscope_layers.first # Don't scope under ourself.
60
- end
61
-
62
- def subscope_name
63
- @subscope_layers.first.legacy_metric_name
64
- end
65
-
66
-
67
- ################################################################################
68
- # Backtrace Handling
69
- ################################################################################
70
- #
71
- # Because we get several layers for the same thing if you call an
72
- # instrumented thing repeatedly, and only some of them may have
73
- # backtraces captured, we store the backtraces off into another spot
74
- # during processing, then at the end, we loop over those saved
75
- # backtraces, putting them back into the metrics hash.
76
- #
77
- # This comes up most often when capturing n+1 backtraces. Because the
78
- # query may be fast enough to evade our time-limit based backtrace
79
- # capture, only the Nth item (see TrackedRequest for more detail) has a
80
- # backtrack captured. This sequence makes sure that we report up that
81
- # backtrace in the aggregated set of metrics around that call.
82
-
83
- # Call this as you are processing each layer. It will store off backtraces
84
- def store_backtrace(layer, meta)
85
- return unless layer.backtrace
86
-
87
- bt = ScoutApm::Utils::BacktraceParser.new(layer.backtrace).call
88
- if bt.any?
89
- meta.backtrace = bt
90
- @backtraces << meta
91
- end
92
- end
93
-
94
- # Call this after you finish walking the layers, and want to take the
95
- # set-aside backtraces and place them into the metas they match
96
- def attach_backtraces(metric_hash)
97
- @backtraces.each do |meta_with_backtrace|
98
- metric_hash.keys.find { |k| k == meta_with_backtrace }.backtrace = meta_with_backtrace.backtrace
99
- end
100
- metric_hash
101
- end
102
-
103
-
104
- ################################################################################
105
- # Limit Handling
106
- ################################################################################
107
-
108
- # To prevent huge traces from being generated, we should stop collecting
109
- # detailed metrics as we go beyond some reasonably large count.
110
- #
111
- # We should still add up the /all aggregates.
112
-
113
- MAX_METRICS = 500
114
-
115
- def over_metric_limit?(metric_hash)
116
- if metric_hash.size > MAX_METRICS
117
- @limited = true
118
- else
119
- false
120
- end
121
- end
122
-
123
- def limited?
124
- !! @limited
125
- end
126
-
127
- ################################################################################
128
- # Meta Scope
129
- ################################################################################
130
-
131
- # When we make MetricMeta records, we need to determine a few things from layer.
132
- def make_meta_options(layer)
133
- scope_hash = make_meta_options_scope(layer)
134
- desc_hash = make_meta_options_desc_hash(layer)
135
-
136
- scope_hash.merge(desc_hash)
137
- end
138
-
139
- def make_meta_options_scope(layer)
140
- # This layer is scoped under another thing. Typically that means this is a layer under a view.
141
- # Like: Controller -> View/users/show -> ActiveRecord/user/find
142
- # in that example, the scope is the View/users/show
143
- if subscoped?(layer)
144
- {:scope => subscope_name}
145
-
146
- # We don't scope the controller under itself
147
- elsif layer == scope_layer
148
- {}
149
-
150
- # This layer is a top level metric ("ActiveRecord", or "HTTP" or
151
- # whatever, directly under the controller), so scope to the
152
- # Controller
153
- else
154
- {:scope => scope_layer.legacy_metric_name}
155
- end
156
- end
157
-
158
- def make_meta_options_desc_hash(layer, max_desc_length=1000)
159
- if layer.desc
160
- desc_s = layer.desc.to_s
161
- trimmed_desc = desc_s[0 .. max_desc_length]
162
- {:desc => trimmed_desc}
163
- else
164
- {}
165
- end
166
- end
167
-
168
-
169
- ################################################################################
170
- # Storing metrics into the hashes
171
- ################################################################################
172
-
173
- # This is the detailed metric - type, name, backtrace, annotations, etc.
174
- def store_specific_metric(layer, metric_hash, allocation_metric_hash)
175
- return false if over_metric_limit?(metric_hash)
176
-
177
- meta_options = make_meta_options(layer)
178
-
179
- meta = MetricMeta.new(layer.legacy_metric_name, meta_options)
180
- meta.extra.merge!(layer.annotations) if layer.annotations
181
-
182
- store_backtrace(layer, meta)
183
-
184
- metric_hash[meta] ||= MetricStats.new(meta_options.has_key?(:scope))
185
- allocation_metric_hash[meta] ||= MetricStats.new(meta_options.has_key?(:scope))
186
-
187
- # timing
188
- stat = metric_hash[meta]
189
- stat.update!(layer.total_call_time, layer.total_exclusive_time)
190
-
191
- # allocations
192
- stat = allocation_metric_hash[meta]
193
- stat.update!(layer.total_allocations, layer.total_exclusive_allocations)
194
-
195
- if LimitedLayer === layer
196
- metric_hash[meta].call_count = layer.count
197
- allocation_metric_hash[meta].call_count = layer.count
198
- end
199
- end
200
-
201
- # Merged Metric - no specifics, just sum up by type (ActiveRecord, View, HTTP, etc)
202
- def store_aggregate_metric(layer, metric_hash, allocation_metric_hash)
203
- meta = MetricMeta.new("#{layer.type}/all")
204
-
205
- metric_hash[meta] ||= MetricStats.new(false)
206
- allocation_metric_hash[meta] ||= MetricStats.new(false)
207
-
208
- # timing
209
- stat = metric_hash[meta]
210
- stat.update!(layer.total_call_time, layer.total_exclusive_time)
211
-
212
- # allocations
213
- stat = allocation_metric_hash[meta]
214
- stat.update!(layer.total_allocations, layer.total_exclusive_allocations)
215
- end
216
-
217
- ################################################################################
218
- # Misc Helpers
219
- ################################################################################
220
-
221
- # Sometimes we start capturing a layer without knowing if we really
222
- # want to make an entry for it. See ActiveRecord instrumentation for
223
- # an example. We start capturing before we know if a query is cached
224
- # or not, and want to skip any cached queries.
225
- def skip_layer?(layer)
226
- return false if layer.annotations.nil?
227
- return true if layer.annotations[:ignorable]
228
- end
229
30
  end
230
31
  end
231
32
  end