scout_apm 1.6.8 → 2.0.0.pre

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -1
  3. data/CHANGELOG.markdown +7 -57
  4. data/ext/allocations/allocations.c +84 -0
  5. data/ext/allocations/extconf.rb +3 -0
  6. data/lib/scout_apm/agent/reporting.rb +9 -32
  7. data/lib/scout_apm/agent.rb +45 -31
  8. data/lib/scout_apm/app_server_load.rb +1 -2
  9. data/lib/scout_apm/attribute_arranger.rb +0 -4
  10. data/lib/scout_apm/background_worker.rb +6 -9
  11. data/lib/scout_apm/bucket_name_splitter.rb +3 -3
  12. data/lib/scout_apm/call_set.rb +1 -0
  13. data/lib/scout_apm/config.rb +110 -66
  14. data/lib/scout_apm/environment.rb +16 -10
  15. data/lib/scout_apm/framework_integrations/rails_2.rb +12 -14
  16. data/lib/scout_apm/framework_integrations/rails_3_or_4.rb +5 -17
  17. data/lib/scout_apm/framework_integrations/ruby.rb +0 -4
  18. data/lib/scout_apm/framework_integrations/sinatra.rb +0 -4
  19. data/lib/scout_apm/histogram.rb +0 -20
  20. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +1 -4
  21. data/lib/scout_apm/instruments/active_record.rb +149 -8
  22. data/lib/scout_apm/instruments/mongoid.rb +5 -78
  23. data/lib/scout_apm/instruments/process/process_cpu.rb +0 -12
  24. data/lib/scout_apm/instruments/process/process_memory.rb +14 -43
  25. data/lib/scout_apm/layaway.rb +34 -134
  26. data/lib/scout_apm/layaway_file.rb +50 -27
  27. data/lib/scout_apm/layer.rb +45 -1
  28. data/lib/scout_apm/layer_converters/allocation_metric_converter.rb +17 -0
  29. data/lib/scout_apm/layer_converters/converter_base.rb +4 -6
  30. data/lib/scout_apm/layer_converters/job_converter.rb +1 -0
  31. data/lib/scout_apm/layer_converters/metric_converter.rb +2 -1
  32. data/lib/scout_apm/layer_converters/slow_job_converter.rb +42 -21
  33. data/lib/scout_apm/layer_converters/slow_request_converter.rb +58 -37
  34. data/lib/scout_apm/metric_meta.rb +1 -5
  35. data/lib/scout_apm/metric_set.rb +6 -15
  36. data/lib/scout_apm/reporter.rb +4 -6
  37. data/lib/scout_apm/serializers/metrics_to_json_serializer.rb +5 -1
  38. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +1 -3
  39. data/lib/scout_apm/serializers/slow_jobs_serializer_to_json.rb +5 -3
  40. data/lib/scout_apm/slow_job_policy.rb +19 -89
  41. data/lib/scout_apm/slow_job_record.rb +12 -20
  42. data/lib/scout_apm/slow_request_policy.rb +12 -80
  43. data/lib/scout_apm/slow_transaction.rb +16 -20
  44. data/lib/scout_apm/stackprof_tree_collapser.rb +103 -0
  45. data/lib/scout_apm/store.rb +16 -78
  46. data/lib/scout_apm/tracked_request.rb +53 -36
  47. data/lib/scout_apm/utils/active_record_metric_name.rb +2 -0
  48. data/lib/scout_apm/utils/fake_stack_prof.rb +40 -0
  49. data/lib/scout_apm/utils/klass_helper.rb +26 -0
  50. data/lib/scout_apm/utils/sql_sanitizer.rb +1 -1
  51. data/lib/scout_apm/utils/sql_sanitizer_regex.rb +2 -2
  52. data/lib/scout_apm/utils/sql_sanitizer_regex_1_8_7.rb +2 -2
  53. data/lib/scout_apm/version.rb +1 -1
  54. data/lib/scout_apm.rb +13 -7
  55. data/scout_apm.gemspec +3 -1
  56. data/test/test_helper.rb +3 -4
  57. data/test/unit/layaway_test.rb +8 -5
  58. data/test/unit/serializers/payload_serializer_test.rb +2 -2
  59. data/test/unit/slow_item_set_test.rb +1 -2
  60. data/test/unit/sql_sanitizer_test.rb +0 -6
  61. metadata +28 -20
  62. data/LICENSE.md +0 -27
  63. data/lib/scout_apm/instruments/grape.rb +0 -69
  64. data/lib/scout_apm/instruments/percentile_sampler.rb +0 -37
  65. data/lib/scout_apm/request_histograms.rb +0 -46
  66. data/lib/scout_apm/scored_item_set.rb +0 -79
  67. data/test/unit/metric_set_test.rb +0 -101
  68. data/test/unit/scored_item_set_test.rb +0 -65
  69. data/test/unit/slow_request_policy_test.rb +0 -42
@@ -7,103 +7,147 @@ require 'scout_apm/environment'
7
7
  #
8
8
  # application_root - override the detected directory of the application
9
9
  # data_file - override the default temporary storage location. Must be a location in a writable directory
10
- # host - override the default hostname detection. Default varies by environment - either system hostname, or PAAS hostname
11
- # direct_host - override the default "direct" host. The direct_host bypasses the ingestion pipeline and goes directly to the webserver, and is primarily used for features under development.
10
+ # hostname - override the default hostname detection. Default varies by environment - either system hostname, or PAAS hostname
12
11
  # key - the account key with Scout APM. Found in Settings in the Web UI
13
12
  # log_file_path - either a directory or "STDOUT".
14
13
  # log_level - DEBUG / INFO / WARN as usual
15
14
  # monitor - true or false. False prevents any instrumentation from starting
16
15
  # name - override the name reported to APM. This is the name that shows in the Web UI
17
16
  # uri_reporting - 'path' or 'full_path' default is 'full_path', which reports URL params as well as the path.
18
- # report_format - 'json' or 'marshal'. Marshal is legacy and will be removed.
17
+ # report_format - 'json' or 'marshal'. Json is default, Marshal is deprecated
19
18
  #
20
19
  # Any of these config settings can be set with an environment variable prefixed
21
20
  # by SCOUT_ and uppercasing the key: SCOUT_LOG_LEVEL for instance.
22
21
 
22
+
23
+ # Config - Made up of config overlay
24
+ # Default -> File -> Environment Var
25
+ # QUESTION: How to embed arrays or hashes into ENV?
26
+
23
27
  module ScoutApm
24
28
  class Config
25
- DEFAULTS = {
26
- 'host' => 'https://checkin.scoutapp.com',
27
- 'direct_host' => 'https://apm.scoutapp.com',
28
- 'log_level' => 'info',
29
- 'uri_reporting' => 'full_path',
30
- 'report_format' => 'json',
31
- 'disabled_instruments' => [],
32
- 'enable_background_jobs' => true,
33
- 'ignore_traces' => [],
34
- }.freeze
35
-
36
- def initialize(config_path = nil)
37
- @config_path = config_path
29
+ # Load up a config instance without attempting to load a file.
30
+ # Useful for bootstrapping.
31
+ def self.without_file
32
+ overlays = [
33
+ ConfigEnvironment.new,
34
+ ConfigDefaults.new,
35
+ ]
36
+ new(overlays)
38
37
  end
39
38
 
40
- def config_file_exists?
41
- File.exist?(config_path)
39
+ # Load up a config instance, attempting to load a yaml file. Allows a
40
+ # definite location if requested, or will attempt to load the default
41
+ # configuration file: APP_ROOT/config/scout_apm.yml
42
+ def self.with_file(file_path=nil)
43
+ overlays = [
44
+ ConfigEnvironment.new,
45
+ ConfigFile.new(file_path),
46
+ ConfigDefaults.new,
47
+ ]
48
+ new(overlays)
42
49
  end
43
50
 
44
- # Fetch a config value.
45
- # It first attempts to fetch an ENV var prefixed with 'SCOUT_',
46
- # then from the settings file.
47
- #
48
- # If you set env_only, then it will not attempt to read the config file at
49
- # all, and only read off the ENV var this is useful to break a loop during
50
- # boot, where we needed an option to set the application root.
51
- def value(key, env_only=false)
52
- value = if env_only
53
- ENV['SCOUT_' + key.upcase]
54
- else
55
- ENV['SCOUT_' + key.upcase] || setting(key)
56
- end
57
-
58
- value.to_s.strip.length.zero? ? nil : value
51
+ def initialize(overlays)
52
+ @overlays = overlays
59
53
  end
60
54
 
61
- private
55
+ def value(key)
56
+ @overlays.each do |overlay|
57
+ if result = overlay.value(key)
58
+ return result
59
+ end
60
+ end
62
61
 
63
- def config_path
64
- @config_path || File.join(ScoutApm::Environment.instance.root, "config", "scout_apm.yml")
62
+ nil
65
63
  end
66
64
 
67
- def config_file
68
- File.expand_path(config_path)
69
- end
65
+ class ConfigDefaults
66
+ DEFAULTS = {
67
+ 'host' => 'https://checkin.scoutapp.com',
68
+ 'log_level' => 'info',
69
+ 'stackprof_interval' => 20000, # microseconds, 1000 = 1 millisecond, so 20k == 20 milliseconds
70
+ 'uri_reporting' => 'full_path',
71
+ 'report_format' => 'json',
72
+ 'disabled_instruments' => [],
73
+ 'enable_background_jobs' => true,
74
+ }.freeze
70
75
 
71
- def setting(key)
72
- settings[key] || settings(true)[key]
76
+ def value(key)
77
+ DEFAULTS[key]
78
+ end
73
79
  end
74
80
 
75
- def settings(try_reload=false)
76
- (@settings.nil? || try_reload) ? @settings = load_file : @settings
81
+ class ConfigEnvironment
82
+ def value(key)
83
+ val = ENV['SCOUT_' + key.upcase]
84
+ val.to_s.strip.length.zero? ? nil : val
85
+ end
77
86
  end
78
87
 
79
- def config_environment
80
- @config_environment ||= ScoutApm::Environment.instance.env
81
- end
88
+ # Attempts to load a configuration file, and parse it as YAML. If the file
89
+ # is not found, inaccessbile, or unparsable, log a message to that effect,
90
+ # and move on.
91
+ class ConfigFile
92
+ def initialize(file_path=nil)
93
+ @resolved_file_path = file_path || determine_file_path
94
+ load_file(@resolved_file_path)
95
+ end
82
96
 
83
- def load_file
84
- settings_hash = {}
85
- begin
86
- if File.exist?(config_file)
87
- settings_hash = YAML.load(ERB.new(File.read(config_file)).result(binding))[config_environment] || {}
97
+ def value(key)
98
+ if @file_loaded
99
+ val = @settings[key]
100
+ val.to_s.strip.length.zero? ? nil : val
88
101
  else
89
- logger.warn "No config file found at [#{config_file}]."
102
+ nil
90
103
  end
91
- rescue Exception => e
92
- logger.warn "Unable to load the config file."
93
- logger.warn e.message
94
- logger.warn e.backtrace
95
104
  end
96
- DEFAULTS.merge(settings_hash)
97
- end
98
105
 
99
- # if we error out early enough, we don't have access to ScoutApm's logger
100
- # in that case, be silent unless ENV['SCOUT_DEBUG'] is set, then STDOUT it
101
- def logger
102
- if defined?(ScoutApm::Agent) && (apm_log = ScoutApm::Agent.instance.logger)
103
- apm_log
104
- else
105
- require 'scout_apm/utils/null_logger'
106
- ENV['SCOUT_DEBUG'] ? Logger.new(STDOUT) : ScoutApm::Utils::NullLogger.new
106
+ private
107
+
108
+ def load_file(file)
109
+ if !File.exist?(@resolved_file_path)
110
+ logger.info("Configuration file #{file} does not exist, skipping.")
111
+ @file_loaded = false
112
+ return
113
+ end
114
+
115
+ if !app_environment
116
+ logger.info("Could not determine application environment, aborting configuration file load")
117
+ @file_loaded = false
118
+ return
119
+ end
120
+
121
+ begin
122
+ raw_file = File.read(@resolved_file_path)
123
+ erb_file = ERB.new(raw_file).result(binding)
124
+ parsed_yaml = YAML.load(erb_file)
125
+ @settings = parsed_yaml[app_environment]
126
+
127
+ if !@settings.is_a? Hash
128
+ raise ("Missing environment key for: #{app_environment}. This can happen if the key is missing, or with a malformed configuration file," +
129
+ " check that there is a top level #{app_environment} key.")
130
+ end
131
+
132
+ logger.info("Loaded Configuration: #{@resolved_file_path}. Using environment: #{app_environment}")
133
+ @file_loaded = true
134
+ rescue Exception => e # Explicit `Exception` handling to catch SyntaxError and anything else that ERB or YAML may throw
135
+ logger.info("Failed loading configuration file: #{e.message}. ScoutAPM will continue starting with configuration from ENV and defaults")
136
+ @file_loaded = false
137
+ end
138
+ end
139
+
140
+ def determine_file_path
141
+ File.join(ScoutApm::Environment.instance.root, "config", "scout_apm.yml")
142
+ end
143
+
144
+ def app_environment
145
+ ScoutApm::Environment.instance.env
146
+ end
147
+
148
+ # TODO: Make this better
149
+ def logger
150
+ ScoutApm::Agent.instance.logger || Logger.new(STDOUT)
107
151
  end
108
152
  end
109
153
  end
@@ -70,10 +70,6 @@ module ScoutApm
70
70
  framework_integration.database_engine
71
71
  end
72
72
 
73
- def raw_database_adapter
74
- framework_integration.raw_database_adapter
75
- end
76
-
77
73
  def processors
78
74
  @processors ||= begin
79
75
  proc_file = '/proc/cpuinfo'
@@ -88,12 +84,11 @@ module ScoutApm
88
84
  end
89
85
 
90
86
  def root
91
- return deploy_integration.root if deploy_integration
92
- framework_root
87
+ @root ||= deploy_integration? ? deploy_integration.root : framework_root
93
88
  end
94
89
 
95
90
  def framework_root
96
- if override_root = Agent.instance.config.value("application_root", true)
91
+ if override_root = Agent.instance.config.value("application_root")
97
92
  return override_root
98
93
  end
99
94
  if framework == :rails
@@ -108,8 +103,7 @@ module ScoutApm
108
103
  end
109
104
 
110
105
  def hostname
111
- config_hostname = Agent.instance.config.value("hostname", !Agent.instance.config.config_file_exists?)
112
- @hostname ||= config_hostname || platform_integration.hostname
106
+ @hostname ||= Agent.instance.config.value("hostname") || platform_integration.hostname
113
107
  end
114
108
 
115
109
  # Returns the whole integration object
@@ -134,7 +128,7 @@ module ScoutApm
134
128
  end
135
129
 
136
130
  def background_job_integration
137
- if Agent.instance.config.value("enable_background_jobs", !Agent.instance.config.config_file_exists?)
131
+ if Agent.instance.config.value("enable_background_jobs")
138
132
  @background_job_integration ||= BACKGROUND_JOB_INTEGRATIONS.detect {|integration| integration.present?}
139
133
  else
140
134
  nil
@@ -175,6 +169,18 @@ module ScoutApm
175
169
  @ruby_2 ||= defined?(RUBY_VERSION) && RUBY_VERSION.match(/^2/)
176
170
  end
177
171
 
172
+ # Returns a string representation of the OS (ex: darwin, linux)
173
+ def os
174
+ return @os if @os
175
+ raw_os = RbConfig::CONFIG['target_os']
176
+ match = raw_os.match(/([a-z]+)/)
177
+ if match
178
+ @os = match[1]
179
+ else
180
+ @os = raw_os
181
+ end
182
+ end
183
+
178
184
  ### framework checks
179
185
 
180
186
  def sinatra?
@@ -37,14 +37,18 @@ module ScoutApm
37
37
  default = :mysql
38
38
 
39
39
  if defined?(ActiveRecord::Base)
40
- case raw_database_adapter
41
- when "postgres" then :postgres
42
- when "postgresql" then :postgres
43
- when "postgis" then :postgres
44
- when "sqlite3" then :sqlite
45
- when "mysql" then :mysql
46
- when "mysql2" then :mysql
47
- else default
40
+ config = ActiveRecord::Base.configurations[env]
41
+ if config && config["adapter"]
42
+ case config["adapter"].to_s
43
+ when "postgres" then :postgres
44
+ when "postgresql" then :postgres
45
+ when "postgis" then :postgres
46
+ when "sqlite3" then :sqlite
47
+ when "mysql" then :mysql
48
+ else default
49
+ end
50
+ else
51
+ default
48
52
  end
49
53
  else
50
54
  default
@@ -52,12 +56,6 @@ module ScoutApm
52
56
  rescue
53
57
  default
54
58
  end
55
-
56
- def raw_database_adapter
57
- ActiveRecord::Base.configurations[env]["adapter"]
58
- rescue
59
- nil
60
- end
61
59
  end
62
60
  end
63
61
  end
@@ -34,19 +34,17 @@ module ScoutApm
34
34
 
35
35
  def database_engine
36
36
  return @database_engine if @database_engine
37
- default = :postgres
37
+ default = :mysql
38
38
 
39
39
  @database_engine = if defined?(ActiveRecord::Base)
40
- adapter = raw_database_adapter # can be nil
40
+ adapter = get_database_adapter # can be nil
41
41
 
42
42
  case adapter.to_s
43
43
  when "postgres" then :postgres
44
44
  when "postgresql" then :postgres
45
45
  when "postgis" then :postgres
46
46
  when "sqlite3" then :sqlite
47
- when "sqlite" then :sqlite
48
47
  when "mysql" then :mysql
49
- when "mysql2" then :mysql
50
48
  else default
51
49
  end
52
50
  else
@@ -55,21 +53,11 @@ module ScoutApm
55
53
  end
56
54
  end
57
55
 
58
- def raw_database_adapter
59
- adapter = if ActiveRecord::Base.respond_to?(:connection_config)
60
- ActiveRecord::Base.connection_config[:adapter].to_s
61
- else
62
- nil
63
- end
64
-
65
- if adapter.nil?
66
- adapter = ActiveRecord::Base.configurations[env]["adapter"]
67
- end
68
-
69
- return adapter
56
+ def get_database_adapter
57
+ ActiveRecord::Base.configurations[env]["adapter"]
70
58
  rescue # this would throw an exception if ActiveRecord::Base is defined but no configuration exists.
71
59
  nil
72
60
  end
73
61
  end
74
62
  end
75
- end
63
+ end
@@ -30,10 +30,6 @@ module ScoutApm
30
30
  def database_engine
31
31
  :mysql
32
32
  end
33
-
34
- def raw_database_adapter
35
- :mysql
36
- end
37
33
  end
38
34
  end
39
35
  end
@@ -37,10 +37,6 @@ module ScoutApm
37
37
  def database_engine
38
38
  :mysql
39
39
  end
40
-
41
- def raw_database_adapter
42
- :mysql
43
- end
44
40
  end
45
41
  end
46
42
  end
@@ -56,26 +56,6 @@ module ScoutApm
56
56
  end
57
57
  end
58
58
 
59
- # Given a value, where in this histogram does it fall?
60
- # Returns a float between 0 and 1
61
- def approximate_quantile_of_value(v)
62
- mutex.synchronize do
63
- return 100 if total == 0
64
-
65
- count_examined = 0
66
-
67
- bins.each_with_index do |bin, index|
68
- if v <= bin.value
69
- break
70
- end
71
-
72
- count_examined += bin.count
73
- end
74
-
75
- count_examined / total.to_f
76
- end
77
- end
78
-
79
59
  def mean
80
60
  mutex.synchronize do
81
61
  if total == 0
@@ -61,10 +61,7 @@ module ScoutApm
61
61
  req = ScoutApm::RequestManager.lookup
62
62
  path = ScoutApm::Agent.instance.config.value("uri_reporting") == 'path' ? request.path : request.fullpath
63
63
  req.annotate_request(:uri => path)
64
-
65
- # IP Spoofing Protection can throw an exception, just move on w/o remote ip
66
- req.context.add_user(:ip => request.remote_ip) rescue nil
67
-
64
+ req.context.add_user(:ip => request.remote_ip)
68
65
  req.set_headers(request.headers)
69
66
  req.web!
70
67
 
@@ -27,14 +27,44 @@ module ScoutApm
27
27
  end
28
28
 
29
29
  def add_instruments
30
- if defined?(::ActiveRecord) && defined?(::ActiveRecord::Base)
30
+ # Setup Tracer on AR::Base
31
+ if Utils::KlassHelper.defined?("ActiveRecord::Base")
32
+ ::ActiveRecord::Base.class_eval do
33
+ include ::ScoutApm::Tracer
34
+ end
35
+ end
36
+
37
+ # Install #log tracing
38
+ if Utils::KlassHelper.defined?("ActiveRecord::ConnectionAdapters::AbstractAdapter")
31
39
  ::ActiveRecord::ConnectionAdapters::AbstractAdapter.module_eval do
32
40
  include ::ScoutApm::Instruments::ActiveRecordInstruments
33
41
  include ::ScoutApm::Tracer
34
42
  end
43
+ end
44
+
45
+ if Utils::KlassHelper.defined?("ActiveRecord::Querying")
46
+ ::ActiveRecord::Querying.module_eval do
47
+ include ::ScoutApm::Tracer
48
+ include ::ScoutApm::Instruments::ActiveRecordQueryingInstruments
49
+ end
50
+ end
35
51
 
36
- ::ActiveRecord::Base.class_eval do
52
+ if Utils::KlassHelper.defined?("ActiveRecord::FinderMethods")
53
+ ::ActiveRecord::FinderMethods.module_eval do
37
54
  include ::ScoutApm::Tracer
55
+ include ::ScoutApm::Instruments::ActiveRecordFinderMethodsInstruments
56
+ end
57
+ end
58
+
59
+ if Utils::KlassHelper.defined?("ActiveSupport::Notifications")
60
+ ActiveSupport::Notifications.subscribe("instantiation.active_record") do |event_name, start, stop, uuid, payload|
61
+ req = ScoutApm::RequestManager.lookup
62
+ layer = req.current_layer
63
+ if layer && layer.type == "ActiveRecord"
64
+ layer.annotate_layer(payload)
65
+ else
66
+ ScoutApm::Agent.instance.logger.debug("Expected layer type: ActiveRecord, got #{layer && layer.type}")
67
+ end
38
68
  end
39
69
  end
40
70
  rescue
@@ -44,6 +74,13 @@ module ScoutApm
44
74
 
45
75
  # Contains ActiveRecord instrument, aliasing +ActiveRecord::ConnectionAdapters::AbstractAdapter#log+ calls
46
76
  # to trace calls to the database.
77
+ ################################################################################
78
+ # #log instrument.
79
+ #
80
+ # #log is very close to where AR calls out to the database itself. We have access
81
+ # to the real SQL, and an AR generated "name" for the Query
82
+ #
83
+ ################################################################################
47
84
  module ActiveRecordInstruments
48
85
  def self.included(instrumented_class)
49
86
  ScoutApm::Agent.instance.logger.info "Instrumenting #{instrumented_class.inspect}"
@@ -51,19 +88,123 @@ module ScoutApm
51
88
  unless instrumented_class.method_defined?(:log_without_scout_instruments)
52
89
  alias_method :log_without_scout_instruments, :log
53
90
  alias_method :log, :log_with_scout_instruments
54
- protected :log
55
91
  end
56
92
  end
57
- end # self.included
93
+ end
58
94
 
59
95
  def log_with_scout_instruments(*args, &block)
96
+ # Extract data from the arguments
60
97
  sql, name = args
61
- self.class.instrument("ActiveRecord",
62
- Utils::ActiveRecordMetricName.new(sql, name),
63
- :desc => Utils::SqlSanitizer.new(sql) ) do
98
+ metric_name = Utils::ActiveRecordMetricName.new(sql, name)
99
+ desc = Utils::SqlSanitizer.new(sql)
100
+
101
+ # Get current ScoutApm context
102
+ req = ScoutApm::RequestManager.lookup
103
+ current_layer = req.current_layer
104
+
105
+
106
+ # If we call #log, we have a real query to run, and we've already
107
+ # gotten through the cache gatekeeper. Since we want to only trace real
108
+ # queries, and not repeated identical queries that just hit cache, we
109
+ # mark layer as ignorable initially in #find_by_sql, then only when we
110
+ # know it's a real database call do we mark it back as usable.
111
+ #
112
+ # This flag is later used in SlowRequestConverter to skip adding ignorable layers
113
+ current_layer.annotate_layer(:ignorable => false) if current_layer
114
+
115
+ # Either: update the current layer and yield, don't start a new one.
116
+ if current_layer && current_layer.type == "ActiveRecord"
117
+ # TODO: Get rid of call .to_s, need to find this without forcing a previous run of the name logic
118
+ if current_layer.name.to_s == Utils::ActiveRecordMetricName::DEFAULT_METRIC
119
+ current_layer.name = metric_name
120
+ current_layer.desc = desc
121
+ end
122
+
64
123
  log_without_scout_instruments(sql, name, &block)
124
+
125
+ # OR: Start a new layer, we didn't pick up instrumentation earlier in the stack.
126
+ else
127
+ layer = ScoutApm::Layer.new("ActiveRecord", metric_name)
128
+ layer.desc = desc
129
+ req.start_layer(layer)
130
+ begin
131
+ log_without_scout_instruments(sql, name, &block)
132
+ ensure
133
+ req.stop_layer
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ ################################################################################
140
+ # Entry-point of instruments.
141
+ #
142
+ # We instrument both ActiveRecord::Querying#find_by_sql and
143
+ # ActiveRecord::FinderMethods#find_with_associations. These are early in
144
+ # the chain of calls when you're using ActiveRecord.
145
+ #
146
+ # Later on, they will call into #log, which we also instrument, at which
147
+ # point, we can fill in additional data gathered at that point (name, sql)
148
+ #
149
+ # Caveats:
150
+ # * We don't have a name for the query yet.
151
+ # * The query hasn't hit the cache yet. In the case of a cache hit, we
152
+ # won't hit #log, so won't get a name, leaving the misleading default.
153
+ # * One call here can result in several calls to #log, especially in the
154
+ # case where Rails needs to load the schema details for the table being
155
+ # queried.
156
+ ################################################################################
157
+
158
+ module ActiveRecordQueryingInstruments
159
+ def self.included(instrumented_class)
160
+ ScoutApm::Agent.instance.logger.info "Instrumenting ActiveRecord::Querying - #{instrumented_class.inspect}"
161
+ instrumented_class.class_eval do
162
+ unless instrumented_class.method_defined?(:find_by_sql_without_scout_instruments)
163
+ alias_method :find_by_sql_without_scout_instruments, :find_by_sql
164
+ alias_method :find_by_sql, :find_by_sql_with_scout_instruments
165
+ end
166
+ end
167
+ end
168
+
169
+ def find_by_sql_with_scout_instruments(*args, &block)
170
+ req = ScoutApm::RequestManager.lookup
171
+ layer = ScoutApm::Layer.new("ActiveRecord", Utils::ActiveRecordMetricName::DEFAULT_METRIC)
172
+ layer.annotate_layer(:ignorable => true)
173
+ req.start_layer(layer)
174
+ req.ignore_children!
175
+ begin
176
+ find_by_sql_without_scout_instruments(*args)
177
+ ensure
178
+ req.acknowledge_children!
179
+ req.stop_layer
65
180
  end
66
181
  end
67
- end # module ActiveRecordInstruments
182
+ end
183
+
184
+ module ActiveRecordFinderMethodsInstruments
185
+ def self.included(instrumented_class)
186
+ ScoutApm::Agent.instance.logger.info "Instrumenting ActiveRecord::FinderMethods - #{instrumented_class.inspect}"
187
+ instrumented_class.class_eval do
188
+ unless instrumented_class.method_defined?(:find_with_associations_without_scout_instruments)
189
+ alias_method :find_with_associations_without_scout_instruments, :find_with_associations
190
+ alias_method :find_with_associations, :find_with_associations_with_scout_instruments
191
+ end
192
+ end
193
+ end
194
+
195
+ def find_with_associations_with_scout_instruments(*args, &block)
196
+ req = ScoutApm::RequestManager.lookup
197
+ layer = ScoutApm::Layer.new("ActiveRecord", Utils::ActiveRecordMetricName::DEFAULT_METRIC)
198
+ layer.annotate_layer(:ignorable => true)
199
+ req.start_layer(layer)
200
+ req.ignore_children!
201
+ begin
202
+ find_with_associations_without_scout_instruments(*args)
203
+ ensure
204
+ req.acknowledge_children!
205
+ req.stop_layer
206
+ end
207
+ end
208
+ end
68
209
  end
69
210
  end