scout_apm 3.0.0.pre28 → 4.0.4

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