scout_apm 2.5.1 → 5.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (156) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +68 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +5 -5
  5. data/CHANGELOG.markdown +176 -3
  6. data/Gemfile +1 -7
  7. data/LICENSE.md +21 -28
  8. data/gems/README.md +28 -0
  9. data/gems/instruments.gemfile +6 -0
  10. data/gems/octoshark.gemfile +4 -0
  11. data/gems/rails3.gemfile +5 -0
  12. data/gems/rails4.gemfile +4 -0
  13. data/gems/rails5.gemfile +4 -0
  14. data/gems/rails6.gemfile +4 -0
  15. data/gems/sidekiq.gemfile +4 -0
  16. data/gems/typhoeus.gemfile +3 -0
  17. data/lib/scout_apm/agent/preconditions.rb +3 -3
  18. data/lib/scout_apm/agent.rb +22 -0
  19. data/lib/scout_apm/agent_context.rb +21 -2
  20. data/lib/scout_apm/app_server_load.rb +7 -2
  21. data/lib/scout_apm/auto_instrument/instruction_sequence.rb +31 -0
  22. data/lib/scout_apm/auto_instrument/layer.rb +23 -0
  23. data/lib/scout_apm/auto_instrument/parser.rb +27 -0
  24. data/lib/scout_apm/auto_instrument/rails.rb +174 -0
  25. data/lib/scout_apm/auto_instrument.rb +5 -0
  26. data/lib/scout_apm/background_job_integrations/delayed_job.rb +1 -1
  27. data/lib/scout_apm/background_job_integrations/faktory.rb +103 -0
  28. data/lib/scout_apm/background_job_integrations/legacy_sneakers.rb +55 -0
  29. data/lib/scout_apm/background_job_integrations/que.rb +134 -0
  30. data/lib/scout_apm/background_job_integrations/shoryuken.rb +2 -0
  31. data/lib/scout_apm/background_job_integrations/sidekiq.rb +15 -10
  32. data/lib/scout_apm/background_job_integrations/sneakers.rb +11 -11
  33. data/lib/scout_apm/config.rb +54 -6
  34. data/lib/scout_apm/detailed_trace.rb +3 -2
  35. data/lib/scout_apm/environment.rb +18 -1
  36. data/lib/scout_apm/error.rb +27 -0
  37. data/lib/scout_apm/error_service/error_buffer.rb +39 -0
  38. data/lib/scout_apm/error_service/error_record.rb +211 -0
  39. data/lib/scout_apm/error_service/ignored_exceptions.rb +66 -0
  40. data/lib/scout_apm/error_service/middleware.rb +32 -0
  41. data/lib/scout_apm/error_service/notifier.rb +33 -0
  42. data/lib/scout_apm/error_service/payload.rb +47 -0
  43. data/lib/scout_apm/error_service/periodic_work.rb +17 -0
  44. data/lib/scout_apm/error_service/railtie.rb +11 -0
  45. data/lib/scout_apm/error_service/sidekiq.rb +80 -0
  46. data/lib/scout_apm/error_service.rb +34 -0
  47. data/lib/scout_apm/exceptions.rb +12 -0
  48. data/lib/scout_apm/extensions/transaction_callback_payload.rb +1 -1
  49. data/lib/scout_apm/external_service_metric_set.rb +97 -0
  50. data/lib/scout_apm/external_service_metric_stats.rb +85 -0
  51. data/lib/scout_apm/fake_store.rb +3 -0
  52. data/lib/scout_apm/framework_integrations/rails_3_or_4.rb +7 -2
  53. data/lib/scout_apm/git_revision.rb +9 -0
  54. data/lib/scout_apm/ignored_uris.rb +3 -1
  55. data/lib/scout_apm/instant/middleware.rb +4 -1
  56. data/lib/scout_apm/instrument_manager.rb +22 -1
  57. data/lib/scout_apm/instruments/action_controller_rails_2.rb +1 -1
  58. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +53 -29
  59. data/lib/scout_apm/instruments/action_view.rb +30 -9
  60. data/lib/scout_apm/instruments/active_record.rb +69 -19
  61. data/lib/scout_apm/instruments/elasticsearch.rb +93 -42
  62. data/lib/scout_apm/instruments/grape.rb +1 -1
  63. data/lib/scout_apm/instruments/http.rb +68 -0
  64. data/lib/scout_apm/instruments/http_client.rb +33 -14
  65. data/lib/scout_apm/instruments/influxdb.rb +2 -2
  66. data/lib/scout_apm/instruments/memcached.rb +58 -0
  67. data/lib/scout_apm/instruments/middleware_detailed.rb +1 -1
  68. data/lib/scout_apm/instruments/middleware_summary.rb +1 -1
  69. data/lib/scout_apm/instruments/mongoid.rb +10 -5
  70. data/lib/scout_apm/instruments/moped.rb +44 -19
  71. data/lib/scout_apm/instruments/net_http.rb +51 -16
  72. data/lib/scout_apm/instruments/rails_router.rb +1 -1
  73. data/lib/scout_apm/instruments/redis.rb +27 -12
  74. data/lib/scout_apm/instruments/redis5.rb +59 -0
  75. data/lib/scout_apm/instruments/sinatra.rb +3 -1
  76. data/lib/scout_apm/instruments/typhoeus.rb +90 -0
  77. data/lib/scout_apm/job_record.rb +4 -2
  78. data/lib/scout_apm/layaway_file.rb +4 -0
  79. data/lib/scout_apm/layer.rb +5 -2
  80. data/lib/scout_apm/layer_children_set.rb +9 -8
  81. data/lib/scout_apm/layer_converters/external_service_converter.rb +65 -0
  82. data/lib/scout_apm/layer_converters/find_layer_by_type.rb +4 -0
  83. data/lib/scout_apm/layer_converters/request_queue_time_converter.rb +2 -0
  84. data/lib/scout_apm/layer_converters/trace_converter.rb +7 -4
  85. data/lib/scout_apm/logger.rb +5 -1
  86. data/lib/scout_apm/middleware.rb +1 -1
  87. data/lib/scout_apm/periodic_work.rb +19 -0
  88. data/lib/scout_apm/remote/message.rb +4 -0
  89. data/lib/scout_apm/remote/server.rb +13 -1
  90. data/lib/scout_apm/reporter.rb +8 -3
  91. data/lib/scout_apm/reporting.rb +2 -1
  92. data/lib/scout_apm/request_histograms.rb +8 -0
  93. data/lib/scout_apm/serializers/app_server_load_serializer.rb +4 -0
  94. data/lib/scout_apm/serializers/directive_serializer.rb +4 -0
  95. data/lib/scout_apm/serializers/external_service_serializer_to_json.rb +15 -0
  96. data/lib/scout_apm/serializers/payload_serializer.rb +4 -3
  97. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +10 -3
  98. data/lib/scout_apm/slow_policy/age_policy.rb +33 -0
  99. data/lib/scout_apm/slow_policy/percent_policy.rb +22 -0
  100. data/lib/scout_apm/slow_policy/percentile_policy.rb +24 -0
  101. data/lib/scout_apm/slow_policy/policy.rb +21 -0
  102. data/lib/scout_apm/slow_policy/speed_policy.rb +16 -0
  103. data/lib/scout_apm/slow_request_policy.rb +18 -77
  104. data/lib/scout_apm/store.rb +31 -1
  105. data/lib/scout_apm/tracer.rb +2 -2
  106. data/lib/scout_apm/tracked_request.rb +35 -4
  107. data/lib/scout_apm/utils/backtrace_parser.rb +3 -0
  108. data/lib/scout_apm/utils/marshal_logging.rb +90 -0
  109. data/lib/scout_apm/utils/sql_sanitizer.rb +47 -7
  110. data/lib/scout_apm/version.rb +1 -1
  111. data/lib/scout_apm.rb +46 -1
  112. data/scout_apm.gemspec +14 -9
  113. data/test/test_helper.rb +2 -2
  114. data/test/tmp/README.md +17 -0
  115. data/test/unit/agent_context_test.rb +29 -0
  116. data/test/unit/auto_instrument/anonymous_block_value.rb +7 -0
  117. data/test/unit/auto_instrument/assignments-instrumented.rb +31 -0
  118. data/test/unit/auto_instrument/assignments.rb +31 -0
  119. data/test/unit/auto_instrument/controller-ast.txt +57 -0
  120. data/test/unit/auto_instrument/controller-instrumented.rb +49 -0
  121. data/test/unit/auto_instrument/controller.rb +49 -0
  122. data/test/unit/auto_instrument/hanging_method.rb +6 -0
  123. data/test/unit/auto_instrument/rescue_from-instrumented.rb +13 -0
  124. data/test/unit/auto_instrument/rescue_from.rb +13 -0
  125. data/test/unit/auto_instrument_test.rb +62 -0
  126. data/test/unit/background_job_integrations/sidekiq_test.rb +17 -0
  127. data/test/unit/environment_test.rb +2 -2
  128. data/test/unit/error_service/error_buffer_test.rb +25 -0
  129. data/test/unit/error_service/ignored_exceptions_test.rb +49 -0
  130. data/test/unit/external_service_metric_set_test.rb +67 -0
  131. data/test/unit/external_service_metric_stats_test.rb +106 -0
  132. data/test/unit/ignored_uris_test.rb +6 -0
  133. data/test/unit/instruments/active_record_test.rb +40 -0
  134. data/test/unit/instruments/http_client_test.rb +24 -0
  135. data/test/unit/instruments/http_test.rb +24 -0
  136. data/test/unit/instruments/moped_test.rb +24 -0
  137. data/test/unit/instruments/net_http_test.rb +11 -1
  138. data/test/unit/instruments/redis_test.rb +24 -0
  139. data/test/unit/instruments/typhoeus_test.rb +42 -0
  140. data/test/unit/layer_children_set_test.rb +9 -0
  141. data/test/unit/remote/{test_message.rb → message_test.rb} +0 -0
  142. data/test/unit/remote/{test_router.rb → route_test.rb} +0 -0
  143. data/test/unit/remote/{test_server.rb → server_test.rb} +4 -1
  144. data/test/unit/request_histograms_test.rb +17 -0
  145. data/test/unit/serializers/payload_serializer_test.rb +39 -3
  146. data/test/unit/slow_request_policy_test.rb +41 -13
  147. data/test/unit/sql_sanitizer_test.rb +106 -0
  148. data/test/unit/tracer_test.rb +25 -0
  149. metadata +118 -60
  150. data/.travis.yml +0 -25
  151. data/lib/scout_apm/instruments/.DS_Store +0 -0
  152. data/lib/scout_apm/slow_job_policy.rb +0 -111
  153. data/lib/scout_apm/utils/sql_sanitizer_regex.rb +0 -25
  154. data/lib/scout_apm/utils/sql_sanitizer_regex_1_8_7.rb +0 -26
  155. data/test/unit/instruments/active_record_instruments_test.rb +0 -5
  156. data/test/unit/slow_job_policy_test.rb +0 -6
@@ -0,0 +1,80 @@
1
+ module ScoutApm
2
+ module ErrorService
3
+ class Sidekiq
4
+ def initialize
5
+ @context = ScoutApm::Agent.instance.context
6
+ end
7
+
8
+ def install
9
+ return false unless defined?(::Sidekiq)
10
+
11
+ if ::Sidekiq::VERSION < "3"
12
+ install_sidekiq_with_middleware
13
+ else
14
+ install_sidekiq_with_error_handler
15
+ end
16
+
17
+ true
18
+ end
19
+
20
+ def install_sidekiq_with_middleware
21
+ # old behavior
22
+ ::Sidekiq.configure_server do |config|
23
+ config.server_middleware do |chain|
24
+ chain.add ScoutApm::ErrorService::Sidekiq::SidekiqExceptionMiddleware
25
+ end
26
+ end
27
+ end
28
+
29
+ def install_sidekiq_with_error_handler
30
+ ::Sidekiq.configure_server do |config|
31
+ config.error_handlers << proc { |exception, job_info|
32
+ context = ScoutApm::Agent.instance.context
33
+
34
+ # Bail out early, and reraise if the error is not interesting.
35
+ if context.ignored_exceptions.ignored?(exception)
36
+ raise
37
+ end
38
+
39
+ job_class =
40
+ begin
41
+ job_class = job_info[:job]["class"]
42
+ job_class = job_info[:job]["args"][0]["job_class"] if job_class == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
43
+ job_class
44
+ rescue
45
+ "UnknownJob"
46
+ end
47
+
48
+ # Capture the error for further processing and shipping
49
+ context.error_buffer.capture(exception, job_info.merge(:custom_controller => job_class))
50
+ }
51
+ end
52
+ end
53
+
54
+ class SidekiqExceptionMiddleware
55
+ def call(worker, msg, queue)
56
+ yield
57
+ rescue => exception
58
+ context = ScoutApm::Agent.instance.context
59
+
60
+ # Bail out early, and reraise if the error is not interesting.
61
+ if context.ignored_exceptions.ignored?(exception)
62
+ raise
63
+ end
64
+
65
+ # Capture the error for further processing and shipping
66
+ context.error_buffer.capture(
67
+ exception,
68
+ {
69
+ :custom_params => msg,
70
+ :custom_controller => msg["class"]
71
+ }
72
+ )
73
+
74
+ # Finally, reraise
75
+ raise exception
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,34 @@
1
+ require "net/http"
2
+ require "net/https"
3
+ require "uri"
4
+
5
+ module ScoutApm
6
+ module ErrorService
7
+ API_VERSION = "1"
8
+
9
+ HEADERS = {
10
+ "Content-type" => "application/json",
11
+ "Accept" => "application/json"
12
+ }
13
+
14
+ # Public API to force a given exception to be captured.
15
+ # Still obeys the ignore list
16
+ # Used internally by SidekiqException
17
+ def self.capture(exception, params = {})
18
+ return if disabled?
19
+
20
+ context = ScoutApm::Agent.instance.context
21
+ return if context.ignored_exceptions.ignore?(exception)
22
+
23
+ context.errors_buffer.capture(exception, env)
24
+ end
25
+
26
+ def self.enabled?
27
+ ScoutApm::Agent.instance.context.config.value("errors_enabled")
28
+ end
29
+
30
+ def self.disabled?
31
+ !enabled?
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,12 @@
1
+ module ScoutApm
2
+ module AllExceptionsExceptOnesWeMustNotRescue
3
+ # Borrowed from https://github.com/rspec/rspec-support/blob/v3.8.0/lib/rspec/support.rb#L132-L140
4
+ # These exceptions are dangerous to rescue as rescuing them
5
+ # would interfere with things we should not interfere with.
6
+ AVOID_RESCUING = [NoMemoryError, SignalException, Interrupt, SystemExit]
7
+
8
+ def self.===(exception)
9
+ AVOID_RESCUING.none? { |ar| ar === exception }
10
+ end
11
+ end
12
+ end
@@ -26,7 +26,7 @@ module ScoutApm
26
26
  # The time in queue of the transaction in ms. If not present, +nil+ is returned as this is unknown.
27
27
  def queue_time_ms
28
28
  # Controller logic
29
- if converter_results[:queue_time] && converter_results[:queue].any?
29
+ if converter_results[:queue_time] && converter_results[:queue_time].any?
30
30
  converter_results[:queue_time].values.first.total_call_time*1000 # ms
31
31
  # Job logic
32
32
  elsif converter_results[:job]
@@ -0,0 +1,97 @@
1
+ # Note, this class must be Marshal Dumpable
2
+ module ScoutApm
3
+ class ExternalServiceMetricSet
4
+ include Enumerable
5
+
6
+ attr_reader :metrics # the raw metrics. You probably want #metrics_to_report
7
+
8
+ def marshal_dump
9
+ [ @metrics ]
10
+ end
11
+
12
+ def marshal_load(array)
13
+ @metrics = array.first
14
+ @context = ScoutApm::Agent.instance.context
15
+ end
16
+
17
+ def initialize(context)
18
+ @context = context
19
+
20
+ # A hash of ExternalServiceMetricStats values, keyed by ExternalServiceMetricStats.key
21
+ @metrics = Hash.new
22
+ end
23
+
24
+ # Need to look this up again if we end up as nil. Which I guess can happen
25
+ # after a Marshal load?
26
+ def context
27
+ @context ||= ScoutApm::Agent.instance.context
28
+ end
29
+
30
+ def each
31
+ metrics.each do |_key, external_service_metric_stat|
32
+ yield external_service_metric_stat
33
+ end
34
+ end
35
+
36
+ # Looks up a ExternalServiceMetricStats instance in the +@metrics+ hash. Sets the value to +other+ if no key
37
+ # Returns a ExternalServiceMetricStats instance
38
+ def lookup(other)
39
+ metrics[other.key] ||= other
40
+ end
41
+
42
+ # Take another set, and merge it with this one
43
+ def combine!(other)
44
+ other.each do |metric|
45
+ self << metric
46
+ end
47
+ self
48
+ end
49
+
50
+ # Add a single ExternalServiceMetricStats object to this set.
51
+ #
52
+ # Looks up an existing one under this key and merges, or just saves a new
53
+ # one under the key
54
+ def <<(stat)
55
+ existing_stat = metrics[stat.key]
56
+ if existing_stat
57
+ existing_stat.combine!(stat)
58
+ elsif at_limit?
59
+ # We're full up, can't add any more.
60
+ # Should I log this? It may get super noisy?
61
+ else
62
+ metrics[stat.key] = stat
63
+ end
64
+ end
65
+
66
+ def increment_transaction_count!
67
+ metrics.each do |_key, external_service_metric_stat|
68
+ external_service_metric_stat.increment_transaction_count!
69
+ end
70
+ end
71
+
72
+ def metrics_to_report
73
+ report_limit = context.config.value('external_service_metric_report_limit')
74
+ if metrics.size > report_limit
75
+ metrics.
76
+ values.
77
+ sort_by {|stat| stat.call_time }.
78
+ reverse.
79
+ take(report_limit)
80
+ else
81
+ metrics.values
82
+ end
83
+ end
84
+
85
+ def inspect
86
+ metrics.map {|key, metric|
87
+ "#{key.inspect} - Count: #{metric.call_count}, Total Time: #{"%.2f" % metric.call_time}"
88
+ }.join("\n")
89
+ end
90
+
91
+ def at_limit?
92
+ @limit ||= context.config.value('external_service_metric_limit')
93
+ metrics.size >= @limit
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,85 @@
1
+ module ScoutApm
2
+ class ExternalServiceMetricStats
3
+
4
+ DEFAULT_HISTOGRAM_SIZE = 50
5
+
6
+ attr_reader :domain_name
7
+ attr_reader :operation
8
+ attr_reader :scope
9
+
10
+ attr_reader :transaction_count
11
+
12
+ attr_reader :call_count
13
+ attr_reader :call_time
14
+
15
+ attr_reader :min_call_time
16
+ attr_reader :max_call_time
17
+
18
+ attr_reader :histogram
19
+
20
+ def initialize(domain_name, operation, scope, call_count, call_time)
21
+ @domain_name = domain_name
22
+ @operation = operation
23
+
24
+ @call_count = call_count
25
+
26
+ @call_time = call_time
27
+ @min_call_time = call_time
28
+ @max_call_time = call_time
29
+
30
+ # This histogram is for call_time
31
+ @histogram = NumericHistogram.new(DEFAULT_HISTOGRAM_SIZE)
32
+ @histogram.add(call_time)
33
+
34
+ @transaction_count = 0
35
+
36
+ @scope = scope
37
+ end
38
+
39
+ # Merge data in this scope. Used in ExternalServiceMetricSet
40
+ def key
41
+ @key ||= [domain_name, operation, scope]
42
+ end
43
+
44
+ # Combine data from another ExternalServiceMetricStats into +self+. Modifies and returns +self+
45
+ def combine!(other)
46
+ return self if other == self
47
+
48
+ @transaction_count += other.transaction_count
49
+ @call_count += other.call_count
50
+ @call_time += other.call_time
51
+
52
+ @min_call_time = other.min_call_time if @min_call_time.zero? or other.min_call_time < @min_call_time
53
+ @max_call_time = other.max_call_time if other.max_call_time > @max_call_time
54
+
55
+ @histogram.combine!(other.histogram)
56
+ self
57
+ end
58
+
59
+ def as_json
60
+ json_attributes = [
61
+ :domain_name,
62
+ :operation,
63
+ :scope,
64
+
65
+ :transaction_count,
66
+ :call_count,
67
+
68
+ :histogram,
69
+ :call_time,
70
+ :max_call_time,
71
+ :min_call_time,
72
+ ]
73
+
74
+ ScoutApm::AttributeArranger.call(self, json_attributes)
75
+ end
76
+
77
+ # Called by the Set on each ExternalServiceMetricStats object that it holds, only
78
+ # once during the recording of a transaction.
79
+ #
80
+ # Don't call elsewhere, and don't set to 1 in the initializer.
81
+ def increment_transaction_count!
82
+ @transaction_count += 1
83
+ end
84
+ end
85
+ end
@@ -26,6 +26,9 @@ module ScoutApm
26
26
  def track_db_query_metrics!(db_query_metric_set, options={})
27
27
  end
28
28
 
29
+ def track_external_service_metrics!(external_service_metric_set, options={})
30
+ end
31
+
29
32
  def track_slow_transaction!(slow_transaction)
30
33
  end
31
34
 
@@ -48,6 +48,7 @@ module ScoutApm
48
48
  when "sqlite" then :sqlite
49
49
  when "mysql" then :mysql
50
50
  when "mysql2" then :mysql
51
+ when "sqlserver" then :sqlserver
51
52
  else default
52
53
  end
53
54
  else
@@ -70,10 +71,14 @@ module ScoutApm
70
71
  #
71
72
  # We avoid this issue by not calling .respond_to? here, and instead using the less optimal `rescue nil` approach
72
73
  def raw_database_adapter
73
- adapter = ActiveRecord::Base.connection_config[:adapter].to_s rescue nil
74
+ adapter = ActiveRecord::Base.connection_db_config.configuration_hash[:adapter] rescue nil
74
75
 
75
76
  if adapter.nil?
76
- adapter = ActiveRecord::Base.configurations[env]["adapter"]
77
+ adapter = ActiveRecord::Base.connection_config[:adapter].to_s rescue nil
78
+ end
79
+
80
+ if adapter.nil?
81
+ adapter = ActiveRecord::Base.configurations.to_h[env]["adapter"]
77
82
  end
78
83
 
79
84
  return adapter
@@ -20,6 +20,7 @@ module ScoutApm
20
20
  detect_from_config ||
21
21
  detect_from_heroku ||
22
22
  detect_from_capistrano ||
23
+ detect_from_mina ||
23
24
  detect_from_git
24
25
  end
25
26
 
@@ -43,6 +44,14 @@ module ScoutApm
43
44
  nil
44
45
  end
45
46
 
47
+ # https://github.com/mina-deploy/mina
48
+ def detect_from_mina
49
+ File.read(File.join(app_root, '.mina_git_revision')).strip
50
+ rescue
51
+ logger.debug "Unable to detect Git Revision from Mina: #{$!.message}"
52
+ nil
53
+ end
54
+
46
55
  def detect_from_git
47
56
  if File.directory?(".git")
48
57
  `git rev-parse --short HEAD`.strip
@@ -4,7 +4,9 @@ module ScoutApm
4
4
  attr_reader :regex
5
5
 
6
6
  def initialize(prefixes)
7
- regexes = Array(prefixes).map {|prefix| %r{\A#{prefix}} }
7
+ regexes = Array(prefixes).
8
+ reject{|prefix| prefix == ""}.
9
+ map {|prefix| %r{\A#{prefix}} }
8
10
  @regex = Regexp.union(*regexes)
9
11
  end
10
12
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: false
2
+
1
3
  module ScoutApm
2
4
  module Instant
3
5
 
@@ -94,7 +96,8 @@ module ScoutApm
94
96
 
95
97
  def preconditions_met?
96
98
  if dev_trace_disabled?
97
- logger.debug("DevTrace: isn't activated via config. Try: SCOUT_DEV_TRACE=true rails server")
99
+ # The line below is very noise as it is called on every request.
100
+ # logger.debug("DevTrace: isn't activated via config. Try: SCOUT_DEV_TRACE=true rails server")
98
101
  return false
99
102
  end
100
103
 
@@ -30,8 +30,12 @@ module ScoutApm
30
30
  install_instrument(ScoutApm::Instruments::Moped)
31
31
  install_instrument(ScoutApm::Instruments::Mongoid)
32
32
  install_instrument(ScoutApm::Instruments::NetHttp)
33
+ install_instrument(ScoutApm::Instruments::Typhoeus)
33
34
  install_instrument(ScoutApm::Instruments::HttpClient)
35
+ install_instrument(ScoutApm::Instruments::HTTP)
36
+ install_instrument(ScoutApm::Instruments::Memcached)
34
37
  install_instrument(ScoutApm::Instruments::Redis)
38
+ install_instrument(ScoutApm::Instruments::Redis5)
35
39
  install_instrument(ScoutApm::Instruments::InfluxDB)
36
40
  install_instrument(ScoutApm::Instruments::Elasticsearch)
37
41
  install_instrument(ScoutApm::Instruments::Grape)
@@ -47,6 +51,23 @@ module ScoutApm
47
51
  (config.value("disabled_instruments") || []).include?(instrument_short_name)
48
52
  end
49
53
 
54
+ def prepend_for_instrument?(instrument_klass)
55
+ instrument_short_name = instrument_klass.name.split("::").last
56
+
57
+ # `use_prepend` defaults to false, which means we use `alias_method` by default.
58
+ # If `use_prepend` is `true`, then we should default to using `prepend` unless
59
+ # the instrument is explicitly listed in the `alias_method_instruments` config array.
60
+ if config.value("use_prepend")
61
+ return false if (config.value("alias_method_instruments") || []).include?(instrument_short_name)
62
+ return true
63
+ else
64
+ # `use_prepend` is false, but we should use `prepend` if the instrument is
65
+ # explicitly listed in the `prepend_instruments` array.
66
+ return true if (config.value("prepend_instruments") || []).include?(instrument_short_name)
67
+ return false
68
+ end
69
+ end
70
+
50
71
  private
51
72
 
52
73
  def install_instrument(instrument_klass)
@@ -59,7 +80,7 @@ module ScoutApm
59
80
 
60
81
  instance = instrument_klass.new(context)
61
82
  @installed_instruments << instance
62
- instance.install
83
+ instance.install(prepend: prepend_for_instrument?(instrument_klass))
63
84
  end
64
85
 
65
86
  def already_installed?(instrument_klass)
@@ -16,7 +16,7 @@ module ScoutApm
16
16
  @installed
17
17
  end
18
18
 
19
- def install
19
+ def install(prepend:)
20
20
  if defined?(::ActionController) && defined?(::ActionController::Base)
21
21
  @installed = true
22
22
 
@@ -17,46 +17,73 @@ module ScoutApm
17
17
  @installed
18
18
  end
19
19
 
20
- def install
21
- # We previously instrumented ActionController::Metal, which missed
22
- # before and after filter timing. Instrumenting Base includes those
23
- # filters, at the expense of missing out on controllers that don't use
24
- # the full Rails stack.
25
- if defined?(::ActionController)
26
- @installed = true
20
+ def installed!
21
+ @installed = true
22
+ end
23
+
24
+ def install(prepend:)
25
+ if !defined?(::ActiveSupport)
26
+ return
27
+ end
28
+
29
+ # The block below runs with `self` equal to the ActionController::Base or ::API module, not this class we're in now. By saving an instance of ourselves into the `this` variable, we can continue accessing what we need.
30
+ this = self
27
31
 
32
+ ActiveSupport.on_load(:action_controller) do
33
+ if this.installed?
34
+ this.logger.info("Skipping ActionController - Already Ran")
35
+ next
36
+ else
37
+ this.logger.info("Instrumenting ActionController (on_load)")
38
+ this.installed!
39
+ end
40
+
41
+ # We previously instrumented ActionController::Metal, which missed
42
+ # before and after filter timing. Instrumenting Base includes those
43
+ # filters, at the expense of missing out on controllers that don't use
44
+ # the full Rails stack.
28
45
  if defined?(::ActionController::Base)
29
- logger.info "Instrumenting ActionController::Base"
46
+ this.logger.info "Instrumenting ActionController::Base"
30
47
  ::ActionController::Base.class_eval do
31
- # include ScoutApm::Tracer
32
48
  include ScoutApm::Instruments::ActionControllerBaseInstruments
33
49
  end
34
50
  end
35
51
 
36
52
  if defined?(::ActionController::Metal)
37
- logger.info "Instrumenting ActionController::Metal"
53
+ this.logger.info "Instrumenting ActionController::Metal"
38
54
  ::ActionController::Metal.class_eval do
39
55
  include ScoutApm::Instruments::ActionControllerMetalInstruments
40
56
  end
41
57
  end
42
58
 
43
59
  if defined?(::ActionController::API)
44
- logger.info "Instrumenting ActionController::Api"
60
+ this.logger.info "Instrumenting ActionController::Api"
45
61
  ::ActionController::API.class_eval do
46
62
  include ScoutApm::Instruments::ActionControllerAPIInstruments
47
63
  end
48
64
  end
49
65
  end
50
66
 
51
- # Returns a new anonymous module each time it is called. So
52
- # we can insert this multiple times into the ancestors
53
- # stack. Otherwise it only exists the first time you include it
54
- # (under Metal, instead of under API) and we miss instrumenting
55
- # before_action callbacks
67
+ ScoutApm::Agent.instance.context.logger.info("Instrumenting ActionController (hook installed)")
56
68
  end
57
69
 
70
+ # Returns a new anonymous module each time it is called. So
71
+ # we can insert this multiple times into the ancestors
72
+ # stack. Otherwise it only exists the first time you include it
73
+ # (under Metal, instead of under API) and we miss instrumenting
74
+ # before_action callbacks
58
75
  def self.build_instrument_module
59
76
  Module.new do
77
+ # Determine the URI of this request to capture. Overridable by users in their controller.
78
+ def scout_transaction_uri(config=ScoutApm::Agent.instance.context.config)
79
+ case config.value("uri_reporting")
80
+ when 'path'
81
+ request.path # strips off the query string for more security
82
+ else # default handles filtered params
83
+ request.filtered_path
84
+ end
85
+ end
86
+
60
87
  def process_action(*args)
61
88
  req = ScoutApm::RequestManager.lookup
62
89
  current_layer = req.current_layer
@@ -68,11 +95,18 @@ module ScoutApm
68
95
  req.instant_key = instant_key
69
96
  end
70
97
 
71
- if current_layer && current_layer.type == "Controller"
72
- # Don't start a new layer if ActionController::API or ActionController::Base handled it already.
98
+ # Don't start a new layer if ActionController::API or
99
+ # ActionController::Base handled it already. Needs to account for
100
+ # any layers started during a around_action (most likely
101
+ # AutoInstrument, but could be another custom instrument)
102
+ if current_layer && (current_layer.type == "Controller" || current_layer.type == "AutoInstrument" || req.web?)
73
103
  super
74
104
  else
75
- req.annotate_request(:uri => ScoutApm::Instruments::ActionControllerRails3Rails4.scout_transaction_uri(request))
105
+ begin
106
+ uri = scout_transaction_uri
107
+ req.annotate_request(:uri => uri)
108
+ rescue
109
+ end
76
110
 
77
111
  # IP Spoofing Protection can throw an exception, just move on w/o remote ip
78
112
  if agent_context.config.value('collect_remote_ip')
@@ -95,16 +129,6 @@ module ScoutApm
95
129
  end
96
130
  end
97
131
 
98
- # Given an +ActionDispatch::Request+, formats the uri based on config settings.
99
- # XXX: Don't lookup context like this - find a way to pass it through
100
- def self.scout_transaction_uri(request, config=ScoutApm::Agent.instance.context.config)
101
- case config.value("uri_reporting")
102
- when 'path'
103
- request.path # strips off the query string for more security
104
- else # default handles filtered params
105
- request.filtered_path
106
- end
107
- end
108
132
  end
109
133
 
110
134
  module ActionControllerMetalInstruments
@@ -29,7 +29,7 @@ module ScoutApm
29
29
  context.environment.supports_module_prepend?
30
30
  end
31
31
 
32
- def install
32
+ def install(prepend:)
33
33
  return unless defined?(::ActionView) && defined?(::ActionView::PartialRenderer)
34
34
 
35
35
  if prependable?
@@ -75,28 +75,40 @@ module ScoutApm
75
75
  end
76
76
 
77
77
  module ActionViewPartialRendererInstruments
78
- def render_partial(*args)
78
+ # In Rails 6, the signature changed to pass the view & template args directly, as opposed to through the instance var
79
+ # New signature is: def render_partial(view, template)
80
+ def render_partial(*args, **kwargs)
79
81
  req = ScoutApm::RequestManager.lookup
80
82
 
81
- template_name = @template.virtual_path rescue "Unknown Partial"
83
+ maybe_template = args[1]
84
+
85
+ template_name = @template.virtual_path rescue nil # Works on Rails 3.2 -> end of Rails 5 series
86
+ template_name ||= maybe_template.virtual_path rescue nil # Works on Rails 6 -> 6.0.3.5
82
87
  template_name ||= "Unknown Partial"
83
- layer_name = template_name + "/Rendering"
84
88
 
89
+ layer_name = template_name + "/Rendering"
85
90
  layer = ScoutApm::Layer.new("View", layer_name)
86
91
  layer.subscopable!
87
92
 
88
93
  begin
89
94
  req.start_layer(layer)
90
- super(*args)
95
+ if ScoutApm::Agent.instance.context.environment.supports_kwarg_delegation?
96
+ super(*args, **kwargs)
97
+ else
98
+ super(*args)
99
+ end
91
100
  ensure
92
101
  req.stop_layer
93
102
  end
94
103
  end
95
104
 
96
- def collection_with_template(*args)
105
+ def collection_with_template(*args, **kwargs)
97
106
  req = ScoutApm::RequestManager.lookup
98
107
 
99
- template_name = @template.virtual_path rescue "Unknown Collection"
108
+ maybe_template = args[1]
109
+
110
+ template_name = @template.virtual_path rescue nil # Works on Rails 3.2 -> end of Rails 5 series
111
+ template_name ||= maybe_template.virtual_path rescue nil # Works on Rails 6 -> 6.0.3.5
100
112
  template_name ||= "Unknown Collection"
101
113
  layer_name = template_name + "/Rendering"
102
114
 
@@ -105,7 +117,11 @@ module ScoutApm
105
117
 
106
118
  begin
107
119
  req.start_layer(layer)
108
- super(*args)
120
+ if ScoutApm::Agent.instance.context.environment.supports_kwarg_delegation?
121
+ super(*args, **kwargs)
122
+ else
123
+ super(*args)
124
+ end
109
125
  ensure
110
126
  req.stop_layer
111
127
  end
@@ -113,10 +129,15 @@ module ScoutApm
113
129
  end
114
130
 
115
131
  module ActionViewTemplateRendererInstruments
132
+ # Don't forward kwargs here, since Rails 3, 4, 5, 6 don't use them, and
133
+ # it causes annoyances in the instrumentation
116
134
  def render_template(*args)
117
135
  req = ScoutApm::RequestManager.lookup
118
136
 
119
- template_name = args[0].virtual_path rescue "Unknown"
137
+ maybe_template = args[1]
138
+
139
+ template_name = args[0].virtual_path rescue nil # Works on Rails 3.2 -> end of Rails 5 series
140
+ template_name ||= maybe_template.virtual_path rescue nil # Works on Rails 6 -> 6.1.3
120
141
  template_name ||= "Unknown"
121
142
  layer_name = template_name + "/Rendering"
122
143