scout_apm 3.0.0.pre28 → 4.0.4

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 (135) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +49 -0
  3. data/.gitignore +1 -1
  4. data/.rubocop.yml +5 -5
  5. data/.travis.yml +19 -14
  6. data/CHANGELOG.markdown +145 -4
  7. data/Gemfile +1 -7
  8. data/README.markdown +13 -4
  9. data/Rakefile +1 -1
  10. data/ext/allocations/allocations.c +2 -0
  11. data/gems/README.md +28 -0
  12. data/gems/octoshark.gemfile +4 -0
  13. data/gems/rails3.gemfile +5 -0
  14. data/gems/rails4.gemfile +4 -0
  15. data/gems/rails5.gemfile +4 -0
  16. data/gems/rails6.gemfile +4 -0
  17. data/lib/scout_apm.rb +39 -9
  18. data/lib/scout_apm/agent.rb +29 -10
  19. data/lib/scout_apm/agent/exit_handler.rb +0 -1
  20. data/lib/scout_apm/agent_context.rb +22 -3
  21. data/lib/scout_apm/app_server_load.rb +7 -2
  22. data/lib/scout_apm/attribute_arranger.rb +0 -2
  23. data/lib/scout_apm/auto_instrument.rb +5 -0
  24. data/lib/scout_apm/auto_instrument/instruction_sequence.rb +31 -0
  25. data/lib/scout_apm/auto_instrument/layer.rb +23 -0
  26. data/lib/scout_apm/auto_instrument/parser.rb +27 -0
  27. data/lib/scout_apm/auto_instrument/rails.rb +175 -0
  28. data/lib/scout_apm/background_job_integrations/delayed_job.rb +1 -1
  29. data/lib/scout_apm/background_job_integrations/faktory.rb +103 -0
  30. data/lib/scout_apm/background_job_integrations/legacy_sneakers.rb +55 -0
  31. data/lib/scout_apm/background_job_integrations/que.rb +134 -0
  32. data/lib/scout_apm/background_job_integrations/resque.rb +6 -2
  33. data/lib/scout_apm/background_job_integrations/shoryuken.rb +124 -0
  34. data/lib/scout_apm/background_job_integrations/sidekiq.rb +5 -19
  35. data/lib/scout_apm/background_job_integrations/sneakers.rb +87 -0
  36. data/lib/scout_apm/config.rb +45 -8
  37. data/lib/scout_apm/detailed_trace.rb +217 -0
  38. data/lib/scout_apm/environment.rb +20 -1
  39. data/lib/scout_apm/error.rb +27 -0
  40. data/lib/scout_apm/error_service.rb +32 -0
  41. data/lib/scout_apm/error_service/error_buffer.rb +39 -0
  42. data/lib/scout_apm/error_service/error_record.rb +211 -0
  43. data/lib/scout_apm/error_service/ignored_exceptions.rb +66 -0
  44. data/lib/scout_apm/error_service/middleware.rb +32 -0
  45. data/lib/scout_apm/error_service/notifier.rb +33 -0
  46. data/lib/scout_apm/error_service/payload.rb +47 -0
  47. data/lib/scout_apm/error_service/periodic_work.rb +17 -0
  48. data/lib/scout_apm/error_service/railtie.rb +11 -0
  49. data/lib/scout_apm/error_service/sidekiq.rb +80 -0
  50. data/lib/scout_apm/extensions/transaction_callback_payload.rb +1 -1
  51. data/lib/scout_apm/fake_store.rb +3 -0
  52. data/lib/scout_apm/framework_integrations/rails_2.rb +2 -1
  53. data/lib/scout_apm/framework_integrations/rails_3_or_4.rb +3 -1
  54. data/lib/scout_apm/git_revision.rb +6 -3
  55. data/lib/scout_apm/instant/middleware.rb +2 -1
  56. data/lib/scout_apm/instrument_manager.rb +9 -7
  57. data/lib/scout_apm/instruments/action_controller_rails_2.rb +3 -1
  58. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +56 -55
  59. data/lib/scout_apm/instruments/action_view.rb +122 -26
  60. data/lib/scout_apm/instruments/active_record.rb +66 -18
  61. data/lib/scout_apm/instruments/http.rb +48 -0
  62. data/lib/scout_apm/instruments/memcached.rb +43 -0
  63. data/lib/scout_apm/instruments/mongoid.rb +9 -4
  64. data/lib/scout_apm/instruments/net_http.rb +8 -1
  65. data/lib/scout_apm/instruments/typhoeus.rb +88 -0
  66. data/lib/scout_apm/job_record.rb +4 -2
  67. data/lib/scout_apm/layaway_file.rb +4 -0
  68. data/lib/scout_apm/layer.rb +6 -57
  69. data/lib/scout_apm/layer_children_set.rb +9 -8
  70. data/lib/scout_apm/layer_converters/converter_base.rb +15 -30
  71. data/lib/scout_apm/layer_converters/slow_job_converter.rb +12 -2
  72. data/lib/scout_apm/layer_converters/slow_request_converter.rb +14 -4
  73. data/lib/scout_apm/layer_converters/trace_converter.rb +184 -0
  74. data/lib/scout_apm/limited_layer.rb +0 -7
  75. data/lib/scout_apm/metric_stats.rb +0 -8
  76. data/lib/scout_apm/middleware.rb +1 -1
  77. data/lib/scout_apm/periodic_work.rb +19 -0
  78. data/lib/scout_apm/remote/message.rb +4 -0
  79. data/lib/scout_apm/remote/server.rb +13 -1
  80. data/lib/scout_apm/reporter.rb +8 -3
  81. data/lib/scout_apm/reporting.rb +2 -1
  82. data/lib/scout_apm/request_histograms.rb +8 -0
  83. data/lib/scout_apm/serializers/app_server_load_serializer.rb +4 -0
  84. data/lib/scout_apm/serializers/directive_serializer.rb +4 -0
  85. data/lib/scout_apm/serializers/payload_serializer.rb +2 -2
  86. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +8 -7
  87. data/lib/scout_apm/slow_job_record.rb +5 -1
  88. data/lib/scout_apm/slow_policy/age_policy.rb +33 -0
  89. data/lib/scout_apm/slow_policy/percent_policy.rb +22 -0
  90. data/lib/scout_apm/slow_policy/percentile_policy.rb +24 -0
  91. data/lib/scout_apm/slow_policy/policy.rb +21 -0
  92. data/lib/scout_apm/slow_policy/speed_policy.rb +16 -0
  93. data/lib/scout_apm/slow_request_policy.rb +18 -77
  94. data/lib/scout_apm/slow_transaction.rb +3 -1
  95. data/lib/scout_apm/store.rb +0 -1
  96. data/lib/scout_apm/tracer.rb +2 -2
  97. data/lib/scout_apm/tracked_request.rb +39 -30
  98. data/lib/scout_apm/utils/backtrace_parser.rb +3 -0
  99. data/lib/scout_apm/utils/marshal_logging.rb +90 -0
  100. data/lib/scout_apm/utils/sql_sanitizer.rb +10 -1
  101. data/lib/scout_apm/utils/sql_sanitizer_regex.rb +8 -1
  102. data/lib/scout_apm/utils/sql_sanitizer_regex_1_8_7.rb +6 -0
  103. data/lib/scout_apm/utils/unique_id.rb +27 -0
  104. data/lib/scout_apm/version.rb +1 -1
  105. data/scout_apm.gemspec +13 -7
  106. data/test/test_helper.rb +2 -2
  107. data/test/unit/agent_context_test.rb +29 -0
  108. data/test/unit/auto_instrument/assignments-instrumented.rb +31 -0
  109. data/test/unit/auto_instrument/assignments.rb +31 -0
  110. data/test/unit/auto_instrument/controller-ast.txt +57 -0
  111. data/test/unit/auto_instrument/controller-instrumented.rb +49 -0
  112. data/test/unit/auto_instrument/controller.rb +49 -0
  113. data/test/unit/auto_instrument/rescue_from-instrumented.rb +13 -0
  114. data/test/unit/auto_instrument/rescue_from.rb +13 -0
  115. data/test/unit/auto_instrument_test.rb +54 -0
  116. data/test/unit/environment_test.rb +2 -2
  117. data/test/unit/error_service/error_buffer_test.rb +25 -0
  118. data/test/unit/error_service/ignored_exceptions_test.rb +49 -0
  119. data/test/unit/instruments/active_record_test.rb +40 -0
  120. data/test/unit/layer_children_set_test.rb +9 -0
  121. data/test/unit/request_histograms_test.rb +17 -0
  122. data/test/unit/serializers/payload_serializer_test.rb +39 -5
  123. data/test/unit/slow_request_policy_test.rb +41 -13
  124. data/test/unit/sql_sanitizer_test.rb +78 -0
  125. data/test/unit/tracer_test.rb +25 -0
  126. metadata +101 -18
  127. data/ext/stacks/extconf.rb +0 -38
  128. data/ext/stacks/scout_atomics.h +0 -86
  129. data/ext/stacks/stacks.c +0 -814
  130. data/lib/scout_apm/slow_job_policy.rb +0 -111
  131. data/lib/scout_apm/trace_compactor.rb +0 -312
  132. data/lib/scout_apm/utils/fake_stacks.rb +0 -88
  133. data/test/unit/instruments/active_record_instruments_test.rb +0 -5
  134. data/test/unit/slow_job_policy_test.rb +0 -6
  135. data/tester.rb +0 -53
data/lib/scout_apm.rb CHANGED
@@ -15,7 +15,7 @@ require 'socket'
15
15
  require 'thread'
16
16
  require 'time'
17
17
  require 'yaml'
18
- require 'rbconfig'
18
+ require 'securerandom'
19
19
 
20
20
  #####################################
21
21
  # Gem Requires
@@ -45,6 +45,7 @@ require 'scout_apm/layer_converters/database_converter'
45
45
  require 'scout_apm/layer_converters/slow_request_converter'
46
46
  require 'scout_apm/layer_converters/request_queue_time_converter'
47
47
  require 'scout_apm/layer_converters/allocation_metric_converter'
48
+ require 'scout_apm/layer_converters/trace_converter'
48
49
  require 'scout_apm/layer_converters/histograms'
49
50
  require 'scout_apm/layer_converters/find_layer_by_type'
50
51
 
@@ -57,8 +58,13 @@ require 'scout_apm/server_integrations/webrick'
57
58
  require 'scout_apm/server_integrations/null'
58
59
 
59
60
  require 'scout_apm/background_job_integrations/sidekiq'
61
+ require 'scout_apm/background_job_integrations/faktory'
60
62
  require 'scout_apm/background_job_integrations/delayed_job'
61
63
  require 'scout_apm/background_job_integrations/resque'
64
+ require 'scout_apm/background_job_integrations/shoryuken'
65
+ require 'scout_apm/background_job_integrations/sneakers'
66
+ require 'scout_apm/background_job_integrations/que'
67
+ require 'scout_apm/background_job_integrations/legacy_sneakers'
62
68
 
63
69
  require 'scout_apm/framework_integrations/rails_2'
64
70
  require 'scout_apm/framework_integrations/rails_3_or_4'
@@ -73,8 +79,10 @@ require 'scout_apm/histogram'
73
79
 
74
80
  require 'scout_apm/instruments/net_http'
75
81
  require 'scout_apm/instruments/http_client'
82
+ require 'scout_apm/instruments/typhoeus'
76
83
  require 'scout_apm/instruments/moped'
77
84
  require 'scout_apm/instruments/mongoid'
85
+ require 'scout_apm/instruments/memcached'
78
86
  require 'scout_apm/instruments/redis'
79
87
  require 'scout_apm/instruments/influxdb'
80
88
  require 'scout_apm/instruments/elasticsearch'
@@ -94,12 +102,6 @@ require 'scout_apm/instruments/process/process_memory'
94
102
  require 'scout_apm/instruments/percentile_sampler'
95
103
  require 'scout_apm/instruments/samplers'
96
104
 
97
- begin
98
- require 'stacks'
99
- rescue LoadError
100
- require 'scout_apm/utils/fake_stacks'
101
- end
102
-
103
105
  require 'scout_apm/app_server_load'
104
106
 
105
107
  require 'scout_apm/ignored_uris.rb'
@@ -113,6 +115,7 @@ require 'scout_apm/utils/time'
113
115
  require 'scout_apm/utils/unique_id'
114
116
  require 'scout_apm/utils/numbers'
115
117
  require 'scout_apm/utils/gzip_helper'
118
+ require 'scout_apm/utils/marshal_logging'
116
119
 
117
120
  require 'scout_apm/config'
118
121
  require 'scout_apm/environment'
@@ -133,7 +136,6 @@ require 'scout_apm/tracer'
133
136
  require 'scout_apm/transaction'
134
137
  require 'scout_apm/context'
135
138
  require 'scout_apm/instant_reporting'
136
- require 'scout_apm/trace_compactor'
137
139
  require 'scout_apm/background_recorder'
138
140
  require 'scout_apm/synchronous_recorder'
139
141
 
@@ -142,9 +144,15 @@ require 'scout_apm/metric_stats'
142
144
  require 'scout_apm/db_query_metric_stats'
143
145
  require 'scout_apm/slow_transaction'
144
146
  require 'scout_apm/slow_job_record'
147
+ require 'scout_apm/detailed_trace'
145
148
  require 'scout_apm/scored_item_set'
149
+
146
150
  require 'scout_apm/slow_request_policy'
147
- require 'scout_apm/slow_job_policy'
151
+ require 'scout_apm/slow_policy/age_policy'
152
+ require 'scout_apm/slow_policy/speed_policy'
153
+ require 'scout_apm/slow_policy/percent_policy'
154
+ require 'scout_apm/slow_policy/percentile_policy'
155
+
148
156
  require 'scout_apm/job_record'
149
157
  require 'scout_apm/request_histograms'
150
158
  require 'scout_apm/transaction_time_consumed'
@@ -185,6 +193,16 @@ require 'scout_apm/tasks/support'
185
193
  require 'scout_apm/extensions/config'
186
194
  require 'scout_apm/extensions/transaction_callback_payload'
187
195
 
196
+ require 'scout_apm/error_service'
197
+ require 'scout_apm/error_service/middleware'
198
+ require 'scout_apm/error_service/notifier'
199
+ require 'scout_apm/error_service/sidekiq'
200
+ require 'scout_apm/error_service/ignored_exceptions'
201
+ require 'scout_apm/error_service/error_buffer'
202
+ require 'scout_apm/error_service/error_record'
203
+ require 'scout_apm/error_service/periodic_work'
204
+ require 'scout_apm/error_service/payload'
205
+
188
206
  if defined?(Rails) && defined?(Rails::VERSION) && defined?(Rails::VERSION::MAJOR) && Rails::VERSION::MAJOR >= 3 && defined?(Rails::Railtie)
189
207
  module ScoutApm
190
208
  class Railtie < Rails::Railtie
@@ -197,6 +215,18 @@ if defined?(Rails) && defined?(Rails::VERSION) && defined?(Rails::VERSION::MAJOR
197
215
  # Attempt to start right away, this will work best for preloading apps, Unicorn & Puma & similar
198
216
  ScoutApm::Agent.instance.install
199
217
 
218
+ if ScoutApm::Agent.instance.context.config.value("auto_instruments")
219
+ ScoutApm::Agent.instance.context.logger.debug("AutoInstruments is enabled.")
220
+ require 'scout_apm/auto_instrument'
221
+ else
222
+ ScoutApm::Agent.instance.context.logger.debug("AutoInstruments is disabled.")
223
+ end
224
+
225
+ if ScoutApm::Agent.instance.context.config.value("errors_enabled")
226
+ app.config.middleware.insert_after ActionDispatch::DebugExceptions, ScoutApm::ErrorService::Middleware
227
+ ScoutApm::ErrorService::Sidekiq.new.install
228
+ end
229
+
200
230
  # Install the middleware every time in development mode.
201
231
  # The middleware is a noop if dev_trace is not enabled in config
202
232
  if Rails.env.development?
@@ -37,10 +37,13 @@ module ScoutApm
37
37
 
38
38
  logger.info "Scout Agent [#{ScoutApm::VERSION}] Initialized"
39
39
 
40
- instrument_manager.install! if should_load_instruments? || force
41
-
42
- install_background_job_integrations
43
- install_app_server_integration
40
+ if should_load_instruments? || force
41
+ instrument_manager.install!
42
+ install_background_job_integrations
43
+ install_app_server_integration
44
+ else
45
+ logger.info "Not Loading Instruments"
46
+ end
44
47
 
45
48
  logger.info "Scout Agent [#{ScoutApm::VERSION}] Installed"
46
49
 
@@ -63,6 +66,7 @@ module ScoutApm
63
66
 
64
67
  if context.started?
65
68
  start_background_worker unless background_worker_running?
69
+ start_error_service_background_worker unless error_service_background_worker_running?
66
70
  return
67
71
  end
68
72
 
@@ -78,6 +82,7 @@ module ScoutApm
78
82
  @app_server_load ||= AppServerLoad.new(context).run
79
83
 
80
84
  start_background_worker
85
+ start_error_service_background_worker
81
86
  end
82
87
 
83
88
  def instrument_manager
@@ -165,12 +170,6 @@ module ScoutApm
165
170
 
166
171
  ScoutApm::Agent::ExitHandler.new(context).install
167
172
 
168
- if context.config.value('profile')
169
- # After we fork, setup the handlers here.
170
- ScoutApm::Instruments::Stacks.install
171
- ScoutApm::Instruments::Stacks.start
172
- end
173
-
174
173
  periodic_work = ScoutApm::PeriodicWork.new(context)
175
174
 
176
175
  @background_worker = ScoutApm::BackgroundWorker.new(context)
@@ -201,5 +200,25 @@ module ScoutApm
201
200
  @background_worker &&
202
201
  @background_worker.running?
203
202
  end
203
+
204
+ # seconds to batch error reports
205
+ ERROR_SEND_FREQUENCY = 5
206
+ def start_error_service_background_worker
207
+ periodic_work = ScoutApm::ErrorService::PeriodicWork.new(context)
208
+
209
+ @error_service_background_worker = ScoutApm::BackgroundWorker.new(context, ERROR_SEND_FREQUENCY)
210
+ @error_service_background_worker_thread = Thread.new do
211
+ @error_service_background_worker.start {
212
+ periodic_work.run
213
+ }
214
+ end
215
+ end
216
+
217
+ def error_service_background_worker_running?
218
+ @error_service_background_worker_thread &&
219
+ @error_service_background_worker_thread.alive? &&
220
+ @error_service_background_worker &&
221
+ @error_service_background_worker.running?
222
+ end
204
223
  end
205
224
  end
@@ -35,7 +35,6 @@ module ScoutApm
35
35
  logger.info "Shutting down ScoutApm"
36
36
  return if !context.started?
37
37
  context.shutting_down!
38
- ScoutApm::Instruments::Stacks.uninstall
39
38
  ::ScoutApm::Agent.instance.stop_background_worker
40
39
  end
41
40
 
@@ -96,11 +96,18 @@ module ScoutApm
96
96
  end
97
97
 
98
98
  def slow_request_policy
99
- @slow_request_policy ||= ScoutApm::SlowRequestPolicy.new(self)
99
+ @slow_request_policy ||= ScoutApm::SlowRequestPolicy.new(self).tap{|p| p.add_default_policies }
100
100
  end
101
101
 
102
102
  def slow_job_policy
103
- @slow_job_policy ||= ScoutApm::SlowJobPolicy.new(self)
103
+ @slow_job_policy ||= ScoutApm::SlowRequestPolicy.new(self).tap{|p| p.add_default_policies }
104
+ end
105
+
106
+ # Maintains a Histogram of insignificant/significant autoinstrument layers.
107
+ # significant = 1
108
+ # insignificant = 0
109
+ def auto_instruments_layer_histograms
110
+ @auto_instruments_layer_histograms ||= ScoutApm::RequestHistograms.new
104
111
  end
105
112
 
106
113
  # Histogram of the cumulative requests since the start of the process
@@ -135,6 +142,18 @@ module ScoutApm
135
142
  config.value('dev_trace') && environment.env == "development"
136
143
  end
137
144
 
145
+ ###################
146
+ # Error Service #
147
+ ###################
148
+
149
+ def error_buffer
150
+ @error_buffer ||= ScoutApm::ErrorService::ErrorBuffer.new(self)
151
+ end
152
+
153
+ def ignored_exceptions
154
+ @ignored_exceptions ||= ScoutApm::ErrorService::IgnoredExceptions.new(self, config.value('errors_ignored_exceptions'))
155
+ end
156
+
138
157
  #############
139
158
  # Setters #
140
159
  #############
@@ -205,7 +224,7 @@ module ScoutApm
205
224
  if !@config.any_keys_found?
206
225
  logger.info("No configuration file loaded, and no configuration found in ENV. " +
207
226
  "For assistance configuring Scout, visit " +
208
- "http://help.apm.scoutapp.com/#configuration-options")
227
+ "https://docs.scoutapm.com/#ruby-configuration-options")
209
228
  end
210
229
  end
211
230
  end
@@ -29,12 +29,17 @@ module ScoutApm
29
29
  end
30
30
 
31
31
  def data
32
- { :server_time => to_s_safe(Time.now),
32
+ {
33
+ :language => 'ruby',
34
+ :language_version => RUBY_VERSION,
35
+ :ruby_version => RUBY_VERSION, # Deprecated.
36
+
33
37
  :framework => to_s_safe(environment.framework_integration.human_name),
34
38
  :framework_version => to_s_safe(environment.framework_integration.version),
39
+
40
+ :server_time => to_s_safe(Time.now),
35
41
  :environment => to_s_safe(environment.framework_integration.env),
36
42
  :app_server => to_s_safe(environment.app_server),
37
- :ruby_version => RUBY_VERSION,
38
43
  :hostname => to_s_safe(environment.hostname),
39
44
  :database_engine => to_s_safe(environment.database_engine), # Detected
40
45
  :database_adapter => to_s_safe(environment.raw_database_adapter), # Raw
@@ -5,8 +5,6 @@ module ScoutApm
5
5
  def self.call(subject, attributes_list)
6
6
  attributes_list.inject({}) do |attribute_hash, attribute|
7
7
  case attribute
8
- when :traces
9
- attribute_hash[attribute] = subject.traces.to_a
10
8
  when Array
11
9
  attribute_hash[attribute[0]] = subject.send(attribute[1])
12
10
  when :bucket
@@ -0,0 +1,5 @@
1
+ if RUBY_VERSION >= "2.3"
2
+ require 'scout_apm/auto_instrument/instruction_sequence'
3
+ else
4
+ warn "ScoutApm::AutoInstrument requires Ruby >= v2.3. Skipping."
5
+ end
@@ -0,0 +1,31 @@
1
+
2
+ require 'scout_apm/auto_instrument/rails'
3
+
4
+ module ScoutApm
5
+ module AutoInstrument
6
+ module InstructionSequence
7
+ def load_iseq(path)
8
+ if Rails.controller_path?(path) & !Rails.ignore?(path)
9
+ begin
10
+ new_code = Rails.rewrite(path)
11
+ return self.compile(new_code, path, path)
12
+ rescue
13
+ warn "Failed to apply auto-instrumentation to #{path}: #{$!}"
14
+ end
15
+ elsif Rails.ignore?(path)
16
+ warn "AutoInstruments are ignored for path=#{path}."
17
+ end
18
+
19
+ return self.compile_file(path)
20
+ end
21
+ end
22
+
23
+ # This should work (https://bugs.ruby-lang.org/issues/15572), but it doesn't.
24
+ # RubyVM::InstructionSequence.extend(InstructionSequence)
25
+
26
+ # So we do this instead:
27
+ class << ::RubyVM::InstructionSequence
28
+ prepend InstructionSequence
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+
2
+ module ScoutApm
3
+ def self.AutoInstrument(name, backtrace)
4
+ request = ScoutApm::RequestManager.lookup
5
+
6
+ file_name, _ = backtrace.first.split(":", 2)
7
+
8
+ begin
9
+ layer = ScoutApm::Layer.new('AutoInstrument', name)
10
+ layer.backtrace = backtrace
11
+ layer.file_name = file_name
12
+
13
+ request.start_layer(layer)
14
+ started_layer = true
15
+
16
+ result = yield
17
+ ensure
18
+ request.stop_layer if started_layer
19
+ end
20
+
21
+ return result
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+
2
+ require 'parser/current'
3
+ raise LoadError, "Parser::TreeRewriter was not defined" unless defined?(Parser::TreeRewriter)
4
+
5
+ module ScoutApm
6
+ module AutoInstrument
7
+ class Cache
8
+ def initialize
9
+ @local_assignments = {}
10
+ end
11
+
12
+ def local_assignments?(node)
13
+ unless @local_assignments.key?(node)
14
+ if node.type == :lvasgn
15
+ @local_assignments[node] = true
16
+ elsif node.children.find{|child| child.is_a?(Parser::AST::Node) && self.local_assignments?(child)}
17
+ @local_assignments[node] = true
18
+ else
19
+ @local_assignments[node] = false
20
+ end
21
+ end
22
+
23
+ return @local_assignments[node]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,175 @@
1
+
2
+ require 'scout_apm/auto_instrument/layer'
3
+ require 'scout_apm/auto_instrument/parser'
4
+
5
+ module ScoutApm
6
+ module AutoInstrument
7
+ module Rails
8
+ # A general pattern to match Rails controller files:
9
+ CONTROLLER_FILE = /\/app\/controllers\/*\/.*_controller.rb$/.freeze
10
+
11
+ # Some gems (Devise) provide controllers that match CONTROLLER_FILE pattern.
12
+ # Try a simple match to see if it's a Gemfile
13
+ GEM_FILE = /\/gems?\//.freeze
14
+
15
+ # Whether the given path is likely to be a Rails controller and not provided by a Gem.
16
+ def self.controller_path? path
17
+ CONTROLLER_FILE.match(path) && !GEM_FILE.match(path)
18
+ end
19
+
20
+ # Autoinstruments increases overhead when applied to many code expressions that perform little work.
21
+ # You can exclude files from autoinstruments via the `auto_instruments_ignore` option.
22
+ def self.ignore?(path)
23
+ res = false
24
+ ScoutApm::Agent.instance.context.config.value('auto_instruments_ignore').each do |ignored_file_name|
25
+ if path.include?(ignored_file_name)
26
+ res = true
27
+ break
28
+ end
29
+ end
30
+ res
31
+ end
32
+
33
+ def self.rewrite(path, code = nil)
34
+ code ||= File.read(path)
35
+
36
+ ast = ::Parser::CurrentRuby.parse(code)
37
+
38
+ # pp ast
39
+
40
+ buffer = ::Parser::Source::Buffer.new(path)
41
+ buffer.source = code
42
+
43
+ rewriter = Rewriter.new
44
+
45
+ # Rewrite the AST, returns a String with the new form.
46
+ rewriter.rewrite(buffer, ast)
47
+ end
48
+
49
+ class Rewriter < ::Parser::TreeRewriter
50
+ def initialize
51
+ super
52
+
53
+ # Keeps track of the parent - child relationship between nodes:
54
+ @nesting = []
55
+
56
+ # The stack of method nodes (type :def):
57
+ @method = []
58
+
59
+ # The stack of class nodes:
60
+ @scope = []
61
+
62
+ @cache = Cache.new
63
+ end
64
+
65
+ def instrument(source, file_name, line)
66
+ # Don't log huge chunks of code... just the first line:
67
+ if lines = source.lines and lines.count > 1
68
+ source = lines.first.chomp + "..."
69
+ end
70
+
71
+ method_name = @method.last.children[0]
72
+ class_name = @scope.last.children[1]
73
+ bt = ["#{file_name}:#{line}:in `#{method_name}'"]
74
+
75
+ return [
76
+ "::ScoutApm::AutoInstrument("+ source.dump + ",#{bt}){",
77
+ "}"
78
+ ]
79
+ end
80
+
81
+ # Look up 1 or more nodes to check if the parent exists and matches the given type.
82
+ # @param type [Symbol] the symbol type to match.
83
+ # @param up [Integer] how far up to look.
84
+ def parent_type?(type, up = 1)
85
+ parent = @nesting[@nesting.size - up - 1] and parent.type == type
86
+ end
87
+
88
+ def on_block(node)
89
+ # If we are not in a method, don't do any instrumentation:
90
+ return if @method.empty?
91
+
92
+ line = node.location.line || 'line?'
93
+ column = node.location.column || 'column?' # not used
94
+ method_name = node.children[0].children[1] || '*unknown*' # not used
95
+ file_name = @source_rewriter.source_buffer.name
96
+
97
+ wrap(node.location.expression, *instrument(node.location.expression.source, file_name, line))
98
+ end
99
+
100
+ def on_mlhs(node)
101
+ # Ignore / don't instrument multiple assignment (LHS).
102
+ return
103
+ end
104
+
105
+ def on_op_asgn(node)
106
+ process(node.children[2])
107
+ end
108
+
109
+ def on_or_asgn(node)
110
+ process(node.children[1])
111
+ end
112
+
113
+ def on_and_asgn(node)
114
+ process(node.children[1])
115
+ end
116
+
117
+ # Handle the method call AST node. If this method doesn't call `super`, no futher rewriting is applied to children.
118
+ def on_send(node)
119
+ # We aren't interested in top level function calls:
120
+ return if @method.empty?
121
+
122
+ if @cache.local_assignments?(node)
123
+ return super
124
+ end
125
+
126
+ # This ignores both initial block method invocation `*x*{}`, and subsequent nested invocations `x{*y*}`:
127
+ return if parent_type?(:block)
128
+
129
+ # Extract useful metadata for instrumentation:
130
+ line = node.location.line || 'line?'
131
+ column = node.location.column || 'column?' # not used
132
+ method_name = node.children[1] || '*unknown*' # not used
133
+ file_name = @source_rewriter.source_buffer.name
134
+
135
+ # Wrap the expression with instrumentation:
136
+ wrap(node.location.expression, *instrument(node.location.expression.source, file_name, line))
137
+ end
138
+
139
+ # def on_class(node)
140
+ # class_name = node.children[1]
141
+ #
142
+ # Kernel.const_get(class_name).ancestors.include? ActionController::Controller
143
+ #
144
+ # if class_name =~ /.../
145
+ # super # continue processing
146
+ # end
147
+ # end
148
+
149
+ # Invoked for every AST node as it is processed top to bottom.
150
+ def process(node)
151
+ # We are nesting inside this node:
152
+ @nesting.push(node)
153
+
154
+ if node and node.type == :def
155
+ # If the node is a method, push it on the method stack as well:
156
+ @method.push(node)
157
+ super
158
+ @method.pop
159
+ elsif node and node.type == :class
160
+ @scope.push(node.children[0])
161
+ super
162
+ @scope.pop
163
+ else
164
+ super
165
+ end
166
+
167
+ @nesting.pop
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ # Force any lazy loading to occur here, before we patch iseq_load. Otherwise you might end up in an infinite loop when rewriting code.
175
+ ScoutApm::AutoInstrument::Rails.rewrite('(preload)', '')